Merge branch 'search_and_keywords' of https://github.com/smilerz/recipes into smilerz-search_and_keywords

# Conflicts:
#	cookbook/admin.py
#	cookbook/helper/recipe_search.py
#	cookbook/integration/chowdown.py
#	cookbook/integration/integration.py
#	cookbook/management/commands/rebuildindex.py
#	cookbook/managers.py
#	cookbook/migrations/0142_build_full_text_index.py
#	cookbook/models.py
#	cookbook/schemas.py
#	cookbook/serializer.py
#	cookbook/static/vue/css/keyword_list_view.css
#	cookbook/static/vue/js/chunk-vendors.js
#	cookbook/static/vue/js/keyword_list_view.js
#	cookbook/tests/api/test_api_keyword.py
#	cookbook/views/api.py
#	cookbook/views/data.py
#	cookbook/views/views.py
#	recipes/settings.py
#	vue/package.json
#	vue/src/apps/KeywordListView/KeywordListView.vue
#	vue/src/components/KeywordCard.vue
#	vue/src/locales/en.json
#	vue/src/utils/openapi/api.ts
#	vue/yarn.lock
This commit is contained in:
vabene1111 2021-08-22 13:14:23 +02:00
commit 43dbb1d973
74 changed files with 1599 additions and 463 deletions

View File

@ -18,8 +18,6 @@ from .models import (Comment, CookLog, Food, Ingredient, InviteLink, Keyword,
from cookbook.managers import DICTIONARY
from cookbook.managers import DICTIONARY
class CustomUserAdmin(UserAdmin):
def has_add_permission(self, request, obj=None):

View File

@ -32,7 +32,7 @@ def rescale_image_png(image_object, base_width=720):
def get_filetype(name):
try:
return os.path.splitext(name)[1]
except:
except Exception:
return '.jpeg'

View File

@ -3,8 +3,6 @@ Source: https://djangosnippets.org/snippets/1703/
"""
from django.conf import settings
from django.core.cache import caches
from django.views.generic.detail import SingleObjectTemplateResponseMixin
from django.views.generic.edit import ModelFormMixin
from cookbook.models import ShareLink
from django.contrib import messages
@ -64,7 +62,7 @@ def is_object_owner(user, obj):
return False
try:
return obj.get_owner() == user
except:
except Exception:
return False

View File

@ -4,7 +4,7 @@ from recipes import settings
from django.contrib.postgres.search import (
SearchQuery, SearchRank, TrigramSimilarity
)
from django.db.models import Q, Subquery, Case, When, Value
from django.db.models import Count, Q, Subquery, Case, When, Value
from django.utils import translation
from cookbook.managers import DICTIONARY
@ -14,6 +14,7 @@ from cookbook.models import Food, Keyword, ViewLog
def search_recipes(request, queryset, params):
search_prefs = request.user.searchpreference
search_string = params.get('query', '')
search_ratings = params.getlist('ratings', [])
search_keywords = params.getlist('keywords', [])
search_foods = params.getlist('foods', [])
search_books = params.getlist('books', [])
@ -24,8 +25,10 @@ def search_recipes(request, queryset, params):
search_internal = params.get('internal', None)
search_random = params.get('random', False)
search_new = params.get('new', False)
search_last_viewed = int(params.get('last_viewed', 0))
# TODO update this to concat with full search queryset qs1 | qs2
if search_last_viewed > 0:
last_viewed_recipes = ViewLog.objects.filter(
created_by=request.user, space=request.space,
@ -34,13 +37,16 @@ def search_recipes(request, queryset, params):
return queryset.filter(pk__in=last_viewed_recipes[len(last_viewed_recipes) - min(len(last_viewed_recipes), search_last_viewed):])
orderby = []
if search_new == 'true':
queryset = queryset.annotate(
new_recipe=Case(When(
created_at__gte=(datetime.now() - timedelta(days=7)), then=Value(100)),
default=Value(0), )).order_by('-new_recipe', 'name')
new_recipe=Case(When(created_at__gte=(datetime.now() - timedelta(days=7)), then=Value(100)),
default=Value(0), ))
orderby += ['new_recipe']
else:
queryset = queryset
search_type = search_prefs.search or 'plain'
search_sort = None
if len(search_string) > 0:
unaccent_include = search_prefs.unaccent.values_list('field', flat=True)
@ -106,22 +112,30 @@ def search_recipes(request, queryset, params):
else:
query_filter = f
# TODO this is kind of a dumb method to sort. create settings to choose rank vs most often made, date created or rating
# 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)
)
queryset = queryset.filter(query_filter).annotate(rank=search_rank)
orderby += ['-rank']
else:
queryset = queryset.filter(name__icontains=search_string)
queryset = queryset.filter(query_filter)
if len(search_keywords) > 0:
# TODO creating setting to include descendants of keywords a setting
if search_keywords_or == 'true':
# when performing an 'or' search all descendants are included in the OR condition
# so descendants are appended to filter all at once
for kw in Keyword.objects.filter(pk__in=search_keywords):
search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
queryset = queryset.filter(keywords__id__in=search_keywords)
else:
for k in search_keywords:
queryset = queryset.filter(keywords__id=k)
# when performing an 'and' search returned recipes should include a parent OR any of its descedants
# AND other keywords selected so filters are appended using keyword__id__in the list of keywords and descendants
for kw in Keyword.objects.filter(pk__in=search_keywords):
queryset = queryset.filter(keywords__id__in=list(kw.get_descendants_and_self().values_list('pk', flat=True)))
if len(search_foods) > 0:
if search_foods_or == 'true':
@ -144,11 +158,122 @@ def search_recipes(request, queryset, params):
if search_random == 'true':
queryset = queryset.order_by("?")
elif search_sort == 'rank':
queryset = queryset.order_by('-rank')
else:
# TODO add order by user settings
orderby += ['name']
queryset = queryset.order_by(*orderby)
return queryset
# this returns a list of keywords in the queryset and how many times it appears
# Keyword.objects.filter(recipe__in=queryset).annotate(kw_count=Count('recipe'))
def get_facet(qs, params):
facets = {}
ratings = params.getlist('ratings', [])
keyword_list = params.getlist('keywords', [])
ingredient_list = params.getlist('ingredient', [])
book_list = params.getlist('book', [])
# this returns a list of keywords in the queryset and how many times it appears
kws = Keyword.objects.filter(recipe__in=qs).annotate(kw_count=Count('recipe'))
# custom django-tree function annotates a queryset to make building a tree easier.
# see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details
kw_a = annotated_qs(kws, root=True, fill=True)
# TODO add rating facet
facets['Ratings'] = []
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
# TODO add food facet
facets['Ingredients'] = []
# TODO add book facet
facets['Books'] = []
return facets
def fill_annotated_parents(annotation, filters):
tree_list = []
parent = []
i = 0
level = -1
for r in annotation:
expand = False
annotation[i][1]['id'] = r[0].id
annotation[i][1]['name'] = r[0].name
annotation[i][1]['count'] = getattr(r[0], 'kw_count', 0)
annotation[i][1]['isDefaultExpanded'] = False
if str(r[0].id) in filters:
expand = True
if r[1]['level'] < level:
parent = parent[:r[1]['level'] - level]
parent[-1] = i
level = r[1]['level']
elif r[1]['level'] > level:
parent.extend([i])
level = r[1]['level']
else:
parent[-1] = i
j = 0
while j < level:
# this causes some double counting when a recipe has both a child and an ancestor
annotation[parent[j]][1]['count'] += getattr(r[0], 'kw_count', 0)
if expand:
annotation[parent[j]][1]['isDefaultExpanded'] = True
j += 1
if level == 0:
tree_list.append(annotation[i][1])
elif level > 0:
annotation[parent[level - 1]][1].setdefault('children', []).append(annotation[i][1])
i += 1
return tree_list
def annotated_qs(qs, root=False, fill=False):
"""
Gets an annotated list from a queryset.
:param root:
Will backfill in annotation to include all parents to root node.
:param fill:
Will fill in gaps in annotation where nodes between children
and ancestors are not included in the queryset.
"""
result, info = [], {}
start_depth, prev_depth = (None, None)
nodes_list = list(qs.values_list('pk', flat=True))
for node in qs.order_by('path'):
node_queue = [node]
while len(node_queue) > 0:
dirty = False
current_node = node_queue[-1]
depth = current_node.get_depth()
parent_id = current_node.parent
if root and depth > 1 and parent_id not in nodes_list:
parent_id = current_node.parent
nodes_list.append(parent_id)
node_queue.append(current_node.__class__.objects.get(pk=parent_id))
dirty = True
if fill and depth > 1 and prev_depth and depth > prev_depth and parent_id not in nodes_list:
nodes_list.append(parent_id)
node_queue.append(current_node.__class__.objects.get(pk=parent_id))
dirty = True
if not dirty:
working_node = node_queue.pop()
if start_depth is None:
start_depth = depth
open = (depth and (prev_depth is None or depth > prev_depth))
if prev_depth is not None and depth < prev_depth:
info['close'] = list(range(0, prev_depth - depth))
info = {'open': open, 'close': [], 'level': depth - start_depth}
result.append((working_node, info,))
prev_depth = depth
if start_depth and start_depth > 0:
info['close'] = list(range(0, prev_depth - start_depth + 1))
return result

View File

@ -1,4 +1,3 @@
from django.shortcuts import redirect
from django.urls import reverse
from django_scopes import scope, scopes_disabled

View File

@ -30,7 +30,6 @@ def text_scraper(text, url=None):
url=None
):
self.wild_mode = False
# self.exception_handling = None # TODO add new method here, old one was deprecated
self.meta_http_equiv = False
self.soup = BeautifulSoup(page_data, "html.parser")
self.url = url

View File

@ -6,6 +6,7 @@ from cookbook.helper.mdx_urlize import UrlizeExtension
from jinja2 import Template, TemplateSyntaxError, UndefinedError
from gettext import gettext as _
class IngredientObject(object):
amount = ""
unit = ""

View File

@ -1,11 +1,6 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient
class Pepperplate(Integration):

View File

@ -1,10 +1,8 @@
import re
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
from cookbook.models import Recipe, Step, Ingredient
class ChefTap(Integration):

View File

@ -1,4 +1,3 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
@ -6,7 +5,7 @@ from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient, Keyword
class Chowdown(Integration):
@ -52,7 +51,7 @@ class Chowdown(Integration):
for k in tags.split(','):
print(f'adding keyword {k.strip()}')
keyword, created = Keyword.get_or_create(name=k.strip(), space=self.request.space)
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
recipe.keywords.add(keyword)
step = Step.objects.create(

View File

@ -1,7 +1,5 @@
import datetime
import json
import os
import re
import traceback
import uuid
from io import BytesIO, StringIO
@ -16,7 +14,7 @@ from django_scopes import scope
from cookbook.forms import ImportExportBase
from cookbook.helper.image_processing import get_filetype
from cookbook.models import Keyword, Recipe
from recipes.settings import DEBUG
from recipes.settings import DATABASES, DEBUG
class Integration:
@ -33,8 +31,29 @@ class Integration:
"""
self.request = request
self.export_type = export_type
# TODO add all import keywords under the importer root node
self.keyword = Keyword.objects.first()
name = f'Import {export_type}'
description = f'Imported by {request.user.get_user_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}. Type: {export_type}'
icon = '📥'
count = Keyword.objects.filter(name__icontains=name, space=request.space).count()
if count != 0:
pk = Keyword.objects.filter(name__icontains=name, space=request.space).order_by('id').first().id
name = name + " " + str(pk)
if DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
parent = Keyword.objects.get_or_create(name='Import', space=request.space)
parent.add_child(
name=name,
description=description,
icon=icon,
space=request.space
)
else:
self.keyword = Keyword.objects.create(
name=name,
description=description,
icon=icon,
space=request.space
)
def do_export(self, recipes):
"""
@ -181,7 +200,7 @@ class Integration:
except BadZipFile:
il.msg += 'ERROR ' + _(
'Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n'
except:
except Exception as e:
msg = 'ERROR ' + _(
'An unexpected error occurred during the import. Please make sure you have uploaded a valid file.') + '\n'
self.handle_exception(e, log=il, message=msg)

View File

@ -6,7 +6,7 @@ from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
from cookbook.models import Recipe, Step, Ingredient
class Mealie(Integration):
@ -24,6 +24,7 @@ class Mealie(Integration):
created_by=self.request.user, internal=True, 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
ingredients_added = False
for s in recipe_json['recipe_instructions']:
@ -50,7 +51,7 @@ class Mealie(Integration):
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
except:
except Exception:
pass
recipe.steps.add(step)
@ -59,7 +60,7 @@ class Mealie(Integration):
import_zip = ZipFile(f['file'])
try:
self.import_recipe_image(recipe, BytesIO(import_zip.read(f'recipes/{recipe_json["slug"]}/images/min-original.webp')), filetype=get_filetype(f'recipes/{recipe_json["slug"]}/images/original'))
except:
except Exception:
pass
return recipe

View File

@ -1,11 +1,8 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient, Keyword
class MealMaster(Integration):

View File

@ -6,7 +6,7 @@ from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
from cookbook.models import Recipe, Step, Ingredient
class NextcloudCookbook(Integration):
@ -25,6 +25,7 @@ class NextcloudCookbook(Integration):
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
ingredients_added = False

View File

@ -1,11 +1,8 @@
import json
import re
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
from cookbook.models import Recipe, Step, Ingredient
class OpenEats(Integration):

View File

@ -1,16 +1,14 @@
import re
import json
import base64
import requests
from io import BytesIO
from zipfile import ZipFile
import imghdr
from django.utils.translation import gettext as _
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient, Keyword
class RecetteTek(Integration):
@ -108,7 +106,7 @@ class RecetteTek(Integration):
recipe.keywords.add(k)
recipe.save()
except Exception as e:
pass
print(recipe.name, ': failed to parse keywords ', str(e))
# TODO: Parse Nutritional Information
@ -123,7 +121,7 @@ class RecetteTek(Integration):
else:
if file['originalPicture'] != '':
response = requests.get(file['originalPicture'])
if imghdr.what(BytesIO(response.content)) != None:
if imghdr.what(BytesIO(response.content)) is not None:
self.import_recipe_image(recipe, BytesIO(response.content), filetype=get_filetype(file['originalPicture']))
else:
raise Exception("Original image failed to download.")

View File

@ -3,12 +3,10 @@ from bs4 import BeautifulSoup
from io import BytesIO
from zipfile import ZipFile
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.recipe_url_import import parse_servings, iso_duration_to_minutes
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient, Keyword
class RecipeKeeper(Integration):
@ -61,7 +59,6 @@ class RecipeKeeper(Integration):
if file.find("span", {"itemprop": "recipeSource"}).text != '':
step.instruction += "\n\nImported from: " + file.find("span", {"itemprop": "recipeSource"}).text
step.save()
source_url_added = True
recipe.steps.add(step)
@ -72,7 +69,7 @@ class RecipeKeeper(Integration):
import_zip = ZipFile(f['file'])
self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipe-photo").get("src"))), filetype='.jpeg')
except Exception as e:
pass
print(recipe.name, ': failed to import image ', str(e))
return recipe

View File

@ -1,9 +1,7 @@
import base64
import json
from io import BytesIO
import requests
from rest_framework.renderers import JSONRenderer
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration

View File

@ -1,11 +1,6 @@
import json
import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
from cookbook.models import Recipe, Step, Ingredient, Keyword
class RezKonv(Integration):

View File

@ -2,7 +2,7 @@ from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
from cookbook.models import Recipe, Step, Ingredient
class Safron(Integration):

View File

@ -13,7 +13,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: 2021-06-22 09:12+0000\n"
"PO-Revision-Date: 2021-07-19 16:40+0000\n"
"Last-Translator: Jesse <jesse.kamps@pm.me>\n"
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/nl/>\n"
@ -22,7 +22,7 @@ msgstr ""
"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.6.2\n"
"X-Generator: Weblate 4.7.1\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:98
#: .\cookbook\templates\forms\edit_internal_recipe.html:246
@ -453,7 +453,7 @@ msgstr "Bewerken"
#: .\cookbook\templates\meal_plan.html:277
#: .\cookbook\templates\recipes_table.html:90
msgid "Delete"
msgstr "Verwijderen"
msgstr "Verwijder"
#: .\cookbook\templates\404.html:5
msgid "404 Error"
@ -1629,7 +1629,7 @@ msgid ""
"To join an existing space either enter your invite token or click on the "
"invite link the space owner send you."
msgstr ""
"Om aan te sluiten bij een bestaande ruimte moet je je uitnodigingstoken "
"Om je aan te sluiten bij een bestaande ruimte moet je jouw uitnodigingstoken "
"invoeren of op de uitnodingslink klikken die je ontvangen hebt."
#: .\cookbook\templates\no_space_info.html:48
@ -1976,7 +1976,7 @@ msgstr "Waarschuwing"
#: .\cookbook\templates\system.html:49 .\cookbook\templates\system.html:64
#: .\cookbook\templates\system.html:80 .\cookbook\templates\system.html:95
msgid "Ok"
msgstr "Ok"
msgstr "Oké"
#: .\cookbook\templates\system.html:51
msgid ""
@ -2098,6 +2098,7 @@ msgstr "Bekijk recept gegevens"
#: .\cookbook\templates\url_import.html:147
msgid "Drag recipe attributes from the right into the appropriate box below."
msgstr ""
"Sleep eigenschappen van het recept van rechts naar het juiste vlak beneden."
#: .\cookbook\templates\url_import.html:156
#: .\cookbook\templates\url_import.html:173
@ -2110,82 +2111,73 @@ msgstr ""
#: .\cookbook\templates\url_import.html:300
#: .\cookbook\templates\url_import.html:351
msgid "Clear Contents"
msgstr ""
msgstr "Wis inhoud"
#: .\cookbook\templates\url_import.html:158
msgid "Text dragged here will be appended to the name."
msgstr ""
msgstr "Hierheen gesleepte tekst wordt aan de naam toegevoegd."
#: .\cookbook\templates\url_import.html:175
msgid "Text dragged here will be appended to the description."
msgstr ""
msgstr "Hierheen gesleepte tekst wordt aan de beschrijving toegevoegd."
#: .\cookbook\templates\url_import.html:192
msgid "Keywords dragged here will be appended to current list"
msgstr ""
msgstr "Hierheen gesleepte Etiketten worden aan de huidige lijst toegevoegd"
#: .\cookbook\templates\url_import.html:207
msgid "Image"
msgstr ""
msgstr "Afbeelding"
#: .\cookbook\templates\url_import.html:239
#, fuzzy
#| msgid "Preparation Time"
msgid "Prep Time"
msgstr "Bereidingstijd"
msgstr "Voorbereidingstijd"
#: .\cookbook\templates\url_import.html:254
#, fuzzy
#| msgid "Time"
msgid "Cook Time"
msgstr "Tijd"
msgstr "Kooktijd"
#: .\cookbook\templates\url_import.html:275
msgid "Ingredients dragged here will be appended to current list."
msgstr ""
msgstr "Hierheen gesleepte Ingrediënten worden aan de huidige lijst toegevoegd."
#: .\cookbook\templates\url_import.html:302
msgid ""
"Recipe instructions dragged here will be appended to current instructions."
msgstr ""
"Hierheen gesleepte Recept instructies worden aan de huidige lijst toegevoegd."
#: .\cookbook\templates\url_import.html:325
#, fuzzy
#| msgid "Discovered Recipes"
msgid "Discovered Attributes"
msgstr "Ontdekte recepten"
msgstr "Ontdekte Eigenschappen"
#: .\cookbook\templates\url_import.html:327
msgid ""
"Drag recipe attributes from below into the appropriate box on the left. "
"Click any node to display its full properties."
msgstr ""
"Sleep recept eigenschappen van beneden naar de juiste doos aan de "
"linkerzijde. Klik er op om alle eigenschappen te zien."
#: .\cookbook\templates\url_import.html:344
#, fuzzy
#| msgid "Show as header"
msgid "Show Blank Field"
msgstr "Laat als kop zien"
msgstr "Toon Leeg Veld"
#: .\cookbook\templates\url_import.html:349
msgid "Blank Field"
msgstr ""
msgstr "Leeg Veld"
#: .\cookbook\templates\url_import.html:353
msgid "Items dragged to Blank Field will be appended."
msgstr ""
msgstr "Naar Leeg Veld gesleepte items worden toegevoegd."
#: .\cookbook\templates\url_import.html:400
#, fuzzy
#| msgid "Delete Step"
msgid "Delete Text"
msgstr "Verwijder stap"
msgstr "Verwijder tekst"
#: .\cookbook\templates\url_import.html:413
#, fuzzy
#| msgid "Delete Recipe"
msgid "Delete image"
msgstr "Verwijder recept"
msgstr "Verwijder afbeelding"
#: .\cookbook\templates\url_import.html:429
msgid "Recipe Name"
@ -2261,7 +2253,7 @@ msgstr "Er is een fout opgetreden bij het synchroniseren met Opslag"
#: .\cookbook\views\api.py:649
msgid "Nothing to do."
msgstr ""
msgstr "Niks te doen."
#: .\cookbook\views\api.py:664
msgid "The requested site provided malformed data and cannot be read."
@ -2282,26 +2274,24 @@ msgstr ""
"te importeren."
#: .\cookbook\views\api.py:694
#, fuzzy
#| msgid "The requested page could not be found."
msgid "No useable data could be found."
msgstr "De opgevraagde pagina kon niet gevonden worden."
msgstr "Er is geen bruikbare data gevonden."
#: .\cookbook\views\api.py:710
msgid "I couldn't find anything to do."
msgstr ""
msgstr "Ik kon niks vinden om te doen."
#: .\cookbook\views\data.py:30 .\cookbook\views\data.py:121
#: .\cookbook\views\edit.py:50 .\cookbook\views\import_export.py:67
#: .\cookbook\views\new.py:32
msgid "You have reached the maximum number of recipes for your space."
msgstr ""
msgstr "Je hebt het maximaal aantal recepten voor jouw ruimte bereikt."
#: .\cookbook\views\data.py:34 .\cookbook\views\data.py:125
#: .\cookbook\views\edit.py:54 .\cookbook\views\import_export.py:71
#: .\cookbook\views\new.py:36
msgid "You have more users than allowed in your space."
msgstr ""
msgstr "Je hebt meer gebruikers dan toegestaan in jouw ruimte."
#: .\cookbook\views\data.py:103
#, python-format
@ -2409,57 +2399,64 @@ msgstr "Er is een fout opgetreden bij het importeren van dit recept!"
#: .\cookbook\views\new.py:226
msgid "Hello"
msgstr ""
msgstr "Hallo"
#: .\cookbook\views\new.py:226
msgid "You have been invited by "
msgstr ""
msgstr "Je bent uitgenodigd door "
#: .\cookbook\views\new.py:227
msgid " to join their Tandoor Recipes space "
msgstr ""
msgstr " om zijn/haar Tandoor Recepten ruimte "
#: .\cookbook\views\new.py:228
msgid "Click the following link to activate your account: "
msgstr ""
msgstr "Klik om de volgende link om je account te activeren: "
#: .\cookbook\views\new.py:229
msgid ""
"If the link does not work use the following code to manually join the space: "
msgstr ""
"Als de linkt niet werkt, gebruik dan de volgende code om handmatig tot de "
"ruimte toe te treden: "
#: .\cookbook\views\new.py:230
msgid "The invitation is valid until "
msgstr ""
msgstr "De uitnodiging is geldig tot "
#: .\cookbook\views\new.py:231
msgid ""
"Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub "
msgstr ""
"Tandoor Recepten is een Open Source recepten manager. Bekijk het op GitHub "
#: .\cookbook\views\new.py:234
msgid "Tandoor Recipes Invite"
msgstr ""
msgstr "Tandoor Recepten uitnodiging"
#: .\cookbook\views\new.py:241
msgid "Invite link successfully send to user."
msgstr ""
msgstr "Uitnodigingslink succesvol verstuurd naar gebruiker."
#: .\cookbook\views\new.py:244
msgid ""
"You have send to many emails, please share the link manually or wait a few "
"hours."
msgstr ""
"Je hebt te veel e-mails verstuurd, deel de link handmatig of wacht enkele "
"uren."
#: .\cookbook\views\new.py:246
msgid "Email to user could not be send, please share link manually."
msgstr ""
msgstr "E-mail aan gebruiker kon niet verzonden worden, deel de link handmatig."
#: .\cookbook\views\views.py:125
msgid ""
"You have successfully created your own recipe space. Start by adding some "
"recipes or invite other people to join you."
msgstr ""
"Je hebt je eigen recepten ruimte succesvol aangemaakt. Start met het "
"toevoegen van recepten of nodig anderen uit om je te vergezellen."
#: .\cookbook\views\views.py:173
msgid "You do not have the required permissions to perform this action!"
@ -2493,14 +2490,12 @@ msgid "Malformed Invite Link supplied!"
msgstr "Onjuiste uitnodigingslink opgegeven!"
#: .\cookbook\views\views.py:441
#, fuzzy
#| msgid "You are not logged in and therefore cannot view this page!"
msgid "You are already member of a space and therefore cannot join this one."
msgstr "Je bent niet ingelogd en kan deze pagina daarom niet bekijken!"
msgstr "Je bent al lid van een ruimte en kan daardoor niet toetreden tot deze."
#: .\cookbook\views\views.py:452
msgid "Successfully joined space."
msgstr ""
msgstr "Succesvol toegetreden tot ruimte."
#: .\cookbook\views\views.py:458
msgid "Invite Link not valid or already used!"

View File

@ -27,5 +27,5 @@ class Command(BaseCommand):
Step.objects.all().update(search_vector=SearchVector('instruction__unaccent', weight='B', config=language))
self.stdout.write(self.style.SUCCESS(_('Recipe index rebuild complete.')))
except:
except Exception:
self.stdout.write(self.style.ERROR(_('Recipe index rebuild failed.')))

View File

@ -1,12 +1,11 @@
from django.contrib.postgres.aggregates import StringAgg
from django.contrib.postgres.search import (
SearchQuery, SearchRank, SearchVector, TrigramSimilarity,
SearchQuery, SearchRank, SearchVector,
)
from django.db import models
from django.db.models import Q
from django.utils import translation
# TODO move this somewhere else and delete this file
DICTIONARY = {
# TODO find custom dictionaries - maybe from here https://www.postgresql.org/message-id/CAF4Au4x6X_wSXFwsQYE8q5o0aQZANrvYjZJ8uOnsiHDnOVPPEg%40mail.gmail.com
# 'hy': 'Armenian',
@ -22,8 +21,6 @@ DICTIONARY = {
}
# TODO add search highlighting
# TODO add language support
# TODO add schedule index rebuild
class RecipeSearchManager(models.Manager):
def search(self, search_text, space):

View File

@ -0,0 +1,24 @@
# Generated by Django 3.2.5 on 2021-07-13 08:42
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0140_userpreference_created_at'),
]
operations = [
migrations.AddField(
model_name='step',
name='step_recipe',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.recipe'),
),
migrations.AlterField(
model_name='step',
name='type',
field=models.CharField(choices=[('TEXT', 'Text'), ('TIME', 'Time'), ('FILE', 'File'), ('RECIPE', 'Recipe')], default='TEXT', max_length=16),
),
]

View File

@ -27,7 +27,7 @@ def set_default_search_vector(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0141_keyword_to_tree'),
('cookbook', '0141_auto_20210713_1042'),
]
operations = [
migrations.AddField(

View File

@ -0,0 +1,70 @@
# Generated by Django 3.1.7 on 2021-03-30 19:42
from treebeard.mp_tree import MP_Node
from django.db import migrations, models
from django_scopes import scopes_disabled
# update if needed
steplen = MP_Node.steplen
alphabet = MP_Node.alphabet
node_order_by = ["name"]
def update_paths(apps, schema_editor):
with scopes_disabled():
Node = apps.get_model("cookbook", "Keyword")
nodes = Node.objects.all().order_by(*node_order_by)
for i, node in enumerate(nodes, 1):
# for default values, this resolves to: "{:04d}".format(i)
node.path = f"{{:{alphabet[0]}{steplen}d}}".format(i)
if nodes:
Node.objects.bulk_update(nodes, ["path"])
def backwards(apps, schema_editor):
"""nothing to do"""
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0145_alter_userpreference_use_fractions'),
]
operations = [
migrations.AddField(
model_name='keyword',
name='depth',
field=models.PositiveIntegerField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name='keyword',
name='numchild',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='keyword',
name='path',
field=models.CharField(default="", max_length=255, unique=False),
preserve_default=False,
),
migrations.AlterField(
model_name='userpreference',
name='use_fractions',
field=models.BooleanField(default=True),
),
migrations.RunPython(update_paths, backwards),
migrations.AlterField(
model_name="keyword",
name="path",
field=models.CharField(max_length=255, unique=True),
),
migrations.AlterUniqueTogether(
name='keyword',
unique_together=set(),
),
migrations.AddConstraint(
model_name='keyword',
constraint=models.UniqueConstraint(fields=('space', 'name'), name='unique_name_per_space'),
),
]

View File

@ -0,0 +1,66 @@
# Generated by Django 3.2.5 on 2021-08-13 16:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0146_keyword_to_tree'),
]
operations = [
migrations.RemoveConstraint(
model_name='keyword',
name='unique_name_per_space',
),
migrations.AlterField(
model_name='userpreference',
name='use_fractions',
field=models.BooleanField(default=False),
),
migrations.AlterUniqueTogether(
name='food',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='recipebookentry',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='supermarket',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='supermarketcategory',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='unit',
unique_together=set(),
),
migrations.AddConstraint(
model_name='food',
constraint=models.UniqueConstraint(fields=('space', 'name'), name='f_unique_name_per_space'),
),
migrations.AddConstraint(
model_name='keyword',
constraint=models.UniqueConstraint(fields=('space', 'name'), name='kw_unique_name_per_space'),
),
migrations.AddConstraint(
model_name='recipebookentry',
constraint=models.UniqueConstraint(fields=('recipe', 'book'), name='rbe_unique_name_per_space'),
),
migrations.AddConstraint(
model_name='supermarket',
constraint=models.UniqueConstraint(fields=('space', 'name'), name='sm_unique_name_per_space'),
),
migrations.AddConstraint(
model_name='supermarketcategory',
constraint=models.UniqueConstraint(fields=('space', 'name'), name='smc_unique_name_per_space'),
),
migrations.AddConstraint(
model_name='unit',
constraint=models.UniqueConstraint(fields=('space', 'name'), name='u_unique_name_per_space'),
),
]

View File

@ -0,0 +1,31 @@
from django.db import migrations, models
from django_scopes import scopes_disabled
models = ["Keyword", "Food", "Unit"]
def update_paths(apps, schema_editor):
with scopes_disabled():
for model in models:
Node = apps.get_model("cookbook", model)
nodes = Node.objects.all().filter(name__startswith=" ")
for i in nodes:
i.name = "_" + i.name
i.save()
nodes = Node.objects.all().filter(name__endswith=" ")
for i in nodes:
i.name = i.name + "_"
i.save()
def backwards(apps, schema_editor):
"""nothing to do"""
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0147_auto_20210813_1829'),
]
operations = [
migrations.RunPython(update_paths, backwards),
]

View File

@ -37,8 +37,20 @@ def get_model_name(model):
return ('_'.join(re.findall('[A-Z][^A-Z]*', model.__name__))).lower()
class PermissionModelMixin:
class TreeManager(MP_NodeManager):
def get_or_create(self, **kwargs):
# model.Manager get_or_create() is not compatible with MP_Tree
kwargs['name'] = kwargs['name'].strip()
q = self.filter(name__iexact=kwargs['name'], space=kwargs['space'])
if len(q) != 0:
return q[0], False
else:
with scopes_disabled():
node = self.model.add_root(**kwargs)
return node, True
class PermissionModelMixin:
@staticmethod
def get_space_key():
return ('space',)
@ -217,8 +229,9 @@ class SupermarketCategory(models.Model, PermissionModelMixin):
return self.name
class Meta:
# TODO according to this https://docs.djangoproject.com/en/3.1/ref/models/options/#unique-together should not be used
unique_together = (('space', 'name'),)
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='smc_unique_name_per_space')
]
class Supermarket(models.Model, PermissionModelMixin):
@ -233,8 +246,9 @@ class Supermarket(models.Model, PermissionModelMixin):
return self.name
class Meta:
# TODO according to this https://docs.djangoproject.com/en/3.1/ref/models/options/#unique-together should not be used
unique_together = (('space', 'name'),)
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='sm_unique_name_per_space')
]
class SupermarketCategoryRelation(models.Model, PermissionModelMixin):
@ -265,7 +279,7 @@ class SyncLog(models.Model, PermissionModelMixin):
class Keyword(ExportModelOperationsMixin('keyword'), MP_Node, PermissionModelMixin):
# TODO create get_or_create method
# TODO add find and fix problem functions
node_order_by = ['name']
name = models.CharField(max_length=64)
icon = models.CharField(max_length=16, blank=True, null=True)
@ -274,7 +288,7 @@ class Keyword(ExportModelOperationsMixin('keyword'), MP_Node, PermissionModelMix
updated_at = models.DateTimeField(auto_now=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space', _manager_class=MP_NodeManager)
objects = ScopedManager(space='space', _manager_class=TreeManager)
_full_name_separator = ' > '
@ -291,19 +305,6 @@ class Keyword(ExportModelOperationsMixin('keyword'), MP_Node, PermissionModelMix
return self.get_parent().id
return None
@classmethod
def get_or_create(self, **kwargs):
# an attempt to mimic get_or_create functionality with Keywords
# function attempts to get the keyword,
# if the length of the return is 0 will add a root node
kwargs['name'] = kwargs['name'].strip()
q = self.get_tree().filter(name=kwargs['name'], space=kwargs['space'])
if len(q) != 0:
return q[0], False
else:
kw = Keyword.add_root(**kwargs)
return kw, True
@property
def full_name(self):
"""
@ -337,6 +338,7 @@ class Keyword(ExportModelOperationsMixin('keyword'), MP_Node, PermissionModelMix
def get_num_children(self):
return self.get_children().count()
# use self.objects.get_or_create() instead
@classmethod
def add_root(self, **kwargs):
with scopes_disabled():
@ -344,7 +346,7 @@ class Keyword(ExportModelOperationsMixin('keyword'), MP_Node, PermissionModelMix
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='unique_name_per_space')
models.UniqueConstraint(fields=['space', 'name'], name='kw_unique_name_per_space')
]
indexes = (Index(fields=['id', 'name']),)
@ -360,8 +362,9 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
return self.name
class Meta:
# TODO according to this https://docs.djangoproject.com/en/3.1/ref/models/options/#unique-together should not be used
unique_together = (('space', 'name'),)
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='u_unique_name_per_space')
]
class Food(ExportModelOperationsMixin('food'), models.Model, PermissionModelMixin):
@ -378,8 +381,9 @@ class Food(ExportModelOperationsMixin('food'), models.Model, PermissionModelMixi
return self.name
class Meta:
# TODO according to this https://docs.djangoproject.com/en/3.1/ref/models/options/#unique-together should not be used
unique_together = (('space', 'name'),)
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='f_unique_name_per_space')
]
indexes = (Index(fields=['id', 'name']),)
@ -407,10 +411,11 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
TEXT = 'TEXT'
TIME = 'TIME'
FILE = 'FILE'
RECIPE = 'RECIPE'
name = models.CharField(max_length=128, default='', blank=True)
type = models.CharField(
choices=((TEXT, _('Text')), (TIME, _('Time')), (FILE, _('File')),),
choices=((TEXT, _('Text')), (TIME, _('Time')), (FILE, _('File')), (RECIPE, _('Recipe')),),
default=TEXT,
max_length=16
)
@ -421,6 +426,7 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
file = models.ForeignKey('UserFile', on_delete=models.PROTECT, null=True, blank=True)
show_as_header = models.BooleanField(default=True)
search_vector = SearchVectorField(null=True)
step_recipe = models.ForeignKey('Recipe', default=None, blank=True, null=True, on_delete=models.PROTECT)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@ -561,8 +567,9 @@ class RecipeBookEntry(ExportModelOperationsMixin('book_entry'), models.Model, Pe
return None
class Meta:
# TODO according to this https://docs.djangoproject.com/en/3.1/ref/models/options/#unique-together should not be used
unique_together = (('recipe', 'book'),)
constraints = [
models.UniqueConstraint(fields=['recipe', 'book'], name='rbe_unique_name_per_space')
]
class MealType(models.Model, PermissionModelMixin):

View File

@ -1,6 +1,5 @@
import io
import os
import tempfile
from datetime import datetime
from os import listdir
from os.path import isfile, join

View File

@ -4,7 +4,6 @@ from rest_framework.schemas.utils import is_list_view
# TODO move to separate class to cleanup
class RecipeSchema(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)
@ -55,10 +54,14 @@ class RecipeSchema(AutoSchema):
"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', },
})
return parameters
# TODO move to separate class to cleanup
class TreeSchema(AutoSchema):
def get_path_parameters(self, path, method):

View File

@ -47,7 +47,7 @@ class CustomDecimalField(serializers.Field):
class SpaceFilterSerializer(serializers.ListSerializer):
def to_representation(self, data):
if (type(data) == QuerySet and data.query.is_sliced) or type(data) == MP_NodeQuerySet:
if (type(data) == QuerySet and data.query.is_sliced):
# if query is sliced it came from api request not nested serializer
return super().to_representation(data)
if self.child.Meta.model == User:
@ -209,29 +209,30 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
return str(obj)
def get_image(self, obj):
recipes = obj.recipe_set.all().exclude(image__isnull=True).exclude(image__exact='')
recipes = obj.recipe_set.all().filter(space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
if len(recipes) == 0:
recipes = Recipe.objects.filter(keywords__in=Keyword.get_tree(obj)).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
recipes = Recipe.objects.filter(keywords__in=obj.get_tree(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
if len(recipes) != 0:
return random.choice(recipes).image.url
else:
return None
def count_recipes(self, obj):
return obj.recipe_set.all().count()
return obj.recipe_set.filter(space=self.context['request'].space).all().count()
def create(self, validated_data):
# since multi select tags dont have id's
# duplicate names might be routed to create
validated_data['name'] = validated_data['name'].strip()
validated_data['space'] = self.context['request'].space
obj, created = Keyword.get_or_create(**validated_data)
obj, created = Keyword.objects.get_or_create(**validated_data)
return obj
class Meta:
# list_serializer_class = SpaceFilterSerializer
model = Keyword
fields = ('id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at', 'updated_at')
read_only_fields = ('id', 'numchild',)
read_only_fields = ('id', 'numchild', 'parent', 'image')
class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
@ -264,7 +265,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial
fields = ('id', 'name')
class SupermarketCategoryRelationSerializer(SpacedModelSerializer):
class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
category = SupermarketCategorySerializer()
class Meta:
@ -284,7 +285,9 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
def create(self, validated_data):
obj, created = Food.objects.get_or_create(name=validated_data['name'].strip(), space=self.context['request'].space)
validated_data['name'] = validated_data['name'].strip()
validated_data['space'] = self.context['request'].space
obj, created = Food.objects.get_or_create(validated_data)
return obj
def update(self, instance, validated_data):
@ -318,6 +321,7 @@ class StepSerializer(WritableNestedModelSerializer):
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')
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
@ -329,11 +333,27 @@ class StepSerializer(WritableNestedModelSerializer):
def get_ingredients_markdown(self, obj):
return obj.get_instruction_render()
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
if obj.step_recipe and type(self.parent.root) == RecipeSerializer:
return StepRecipeSerializer(obj.step_recipe).data
class Meta:
model = Step
fields = (
'id', 'name', 'type', 'instruction', 'ingredients', 'ingredients_markdown',
'ingredients_vue', 'time', 'order', 'show_as_header', 'file',
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe', 'step_recipe_data'
)
class StepRecipeSerializer(WritableNestedModelSerializer):
steps = StepSerializer(many=True)
class Meta:
model = Recipe
fields = (
'id', 'name', 'steps',
)
@ -439,9 +459,11 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
def create(self, validated_data):
book = validated_data['book']
recipe = validated_data['recipe']
if not book.get_owner() == self.context['request'].user:
raise NotFound(detail=None, code=None)
return super().create(validated_data)
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
return obj
class Meta:
model = RecipeBookEntry

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
.shake[data-v-88855b04]{-webkit-animation:shake-data-v-88855b04 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-88855b04 .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-88855b04{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-88855b04{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}
.shake[data-v-54d4941f]{-webkit-animation:shake-data-v-54d4941f .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-54d4941f .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-54d4941f{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-54d4941f{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -97,6 +97,9 @@
<a class="dropdown-item" href="{% url 'list_food' %}"><i
class="fas fa-leaf fa-fw"></i> {% trans 'Ingredients' %}
</a>
<a class="dropdown-item" href="{% url 'view_supermarket' %}"><i
class="fas fa-store-alt fa-fw"></i> {% trans 'Supermarket' %}
</a>
</div>
</li>
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'list_keyword,data_batch_edit' %}active{% endif %}">

View File

@ -202,6 +202,7 @@
<option value="TEXT">{% trans 'Text' %}</option>
<option value="TIME">{% trans 'Time' %}</option>
<option value="FILE">{% trans 'File' %}</option>
<option value="RECIPE">{% trans 'Recipe' %}</option>
</select>
</div>
</div>
@ -214,7 +215,7 @@
:id="'id_step_' + step.id + '_time'">
</div>
<div class="col-md-9">
<div class="col-md-9" v-if="step.type === 'FILE'">
<label :for="'id_step_' + step.id + '_file'">{% trans 'File' %}</label>
<multiselect
v-tabindex
@ -235,6 +236,28 @@
@search-change="searchFiles">
</multiselect>
</div>
<div class="col-md-9" v-if="step.type === 'RECIPE'">
<label :for="'id_step_' + step.id + '_recipe'">{% trans 'Recipe' %}</label>
<multiselect
v-tabindex
ref="step_recipe"
v-model="step.step_recipe"
:options="recipes.map(recipe => recipe.id)"
:close-on-select="true"
:clear-on-select="true"
:allow-empty="true"
:preserve-search="true"
placeholder="{% trans 'Select Recipe' %}"
select-label="{% trans 'Select' %}"
:id="'id_step_' + step.id + '_recipe'"
:custom-label="opt => recipes.find(x => x.id == opt).name"
:multiple="false"
:loading="recipes_loading"
@search-change="searchRecipes">
</multiselect>
</div>
</div>
<template v-if="step.type == 'TEXT'">
@ -524,6 +547,8 @@
units_loading: false,
files: [],
files_loading: false,
recipes: [],
recipes_loading: false,
message: '',
},
directives: {
@ -550,6 +575,7 @@
this.searchFoods('')
this.searchKeywords('')
this.searchFiles('')
this.searchRecipes('')
this._keyListener = function (e) {
if (e.code === "Space" && e.ctrlKey) {
@ -722,6 +748,16 @@
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
searchRecipes: function (query) {
this.recipes_loading = true
this.$http.get("{% url 'api:recipe-list' %}" + '?query=' + query + '&limit=10').then((response) => {
this.recipes = response.data.results
this.recipes_loading = false
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
searchUnits: function (query) {
this.units_loading = true
this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => {

View File

@ -73,6 +73,7 @@
}
$('#id_log_rating').on("input", () => {
let rating = $('#id_log_rating')
$('#id_rating_show').html(rating.val() + '/5')
});

View File

@ -32,7 +32,7 @@
</div>
<div class="card-body">
<h5 class="card-title">{% trans 'Join an existing space.' %}</h5>
<p class="card-text">{% trans 'To join an existing space either enter your invite token or click on the invite link the space owner send you.' %}</p>
<p class="card-text" style="height: 64px">{% trans 'To join an existing space either enter your invite token or click on the invite link the space owner send you.' %}</p>
<form method="POST" action="{% url 'view_no_space' %}">
{% csrf_token %}
@ -49,7 +49,7 @@
</div>
<div class="card-body">
<h5 class="card-title">{% trans 'Create your own recipe space.' %}</h5>
<p class="card-text">{% trans 'Start your own recipe space and invite other users to it.' %}</p>
<p class="card-text" style="height: 64px">{% trans 'Start your own recipe space and invite other users to it.' %}</p>
<form method="POST" action="{% url 'view_no_space' %}">
{% csrf_token %}
{{ create_form | crispy }}

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,15 @@ from django_scopes import scopes_disabled
from cookbook.models import Keyword
from cookbook.tests.conftest import get_random_recipe
# ------------------ IMPORTANT -------------------
#
# if changing any capabilities associated with keywords
# you will need to ensure that it is tested against both
# SqlLite and PostgresSQL
# adding load_env() to settings.py will enable Postgress access
#
# ------------------ IMPORTANT -------------------
LIST_URL = 'api:keyword-list'
DETAIL_URL = 'api:keyword-detail'
MOVE_URL = 'api:keyword-move'
@ -16,7 +25,7 @@ MERGE_URL = 'api:keyword-merge'
# TODO are there better ways to manage these fixtures?
@pytest.fixture()
def obj_1(space_1):
return Keyword.add_root(name='test_1', space=space_1)
return Keyword.objects.get_or_create(name='test_1', space=space_1)[0]
@pytest.fixture()
@ -31,12 +40,12 @@ def obj_1_1_1(obj_1_1, space_1):
@pytest.fixture
def obj_2(space_1):
return Keyword.add_root(name='test_2', space=space_1)
return Keyword.objects.get_or_create(name='test_2', space=space_1)[0]
@pytest.fixture()
def obj_3(space_2):
return Keyword.add_root(name='test_3', space=space_2)
return Keyword.objects.get_or_create(name='test_3', space=space_2)[0]
@pytest.fixture()
@ -158,7 +167,6 @@ def test_add(arg, request, u1_s2):
assert r.status_code == 404
@pytest.mark.django_db(transaction=True)
def test_add_duplicate(u1_s1, u1_s2, obj_1, obj_3):
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 1
@ -246,13 +254,13 @@ def test_move(u1_s1, obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, space_1):
r = u1_s1.put(
reverse(MOVE_URL, args=[obj_1.id, 9999])
)
assert r.status_code == 400
assert r.status_code == 404
# attempt to move to wrong space
r = u1_s1.put(
reverse(MOVE_URL, args=[obj_1_1.id, obj_3.id])
)
assert r.status_code == 400
assert r.status_code == 404
# run diagnostic to find problems - none should be found
with scopes_disabled():
@ -318,13 +326,13 @@ def test_merge(
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_1_1.id, 9999])
)
assert r.status_code == 400
assert r.status_code == 404
# attempt to move to wrong space
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_2.id, obj_3.id])
)
assert r.status_code == 400
assert r.status_code == 404
# attempt to merge with child
r = u1_s1.put(

View File

@ -4,12 +4,16 @@ import pytest
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food, Ingredient, Step, Recipe
from cookbook.models import Recipe
LIST_URL = 'api:recipe-list'
DETAIL_URL = 'api:recipe-detail'
# TODO need to add extensive tests against recipe search to go through all of the combinations of parameters
# probably needs to include a far more extensive set of initial recipes to effectively test results
# and to ensure that all parts of the code are exercised.
# TODO should probably consider adding code coverage plugin to the test suite
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],

View File

@ -11,7 +11,7 @@ LIST_URL = 'api:recipebookentry-list'
DETAIL_URL = 'api:recipebookentry-detail'
@pytest.fixture()
@pytest.fixture
def obj_1(space_1, u1_s1, recipe_1_s1):
b = RecipeBook.objects.create(name='test_1', created_by=auth.get_user(u1_s1), space=space_1)
@ -100,7 +100,7 @@ def test_add_duplicate(u1_s1, obj_1):
{'book': obj_1.book.pk, 'recipe': obj_1.recipe.pk},
content_type='application/json'
)
assert r.status_code == 400
assert r.status_code == 201
def test_delete(u1_s1, u1_s2, obj_1):

View File

@ -7,7 +7,7 @@ from django.contrib import auth
from django.contrib.auth.models import User, Group
from django_scopes import scopes_disabled
from cookbook.models import Space, Recipe, Step, Ingredient, Food, Unit, Storage
from cookbook.models import Space, Recipe, Step, Ingredient, Food, Unit
# hack from https://github.com/raphaelm/django-scopes to disable scopes for all fixtures

View File

@ -36,6 +36,7 @@ router.register(r'recipe-book', api.RecipeBookViewSet)
router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
router.register(r'supermarket', api.SupermarketViewSet)
router.register(r'supermarket-category', api.SupermarketCategoryViewSet)
router.register(r'supermarket-category-relation', api.SupermarketCategoryRelationViewSet)
router.register(r'import-log', api.ImportLogViewSet)
router.register(r'bookmarklet-import', api.BookmarkletImportViewSet)
router.register(r'user-file', api.UserFileViewSet)

View File

@ -4,9 +4,9 @@ import re
import uuid
import requests
from PIL import Image
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
@ -37,13 +37,13 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
group_required)
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_search import search_recipes
from cookbook.helper.recipe_search import search_recipes, get_facet
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)
ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile, ShareLink, SupermarketCategoryRelation)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
@ -61,7 +61,7 @@ from cookbook.serializer import (FoodSerializer, IngredientSerializer,
UserNameSerializer, UserPreferenceSerializer,
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer,
RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer,
BookmarkletImportSerializer, SupermarketCategorySerializer, UserFileSerializer)
BookmarkletImportSerializer, SupermarketCategorySerializer, UserFileSerializer, SupermarketCategoryRelationSerializer)
class StandardFilterMixin(ViewSetMixin):
@ -144,18 +144,18 @@ class TreeMixin(FuzzyFilterMixin):
except self.model.DoesNotExist:
self.queryset = self.model.objects.none()
if root == 0:
self.queryset = self.model.get_root_nodes().filter(space=self.request.space)
self.queryset = self.model.get_root_nodes() | self.model.objects.filter(depth=0)
else:
self.queryset = self.model.objects.get(id=root).get_children().filter(space=self.request.space)
self.queryset = self.model.objects.get(id=root).get_children()
elif tree:
if tree.isnumeric():
try:
self.queryset = self.model.objects.get(id=int(tree)).get_descendants_and_self().filter(space=self.request.space)
self.queryset = self.model.objects.get(id=int(tree)).get_descendants_and_self()
except Keyword.DoesNotExist:
self.queryset = self.model.objects.none()
else:
return super().get_queryset()
return self.queryset
return self.queryset.filter(space=self.request.space)
@decorators.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'],)
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
@ -166,7 +166,7 @@ class TreeMixin(FuzzyFilterMixin):
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')}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
return Response(content, status=status.HTTP_404_NOT_FOUND)
parent = int(parent)
# parent 0 is root of the tree
@ -184,7 +184,7 @@ class TreeMixin(FuzzyFilterMixin):
parent = self.model.objects.get(pk=parent, space=self.request.space)
except (self.model.DoesNotExist):
content = {'error': True, 'msg': _(f'No {self.basename} with id {parent} exists')}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
return Response(content, status=status.HTTP_404_NOT_FOUND)
try:
with scopes_disabled():
@ -204,7 +204,7 @@ class TreeMixin(FuzzyFilterMixin):
source = self.model.objects.get(pk=pk, space=self.request.space)
except (self.model.DoesNotExist):
content = {'error': True, 'msg': _(f'No {self.basename} with id {pk} exists')}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
return Response(content, status=status.HTTP_404_NOT_FOUND)
if int(target) == source.id:
content = {'error': True, 'msg': _('Cannot merge with the same object!')}
@ -215,14 +215,14 @@ class TreeMixin(FuzzyFilterMixin):
target = self.model.objects.get(pk=target, space=self.request.space)
except (self.model.DoesNotExist):
content = {'error': True, 'msg': _(f'No {self.basename} with id {target} exists')}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
return Response(content, status=status.HTTP_404_NOT_FOUND)
try:
if target in source.get_descendants_and_self():
content = {'error': True, 'msg': _('Cannot merge with child object!')}
return Response(content, status=status.HTTP_403_FORBIDDEN)
########################################################################
# this needs abstracted to update steps instead of recipes for food merge
# TODO this needs abstracted to update steps instead of recipes for food merge
########################################################################
recipes = Recipe.objects.filter(**{"%ss" % self.basename: source}, space=self.request.space)
@ -322,9 +322,18 @@ class SupermarketCategoryViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset()
class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
# TODO check if fuzzyfilter is conflicting - may also need to create 'tree filter' mixin
class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMixin):
queryset = SupermarketCategoryRelation.objects
serializer_class = SupermarketCategoryRelationSerializer
permission_classes = [CustomIsUser]
pagination_class = DefaultPagination
def get_queryset(self):
self.queryset = self.queryset.filter(supermarket__space=self.request.space)
return super().get_queryset()
class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
queryset = Keyword.objects
model = Keyword
serializer_class = KeywordSerializer
@ -438,6 +447,19 @@ class RecipePagination(PageNumberPagination):
page_size_query_param = 'page_size'
max_page_size = 100
def paginate_queryset(self, queryset, request, view=None):
self.facets = get_facet(queryset, request.query_params)
return super().paginate_queryset(queryset, request, view)
def get_paginated_response(self, data):
return Response(OrderedDict([
('count', self.page.paginator.count),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('results', data),
('facets', self.facets)
]))
class RecipeViewSet(viewsets.ModelViewSet):
queryset = Recipe.objects
@ -679,7 +701,7 @@ def share_link(request, pk):
def log_cooking(request, recipe_id):
recipe = get_object_or_None(Recipe, id=recipe_id)
if recipe:
log = CookLog.objects.create(created_by=request.user, recipe=recipe)
log = CookLog.objects.create(created_by=request.user, recipe=recipe, space=request.space)
servings = request.GET['s'] if 's' in request.GET else None
if servings and re.match(r'^([1-9])+$', servings):
log.servings = int(servings)

View File

@ -13,7 +13,7 @@ from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import ngettext
from django_tables2 import RequestConfig
from PIL import Image, UnidentifiedImageError
from PIL import UnidentifiedImageError
from requests.exceptions import MissingSchema
from cookbook.forms import BatchEditForm, SyncForm
@ -150,11 +150,7 @@ def import_url(request):
all_keywords = Keyword.get_tree()
for kw in data['keywords']:
q = all_keywords.filter(name=kw['text'], space=request.space)
if len(q) != 0:
recipe.keywords.add(q[0])
elif data['all_keywords']:
k = Keyword.add_root(name=kw['text'], space=request.space)
k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space)
recipe.keywords.add(k)
for ing in data['recipeIngredient']:

View File

@ -3,12 +3,12 @@ import json
import requests
from django.db.models import Q
from django.http import JsonResponse
from django.shortcuts import render, get_object_or_404
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from cookbook.helper.ingredient_parser import parse, get_unit, get_food
from cookbook.helper.permission_helper import group_required
from cookbook.models import TelegramBot, ShoppingList, ShoppingListEntry, Food, Unit
from cookbook.models import TelegramBot, ShoppingList, ShoppingListEntry
@group_required('user')
@ -58,7 +58,7 @@ def hook(request, token):
)
)
return JsonResponse({'data': data['message']['text']})
except:
except Exception:
pass
return JsonResponse({})

View File

@ -15,6 +15,8 @@ from django.db.models import Avg, Q
from django.db.models import Sum
from django.http import HttpResponseRedirect
from django.http import JsonResponse
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.urls import reverse, reverse_lazy
from django.utils import timezone
@ -27,6 +29,8 @@ from cookbook.filters import RecipeFilter
from cookbook.forms import (CommentForm, Recipe, User,
UserCreateForm, UserNameForm, UserPreference,
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm, SearchPreferenceForm)
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm,
SearchPreferenceForm)
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
@ -122,6 +126,7 @@ def no_space(request):
max_users=settings.SPACE_DEFAULT_MAX_USERS,
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
)
request.user.userpreference.space = created_space
request.user.userpreference.save()
request.user.groups.add(Group.objects.filter(name='admin').get())
@ -141,7 +146,7 @@ def no_space(request):
if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
create_form = SpaceCreateForm()
create_form = SpaceCreateForm(initial={'name': f'{request.user.username}\'s Space'})
join_form = SpaceJoinForm()
return render(request, 'no_space_info.html', {'create_form': create_form, 'join_form': join_form})

View File

@ -7,16 +7,32 @@ These intructions are inspired from a standard django/gunicorn/postgresql instru
## Prerequisites
*Optional*: create a virtual env and activate it
Setup user: `sudo useradd recipes`
Get the last version from the repository: `git clone https://github.com/vabene1111/recipes.git -b master`
Install postgresql requirements: `sudo apt install libpq-dev postgresql`
Install project requirements: `pip3.9 install -r requirements.txt`
Move it to the `/var/www` directory: `mv recipes /var/www`
Change to the directory: `cd /var/www/recipes`
Give the user permissions: `chown -R recipes:www-data /var/www/recipes`
Create virtual env: `python3.9 -m venv /var/www/recipes`
### Install postgresql requirements
`sudo apt install libpq-dev postgresql`
###Install project requirements
Using binaries from the virtual env:
`/var/www/recipes/bin/pip3.9 install -r requirements.txt`
## Setup postgresql
Run `sudo -u postgres psql`
`sudo -u postgres psql`
In the psql console:
@ -37,12 +53,18 @@ ALTER USER djangouser WITH SUPERUSER;
Download the `.env` configuration file and **edit it accordingly**.
```shell
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O /var/www/recipes/.env
```
Things to edit:
- `SECRET_KEY`: use something secure.
- `POSTGRES_HOST`: probably 127.0.0.1.
- `POSTGRES_PASSWORD`: the password we set earlier when setting up djangodb.
- `STATIC_URL`, `MEDIA_URL`: these will be in `/var/www/recipes`, under `/staticfiles/` and `/mediafiles/` respectively.
## Initialize the application
Execute `export $(cat .env |grep "^[^#]" | xargs)` to load variables from `.env`
Execute `export $(cat /var/www/recipes/.env |grep "^[^#]" | xargs)` to load variables from `/var/www/recipes/.env`
Execute `/python3.9 manage.py migrate`
@ -67,10 +89,11 @@ After=network.target
Type=simple
Restart=always
RestartSec=3
User=recipes
Group=www-data
WorkingDirectory=/media/data/recipes
EnvironmentFile=/media/data/recipes/.env
ExecStart=/opt/.pyenv/versions/3.9/bin/gunicorn --error-logfile /tmp/gunicorn_err.log --log-level debug --capture-output --bind unix:/media/data/recipes/recipes.sock recipes.wsgi:application
WorkingDirectory=/var/www/recipes
EnvironmentFile=/var/www/recipes/.env
ExecStart=/var/www/recipes/bin/gunicorn --error-logfile /tmp/gunicorn_err.log --log-level debug --capture-output --bind unix:/var/www/recipes/recipes.sock recipes.wsgi:application
[Install]
WantedBy=multi-user.target
@ -80,11 +103,11 @@ WantedBy=multi-user.target
*Note2*: Fix the path in the `ExecStart` line to where you gunicorn and recipes are
Finally, run `sudo systemctl enable gunicorn_recipes.service` and `sudo systemctl start gunicorn_recipes.service`. You can check that the service is correctly started with `systemctl status gunicorn_recipes.service`
Finally, run `sudo systemctl enable gunicorn_recipes` and `sudo systemctl start gunicorn_recipes`. You can check that the service is correctly started with `systemctl status gunicorn_recipes`
### nginx
Now we tell nginx to listen to a new port and forward that to gunicorn. `sudo nano /etc/nginx/sites-available/recipes.conf`
Now we tell nginx to listen to a new port and forward that to gunicorn. `sudo nano /etc/nginx/conf.d/recipes.conf`
And enter these lines:
@ -95,20 +118,21 @@ server {
#error_log /var/log/nginx/error.log;
# serve media files
location /static {
alias /media/data/recipes/staticfiles;
location /staticfiles {
alias /var/www/recipes/staticfiles;
}
location /media {
alias /media/data/recipes/mediafiles;
location /mediafiles {
alias /var/www/recipes/mediafiles;
}
location / {
proxy_pass http://unix:/media/data/recipes/recipes.sock;
proxy_set_header Host $http_host;
proxy_pass http://unix:/var/www/recipes/recipes.sock;
}
}
```
*Note*: Enter the correct path in static and proxy_pass lines.
Enable the website `sudo ln -s /etc/nginx/sites-available/recipes.conf /etc/nginx/sites-enabled` and restart nginx : `sudo systemctl restart nginx.service`
Reload nginx : `sudo systemctl reload nginx`

View File

@ -2,7 +2,7 @@ server {
listen 80;
server_name localhost;
client_max_body_size 16M;
client_max_body_size 128M;
# serve media files
location /media/ {

View File

@ -15,18 +15,15 @@ import os
import re
from django.contrib import messages
from django.contrib.staticfiles.storage import staticfiles_storage
from django.utils.translation import gettext_lazy as _
from dotenv import load_dotenv
from webpack_loader.loader import WebpackLoader
load_dotenv()
# from dotenv import load_dotenv
# load_dotenv()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Get vars from .env files
SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv('SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV'
DEBUG = bool(int(os.getenv('DEBUG', True)))
DEMO = bool(int(os.getenv('DEMO', False)))
SOCIAL_DEFAULT_ACCESS = bool(int(os.getenv('SOCIAL_DEFAULT_ACCESS', False)))
SOCIAL_DEFAULT_GROUP = os.getenv('SOCIAL_DEFAULT_GROUP', 'guest')
@ -279,6 +276,16 @@ else:
# }
# }
# SQLite testing DB
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'OPTIONS': ast.literal_eval(os.getenv('DB_OPTIONS')) if os.getenv('DB_OPTIONS') else {},
# 'NAME': 'db.sqlite3',
# 'CONN_MAX_AGE': 600,
# }
# }
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',

View File

@ -1,4 +1,4 @@
Django==3.2.4
Django==3.2.5
cryptography==3.4.7
django-annoying==0.10.6
django-autocomplete-light==3.8.2
@ -9,19 +9,19 @@ django-filter==2.4.0
django-tables2==2.4.0
djangorestframework==3.12.4
drf-writable-nested==0.6.3
bleach==3.3.0
bleach==3.3.1
bleach-allowlist==1.0.3
gunicorn==20.1.0
lxml==4.6.3
Markdown==3.3.4
Pillow==8.2.0
Pillow==8.3.1
psycopg2-binary==2.9.1
python-dotenv==0.18.0
requests==2.25.1
simplejson==3.17.2
requests==2.26.0
simplejson==3.17.3
six==1.16.0
webdavclient3==3.14.5
whitenoise==5.2.0
whitenoise==5.3.0
icalendar==4.0.7
pyyaml==5.4.1
uritemplate==3.0.1
@ -30,14 +30,14 @@ microdata==0.7.1
Jinja2==3.0.1
django-webpack-loader==1.1.0
django-js-reverse==0.9.1
django-allauth==0.44.0
recipe-scrapers==13.3.0
django-allauth==0.45.0
recipe-scrapers==13.3.4
django-scopes==1.2.0
pytest==6.2.4
pytest-django==4.4.0
django-cors-headers==3.7.0
django-treebeard==4.5.1
django-storages==1.11.1
boto3==1.17.102
boto3==1.18.4
django-prometheus==2.1.0
django-hCaptcha==0.1.0

View File

@ -10,6 +10,7 @@
"dependencies": {
"@babel/eslint-parser": "^7.13.14",
"@kevinfaguiar/vue-twemoji-picker": "^5.7.4",
"@riophae/vue-treeselect": "^0.4.0",
"axios": "^0.21.1",
"bootstrap-vue": "^2.21.2",
"core-js": "^3.14.0",

View File

@ -6,7 +6,7 @@
</div>
<div class="col-xl-8 col-12">
<!-- TODO only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different componenet? -->
<!-- TODO only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different component? -->
<div class="container-fluid d-flex flex-column flex-grow-1" :class="{'vh-100' : show_split}">
<!-- <div class="container-fluid d-flex flex-column flex-grow-1 vh-100"> -->
<!-- expanded options box -->
@ -450,10 +450,11 @@ export default {
let parent = {}
let pageSize = 200
let keyword = String(kw.id)
console.log(apiClient.listRecipes)
apiClient.listRecipes(
undefined, keyword, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, pageSize
undefined, undefined, undefined, undefined, undefined, pageSize, undefined
).then(result => {
if (col == 'left') {
parent = this.findKeyword(this.keywords, kw.id)

View File

@ -10,13 +10,20 @@
<b-input class="form-control form-control-lg form-control-borderless form-control-search" v-model="settings.search_input"
v-bind:placeholder="$t('Search')"></b-input>
<b-input-group-append>
<b-button v-b-toggle.collapse_advanced_search
v-bind:class="{'btn-primary': !isAdvancedSettingsSet(), 'btn-danger': isAdvancedSettingsSet()}"
class="shadow-none btn"><i
class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i><i
class="fas fa-caret-up"
v-if="settings.advanced_search_visible"></i>
<b-button variant="light"
v-b-tooltip.hover :title="$t('Random Recipes')"
@click="openRandom()">
<i class="fas fa-dice-five" style="font-size: 1.5em"></i>
</b-button>
<b-button v-b-toggle.collapse_advanced_search
v-b-tooltip.hover :title="$t('Advanced Settings')"
v-bind:variant="!isAdvancedSettingsSet() ? 'primary' : 'danger'"
>
<!-- consider changing this icon to a filter -->
<i class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i>
<i class="fas fa-caret-up" v-if="settings.advanced_search_visible"></i>
</b-button>
</b-input-group-append>
</b-input-group>
</div>
@ -73,6 +80,21 @@
></b-form-input>
</b-form-group>
<b-form-group
v-bind:label="$t('Recipes_per_page')"
label-for="popover-input-page-count"
label-cols="6"
class="mb-3">
<b-form-input
type="number"
v-model="settings.page_count"
id="popover-input-page-count"
size="sm"
></b-form-input>
</b-form-group>
<b-form-group
v-bind:label="$t('Meal_Plan')"
label-for="popover-input-2"
@ -85,6 +107,24 @@
size="sm"
></b-form-checkbox>
</b-form-group>
<b-form-group
v-bind:label="$t('Sort_by_new')"
label-for="popover-input-3"
label-cols="6"
class="mb-3">
<b-form-checkbox
switch
v-model="settings.sort_by_new"
id="popover-input-3"
size="sm"
></b-form-checkbox>
</b-form-group>
</div>
<div class="row" style="margin-top: 1vh">
<div class="col-12">
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t('Advanced Search Settings') }}</a>
</div>
</div>
<div class="row" style="margin-top: 1vh">
<div class="col-12">
@ -103,12 +143,16 @@
<div class="row">
<div class="col-12">
<b-input-group class="mt-2">
<generic-multiselect @change="genericSelectChanged" parent_variable="search_keywords"
<!-- <generic-multiselect @change="genericSelectChanged" parent_variable="search_keywords"
:initial_selection="settings.search_keywords"
search_function="listKeywords" label="label"
:tree_api="true"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Keywords')"></generic-multiselect>
v-bind:placeholder="$t('Keywords')"></generic-multiselect> -->
<treeselect v-model="settings.search_keywords" :options="facets.Keywords" :flat="true"
searchNested multiple :placeholder="$t('Keywords')" :normalizer="normalizer"
@input="refreshData(false)"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"/>
<b-input-group-append>
<b-input-group-text>
<b-form-checkbox v-model="settings.search_keywords_or" name="check-button"
@ -179,7 +223,7 @@
<div class="row">
<div class="col col-md-12 text-right" style="margin-top: 2vh">
<span class="text-muted">
{{ $t('Page') }} {{ settings.pagination_page }}/{{ pagination_count }} <a href="#" @click="resetSearch"><i
{{ $t('Page') }} {{ settings.pagination_page }}/{{ Math.ceil(pagination_count/settings.page_count) }} <a href="#" @click="resetSearch"><i
class="fas fa-times-circle"></i> {{ $t('Reset') }}</a>
</span>
</div>
@ -187,10 +231,10 @@
<div class="row">
<div class="col col-md-12">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 0.8rem;" >
<template
v-if="settings.search_input === '' && settings.search_keywords.length === 0 && settings.search_foods.length === 0 && settings.search_books.length === 0">
v-if="settings.search_input === '' && settings.search_keywords.length === 0 && settings.search_foods.length === 0 && settings.search_books.length === 0 && this.settings.pagination_page === 1 && !random_search">
<recipe-card v-bind:key="`mp_${m.id}`" v-for="m in meal_plans" :recipe="m.recipe"
:meal_plan="m" :footer_text="m.meal_type_name"
footer_icon="far fa-calendar-alt"></recipe-card>
@ -204,12 +248,12 @@
</div>
</div>
<div class="row" style="margin-top: 2vh">
<div class="row" style="margin-top: 2vh" v-if="!random_search">
<div class="col col-md-12">
<b-pagination pills
v-model="settings.pagination_page"
:total-rows="pagination_count"
per-page="25"
:per-page="settings.page_count"
@change="pageChange"
align="center">
@ -223,8 +267,6 @@
</div>
</div>
</div>
</template>
<script>
@ -246,6 +288,8 @@ import LoadingSpinner from "@/components/LoadingSpinner";
import {ApiApiFactory} from "@/utils/openapi/api.ts";
import RecipeCard from "@/components/RecipeCard";
import GenericMultiselect from "@/components/GenericMultiselect";
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
Vue.use(BootstrapVue)
@ -254,10 +298,11 @@ let SETTINGS_COOKIE_NAME = 'search_settings'
export default {
name: 'RecipeSearchView',
mixins: [ResolveUrlMixin],
components: {GenericMultiselect, RecipeCard},
components: {GenericMultiselect, RecipeCard, Treeselect},
data() {
return {
recipes: [],
facets: [],
meal_plans: [],
last_viewed_recipes: [],
@ -275,11 +320,13 @@ export default {
advanced_search_visible: false,
show_meal_plan: true,
recently_viewed: 5,
sort_by_new: true,
pagination_page: 1,
page_count: 25,
},
pagination_count: 0,
random_search: false,
}
},
@ -339,15 +386,18 @@ export default {
'settings.search_input': _debounce(function () {
this.refreshData(false)
}, 300),
'settings.page_count': _debounce(function () {
this.refreshData(false)
}, 300),
},
methods: {
refreshData: function (page_load) {
refreshData: function (random) {
this.random_search = random
let apiClient = new ApiApiFactory()
apiClient.listRecipes(
this.settings.search_input,
this.settings.search_keywords.map(function (A) {
return A["id"];
}),
this.settings.search_keywords,
this.settings.search_foods.map(function (A) {
return A["id"];
}),
@ -359,14 +409,20 @@ export default {
this.settings.search_books_or,
this.settings.search_internal,
undefined,
random,
this.settings.sort_by_new,
this.settings.pagination_page,
this.settings.page_count
).then(result => {
window.scrollTo(0, 0);
this.pagination_count = result.data.count
this.recipes = result.data.results
this.facets = result.data.facets
})
},
openRandom: function () {
this.refreshData(true)
},
loadMealPlan: function () {
let apiClient = new ApiApiFactory()
@ -389,7 +445,7 @@ export default {
let apiClient = new ApiApiFactory()
if (this.settings.recently_viewed > 0) {
apiClient.listRecipes(undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, {query: {last_viewed: this.settings.recently_viewed}}).then(result => {
undefined, undefined, undefined, this.settings.sort_by_new, 1, this.settings.recently_viewed, {query: {last_viewed: this.settings.recently_viewed}}).then(result => {
this.last_viewed_recipes = result.data.results
})
} else {
@ -411,10 +467,18 @@ export default {
},
pageChange: function (page) {
this.settings.pagination_page = page
this.refreshData()
this.refreshData(false)
},
isAdvancedSettingsSet() {
return ((this.settings.search_keywords.length + this.settings.search_foods.length + this.settings.search_books.length) > 0)
},
normalizer(node) {
return {
id: node.id,
label: node.name + ' (' + node.count + ')',
children: node.children,
isDefaultExpanded: node.isDefaultExpanded
}
}
}
}

View File

@ -8,7 +8,7 @@
<h2>{{ $t('Supermarket') }}</h2>
<multiselect v-model="selected_supermarket" track-by="id" label="name"
:options="supermarkets">
:options="supermarkets" @input="selectedSupermarketChanged">
</multiselect>
<b-button class="btn btn-primary btn-block" style="margin-top: 1vh" v-b-modal.modal-supermarket>
@ -25,12 +25,14 @@
<div class="row">
<div class="col col-md-6">
<h4>{{ $t('Categories') }}
<button class="btn btn-success btn-sm">{{ $t('New') }}</button>
<button class="btn btn-success btn-sm" @click="selected_category = {new:true, name:''}"
v-b-modal.modal-category>{{ $t('New') }}
</button>
</h4>
<draggable :list="categories" group="supermarket_categories"
<draggable :list="selectable_categories" group="supermarket_categories"
:empty-insert-threshold="10">
<div v-for="c in categories" :key="c.id">
<div v-for="c in selectable_categories" :key="c.id">
<button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
</div>
@ -41,9 +43,9 @@
<div class="col col-md-6">
<h4>{{ $t('Selected') }} {{ $t('Categories') }}</h4>
<draggable :list="selected_categories" group="supermarket_categories"
:empty-insert-threshold="10">
<div v-for="c in selected_categories" :key="c.id">
<draggable :list="supermarket_categories" group="supermarket_categories"
:empty-insert-threshold="10" @change="selectedCategoriesChanged">
<div v-for="c in supermarket_categories" :key="c.id">
<button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
</div>
@ -61,9 +63,9 @@
</b-modal>
<b-modal id="modal-category" v-bind:title="$t('Category')" @ok="categoryModalOk()">
<label v-if="selected_supermarket !== undefined">
<label v-if="selected_category !== undefined">
{{ $t('Name') }}
<b-input v-model="selected_supermarket.name"></b-input>
<b-input v-model="selected_category.name"></b-input>
</label>
</b-modal>
@ -107,12 +109,15 @@ export default {
categories: [],
selected_supermarket: {},
selected_categories: [],
selected_category: {},
selectable_categories: [],
supermarket_categories: [],
}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
console.log('LOADED')
this.loadInitial()
},
methods: {
@ -123,8 +128,45 @@ export default {
})
apiClient.listSupermarketCategorys().then(results => {
this.categories = results.data
this.selectable_categories = this.categories
})
},
selectedCategoriesChanged: function (data) {
let apiClient = new ApiApiFactory()
if ('removed' in data) {
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === data.removed.element.id)[0]
apiClient.destroySupermarketCategoryRelation(relation.id)
}
if ('added' in data) {
apiClient.createSupermarketCategoryRelation({
category: data.added.element,
supermarket: this.selected_supermarket.id, order: 0
}).then(results => {
this.selected_supermarket.category_to_supermarket.push(results.data)
})
}
if ('moved' in data || 'added' in data) {
this.supermarket_categories.forEach( (element,index) =>{
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === element.id)[0]
console.log(relation)
apiClient.partialUpdateSupermarketCategoryRelation(relation.id, {order: index})
})
}
},
selectedSupermarketChanged: function (supermarket, id) {
this.supermarket_categories = []
this.selectable_categories = this.categories
for (let i of supermarket.category_to_supermarket) {
this.supermarket_categories.push(i.category)
this.selectable_categories = this.selectable_categories.filter(function (el) {
return el.id !== i.category.id
});
}
},
supermarketModalOk: function () {
let apiClient = new ApiApiFactory()
if (this.selected_supermarket.new) {
@ -139,14 +181,13 @@ export default {
},
categoryModalOk: function () {
let apiClient = new ApiApiFactory()
if (this.selected_supermarket.new) {
apiClient.createSupermarket({name: this.selected_supermarket.name}).then(results => {
this.selected_supermarket = undefined
if (this.selected_category.new) {
apiClient.createSupermarketCategory({name: this.selected_category.name}).then(results => {
this.selected_category = {}
this.loadInitial()
})
} else {
apiClient.partialUpdateSupermarket(this.selected_supermarket.id, {name: this.selected_supermarket.name})
apiClient.partialUpdateSupermarketCategory(this.selected_category.id, {name: this.selected_category.name})
}
}
}

View File

@ -2,7 +2,7 @@
<div row>
<b-card no-body d-flex flex-column :class="{'border border-primary' : over, 'shake': isError}"
refs="keywordCard"
style="height: 10vh;" :style="{'cursor:grab' : draggle}"
style="height: 10vh;" :style="{'cursor:grab' : draggable}"
@dragover.prevent
@dragenter.prevent
:draggable="draggable"

View File

@ -0,0 +1,23 @@
<template>
<div>
<nav class="navbar navbar-expand-lg navbar-light bg-header"
style="position: sticky; top: 0; left: 0; z-index: 1000;">
<div class="collapse navbar-collapse" id="navbarText">
</div>
</nav>
</div>
</template>
<script>
export default {
name: "PinnedRecipeBar"
}
</script>
<style scoped>
</style>

View File

@ -1,9 +1,9 @@
<template>
<div>
<hr/>
<hr />
<template v-if="step.type === 'TEXT'">
<template v-if="step.type === 'TEXT' || step.type === 'RECIPE'">
<div class="row" v-if="recipe.steps.length > 1">
<div class="col col-md-8">
<h5 class="text-primary">
@ -22,15 +22,21 @@
</div>
<div class="col col-md-4" style="text-align: right">
<b-button @click="details_visible = !details_visible" style="border: none; background: none"
class="shadow-none d-print-none" :class="{ 'text-primary': details_visible, 'text-success': !details_visible}">
class="shadow-none d-print-none"
:class="{ 'text-primary': details_visible, 'text-success': !details_visible}">
<i class="far fa-check-circle"></i>
</b-button>
</div>
</div>
</template>
<template v-if="step.type === 'TEXT'">
<b-collapse id="collapse-1" v-model="details_visible">
<div class="row">
<div class="col col-md-4" v-if="step.ingredients.length > 0 && recipe.steps.length > 1">
<div class="col col-md-4"
v-if="step.ingredients.length > 0 && (recipe.steps.length > 1 || force_ingredients)">
<table class="table table-sm">
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
<template v-for="i in step.ingredients">
@ -65,7 +71,8 @@
<div class="col-md-2" style="text-align: right">
<b-button @click="details_visible = !details_visible" style="border: none; background: none"
class="shadow-none d-print-none" :class="{ 'text-primary': details_visible, 'text-success': !details_visible}">
class="shadow-none d-print-none"
:class="{ 'text-primary': details_visible, 'text-success': !details_visible}">
<i class="far fa-check-circle"></i>
</b-button>
</div>
@ -89,13 +96,31 @@
<img :src="step.file.file" style="max-width: 50vw; max-height: 50vh">
</div>
<div v-else>
<a :href="step.file.file" target="_blank" rel="noreferrer nofollow">{{ $t('Download') }} {{ $t('File') }}</a>
<a :href="step.file.file" target="_blank" rel="noreferrer nofollow">{{ $t('Download') }} {{
$t('File')
}}</a>
</div>
</template>
</div>
</div>
<div class="card" v-if="step.type === 'RECIPE' && step.step_recipe_data !== null">
<b-collapse id="collapse-1" v-model="details_visible">
<div class="card-body">
<h2 class="card-title">
<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>
</div>
</div>
</b-collapse>
</div>
<div v-if="start_time !== ''">
<b-popover
:target="`id_reactive_popover_${step.id}`"
@ -139,6 +164,8 @@ import {GettextMixin} from "@/utils/utils";
import CompileComponent from "@/components/CompileComponent";
import Vue from "vue";
import moment from "moment";
import Keywords from "@/components/Keywords";
import {ResolveUrlMixin} from "@/utils/utils";
Vue.prototype.moment = moment
@ -146,6 +173,7 @@ export default {
name: 'Step',
mixins: [
GettextMixin,
ResolveUrlMixin,
],
components: {
Ingredient,
@ -157,6 +185,10 @@ export default {
index: Number,
recipe: Object,
start_time: String,
force_ingredients: {
type: Boolean,
default: false
}
},
data() {
return {

View File

@ -20,6 +20,8 @@
"Add_to_Shopping": "Add to Shopping",
"Add_to_Plan": "Add to Plan",
"Step_start_time": "Step start time",
"Sort_by_new": "Sort by new",
"Recipes_per_page": "Recipes per Page",
"Meal_Plan": "Meal Plan",
"Select_Book": "Select Book",

View File

@ -53,5 +53,26 @@
"Supermarket": "Supermarkt",
"Categories": "Categorieën",
"Category": "Categorie",
"Selected": "Geselecteerd"
"Selected": "Geselecteerd",
"Copy": "Kopie",
"Link": "Link",
"Sort_by_new": "Sorteer op nieuw",
"Recipes_per_page": "Recepten per pagina",
"Files": "Bestanden",
"Size": "Grootte",
"File": "Bestand",
"err_fetching_resource": "Bij het ophalen van een hulpbron is een foutmelding opgetreden!",
"err_creating_resource": "Bij het maken van een hulpbron is een foutmelding opgetreden!",
"err_updating_resource": "Bij het updaten van een hulpbron is een foutmelding opgetreden!",
"success_fetching_resource": "Hulpbron is succesvol opgehaald!",
"success_creating_resource": "Hulpbron succesvol aangemaakt!",
"success_updating_resource": "Hulpbron succesvol geüpdatet!",
"Success": "Succes",
"Download": "Download",
"err_deleting_resource": "Bij het verwijderen van een hulpbron is een foutmelding opgetreden!",
"success_deleting_resource": "Hulpbron succesvol verwijderd!",
"Cancel": "Annuleer",
"Delete": "Verwijder",
"Ok": "Open",
"Load_More": "Laad meer"
}

View File

@ -1188,6 +1188,18 @@ export interface RecipeSteps {
* @memberof RecipeSteps
*/
file?: StepFile | null;
/**
*
* @type {number}
* @memberof RecipeSteps
*/
step_recipe?: number | null;
/**
*
* @type {string}
* @memberof RecipeSteps
*/
step_recipe_data?: string;
}
/**
@ -1197,7 +1209,8 @@ export interface RecipeSteps {
export enum RecipeStepsTypeEnum {
Text = 'TEXT',
Time = 'TIME',
File = 'FILE'
File = 'FILE',
Recipe = 'RECIPE'
}
/**
@ -1593,6 +1606,18 @@ export interface Step {
* @memberof Step
*/
file?: StepFile | null;
/**
*
* @type {number}
* @memberof Step
*/
step_recipe?: number | null;
/**
*
* @type {string}
* @memberof Step
*/
step_recipe_data?: string;
}
/**
@ -1602,7 +1627,8 @@ export interface Step {
export enum StepTypeEnum {
Text = 'TEXT',
Time = 'TIME',
File = 'FILE'
File = 'FILE',
Recipe = 'RECIPE'
}
/**
@ -1851,6 +1877,37 @@ export interface SupermarketCategory {
*/
name: string;
}
/**
*
* @export
* @interface SupermarketCategoryRelation
*/
export interface SupermarketCategoryRelation {
/**
*
* @type {number}
* @memberof SupermarketCategoryRelation
*/
id?: number;
/**
*
* @type {ShoppingListSupermarketCategory}
* @memberof SupermarketCategoryRelation
*/
category: ShoppingListSupermarketCategory;
/**
*
* @type {number}
* @memberof SupermarketCategoryRelation
*/
supermarket: number;
/**
*
* @type {number}
* @memberof SupermarketCategoryRelation
*/
order?: number;
}
/**
*
* @export
@ -2756,6 +2813,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions,
};
},
/**
*
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createSupermarketCategoryRelation: async (supermarketCategoryRelation?: SupermarketCategoryRelation, options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/supermarket-category-relation/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(supermarketCategoryRelation, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {Sync} [sync]
@ -3528,6 +3618,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
destroySupermarketCategoryRelation: async (id: string, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('destroySupermarketCategoryRelation', 'id', id)
const localVarPath = `/api/supermarket-category-relation/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -4070,12 +4193,13 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
* @param {string} [booksOr] If recipe should be in all (AND) or any (OR) any of the provided books.
* @param {string} [internal] true or false. If only internal recipes should be returned or not.
* @param {string} [random] true or false. returns the results in randomized order.
* @param {string} [_new] true or false. returns new results first in search results
* @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}
*/
listRecipes: async (query?: string, keywords?: string, foods?: string, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
listRecipes: async (query?: string, keywords?: string, foods?: string, books?: 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);
@ -4124,6 +4248,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
localVarQueryParameter['random'] = random;
}
if (_new !== undefined) {
localVarQueryParameter['new'] = _new;
}
if (page !== undefined) {
localVarQueryParameter['page'] = page;
}
@ -4279,6 +4407,35 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listSupermarketCategoryRelations: async (options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/supermarket-category-relation/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -5297,6 +5454,43 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
partialUpdateSupermarketCategoryRelation: async (id: string, supermarketCategoryRelation?: SupermarketCategoryRelation, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('partialUpdateSupermarketCategoryRelation', 'id', id)
const localVarPath = `/api/supermarket-category-relation/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(supermarketCategoryRelation, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -6089,6 +6283,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
retrieveSupermarketCategoryRelation: async (id: string, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('retrieveSupermarketCategoryRelation', 'id', id)
const localVarPath = `/api/supermarket-category-relation/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -6995,6 +7222,43 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateSupermarketCategoryRelation: async (id: string, supermarketCategoryRelation?: SupermarketCategoryRelation, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updateSupermarketCategoryRelation', 'id', id)
const localVarPath = `/api/supermarket-category-relation/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(supermarketCategoryRelation, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -7392,6 +7656,16 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.createSupermarketCategory(supermarketCategory, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createSupermarketCategoryRelation(supermarketCategoryRelation?: SupermarketCategoryRelation, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SupermarketCategoryRelation>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createSupermarketCategoryRelation(supermarketCategoryRelation, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {Sync} [sync]
@ -7625,6 +7899,16 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.destroySupermarketCategory(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async destroySupermarketCategoryRelation(id: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.destroySupermarketCategoryRelation(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -7792,13 +8076,14 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {string} [booksOr] If recipe should be in all (AND) or any (OR) any of the provided books.
* @param {string} [internal] true or false. If only internal recipes should be returned or not.
* @param {string} [random] true or false. returns the results in randomized order.
* @param {string} [_new] true or false. returns new results first in search results
* @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 listRecipes(query?: string, keywords?: string, foods?: string, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2001>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, foods, books, keywordsOr, foodsOr, booksOr, internal, random, page, pageSize, options);
async listRecipes(query?: string, keywords?: string, foods?: string, 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<InlineResponse2001>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, foods, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -7846,6 +8131,15 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.listStorages(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listSupermarketCategoryRelations(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<SupermarketCategoryRelation>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listSupermarketCategoryRelations(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
@ -8149,6 +8443,17 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateSupermarketCategory(id, supermarketCategory, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async partialUpdateSupermarketCategoryRelation(id: string, supermarketCategoryRelation?: SupermarketCategoryRelation, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SupermarketCategoryRelation>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateSupermarketCategoryRelation(id, supermarketCategoryRelation, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -8387,6 +8692,16 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveSupermarketCategory(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async retrieveSupermarketCategoryRelation(id: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SupermarketCategoryRelation>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveSupermarketCategoryRelation(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -8655,6 +8970,17 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateSupermarketCategory(id, supermarketCategory, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateSupermarketCategoryRelation(id: string, supermarketCategoryRelation?: SupermarketCategoryRelation, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SupermarketCategoryRelation>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateSupermarketCategoryRelation(id, supermarketCategoryRelation, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -8885,6 +9211,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
createSupermarketCategory(supermarketCategory?: SupermarketCategory, options?: any): AxiosPromise<SupermarketCategory> {
return localVarFp.createSupermarketCategory(supermarketCategory, options).then((request) => request(axios, basePath));
},
/**
*
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createSupermarketCategoryRelation(supermarketCategoryRelation?: SupermarketCategoryRelation, options?: any): AxiosPromise<SupermarketCategoryRelation> {
return localVarFp.createSupermarketCategoryRelation(supermarketCategoryRelation, options).then((request) => request(axios, basePath));
},
/**
*
* @param {Sync} [sync]
@ -9095,6 +9430,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
destroySupermarketCategory(id: string, options?: any): AxiosPromise<void> {
return localVarFp.destroySupermarketCategory(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
destroySupermarketCategoryRelation(id: string, options?: any): AxiosPromise<void> {
return localVarFp.destroySupermarketCategoryRelation(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -9246,13 +9590,14 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {string} [booksOr] If recipe should be in all (AND) or any (OR) any of the provided books.
* @param {string} [internal] true or false. If only internal recipes should be returned or not.
* @param {string} [random] true or false. returns the results in randomized order.
* @param {string} [_new] true or false. returns new results first in search results
* @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}
*/
listRecipes(query?: string, keywords?: string, foods?: string, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2001> {
return localVarFp.listRecipes(query, keywords, foods, books, keywordsOr, foodsOr, booksOr, internal, random, page, pageSize, options).then((request) => request(axios, basePath));
listRecipes(query?: string, keywords?: string, foods?: string, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2001> {
return localVarFp.listRecipes(query, keywords, foods, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(axios, basePath));
},
/**
*
@ -9294,6 +9639,14 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
listStorages(options?: any): AxiosPromise<Array<Storage>> {
return localVarFp.listStorages(options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listSupermarketCategoryRelations(options?: any): AxiosPromise<Array<SupermarketCategoryRelation>> {
return localVarFp.listSupermarketCategoryRelations(options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
@ -9568,6 +9921,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
partialUpdateSupermarketCategory(id: string, supermarketCategory?: SupermarketCategory, options?: any): AxiosPromise<SupermarketCategory> {
return localVarFp.partialUpdateSupermarketCategory(id, supermarketCategory, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
partialUpdateSupermarketCategoryRelation(id: string, supermarketCategoryRelation?: SupermarketCategoryRelation, options?: any): AxiosPromise<SupermarketCategoryRelation> {
return localVarFp.partialUpdateSupermarketCategoryRelation(id, supermarketCategoryRelation, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -9783,6 +10146,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
retrieveSupermarketCategory(id: string, options?: any): AxiosPromise<SupermarketCategory> {
return localVarFp.retrieveSupermarketCategory(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
retrieveSupermarketCategoryRelation(id: string, options?: any): AxiosPromise<SupermarketCategoryRelation> {
return localVarFp.retrieveSupermarketCategoryRelation(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -10026,6 +10398,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
updateSupermarketCategory(id: string, supermarketCategory?: SupermarketCategory, options?: any): AxiosPromise<SupermarketCategory> {
return localVarFp.updateSupermarketCategory(id, supermarketCategory, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateSupermarketCategoryRelation(id: string, supermarketCategoryRelation?: SupermarketCategoryRelation, options?: any): AxiosPromise<SupermarketCategoryRelation> {
return localVarFp.updateSupermarketCategoryRelation(id, supermarketCategoryRelation, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -10287,6 +10669,17 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).createSupermarketCategory(supermarketCategory, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public createSupermarketCategoryRelation(supermarketCategoryRelation?: SupermarketCategoryRelation, options?: any) {
return ApiApiFp(this.configuration).createSupermarketCategoryRelation(supermarketCategoryRelation, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {Sync} [sync]
@ -10543,6 +10936,17 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).destroySupermarketCategory(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public destroySupermarketCategoryRelation(id: string, options?: any) {
return ApiApiFp(this.configuration).destroySupermarketCategoryRelation(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -10726,14 +11130,15 @@ export class ApiApi extends BaseAPI {
* @param {string} [booksOr] If recipe should be in all (AND) or any (OR) any of the provided books.
* @param {string} [internal] true or false. If only internal recipes should be returned or not.
* @param {string} [random] true or false. returns the results in randomized order.
* @param {string} [_new] true or false. returns new results first in search results
* @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 listRecipes(query?: string, keywords?: string, foods?: string, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, page?: number, pageSize?: number, options?: any) {
return ApiApiFp(this.configuration).listRecipes(query, keywords, foods, books, keywordsOr, foodsOr, booksOr, internal, random, page, pageSize, options).then((request) => request(this.axios, this.basePath));
public listRecipes(query?: string, keywords?: string, foods?: string, 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, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(this.axios, this.basePath));
}
/**
@ -10786,6 +11191,16 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).listStorages(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public listSupermarketCategoryRelations(options?: any) {
return ApiApiFp(this.configuration).listSupermarketCategoryRelations(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.
@ -11118,6 +11533,18 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).partialUpdateSupermarketCategory(id, supermarketCategory, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public partialUpdateSupermarketCategoryRelation(id: string, supermarketCategoryRelation?: SupermarketCategoryRelation, options?: any) {
return ApiApiFp(this.configuration).partialUpdateSupermarketCategoryRelation(id, supermarketCategoryRelation, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -11379,6 +11806,17 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).retrieveSupermarketCategory(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public retrieveSupermarketCategoryRelation(id: string, options?: any) {
return ApiApiFp(this.configuration).retrieveSupermarketCategoryRelation(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this sync.
@ -11672,6 +12110,18 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).updateSupermarketCategory(id, supermarketCategory, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this supermarket category relation.
* @param {SupermarketCategoryRelation} [supermarketCategoryRelation]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public updateSupermarketCategoryRelation(id: string, supermarketCategoryRelation?: SupermarketCategoryRelation, options?: any) {
return ApiApiFp(this.configuration).updateSupermarketCategoryRelation(id, supermarketCategoryRelation, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this sync.

View File

@ -79,7 +79,10 @@ module.exports = {
priority: 1
},
},
});
},
// TODO make this conditional on .env DEBUG = TRUE
// config.optimization.minimize(false)
);
//TODO somehow remov them as they are also added to the manifest config of the service worker
/*

View File

@ -1063,6 +1063,20 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
"@riophae/vue-treeselect@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@riophae/vue-treeselect/-/vue-treeselect-0.4.0.tgz#0baed5a794cffc580b63591f35c125e51c0df241"
integrity sha512-J4atYmBqXQmiPFK/0B5sXKjtnGc21mBJEiyKIDZwk0Q9XuynVFX6IJ4EpaLmUgL5Tve7HAS7wkiGGSti6Uaxcg==
dependencies:
"@babel/runtime" "^7.3.1"
babel-helper-vue-jsx-merge-props "^2.0.3"
easings-css "^1.0.0"
fuzzysearch "^1.0.3"
is-promise "^2.1.0"
lodash "^4.0.0"
material-colors "^1.2.6"
watch-size "^2.0.0"
"@rollup/plugin-babel@^5.2.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879"
@ -2255,6 +2269,11 @@ babel-extract-comments@^1.0.0:
dependencies:
babylon "^6.18.0"
babel-helper-vue-jsx-merge-props@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6"
integrity sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==
babel-loader@^8.1.0:
version "8.2.2"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81"
@ -3804,6 +3823,11 @@ duplexify@^3.4.2, duplexify@^3.6.0:
readable-stream "^2.0.0"
stream-shift "^1.0.0"
easings-css@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/easings-css/-/easings-css-1.0.0.tgz#dde569003bb7a4a0c0b77878f5db3e0be5679c81"
integrity sha512-7Uq7NdazNfVtr0RNmPAys8it0zKCuaqxJStYKEl72D3j4gbvXhhaM7iWNbqhA4C94ygCye6VuyhzBRQC4szeBg==
easy-stack@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/easy-stack/-/easy-stack-1.0.1.tgz#8afe4264626988cabb11f3c704ccd0c835411066"
@ -4674,6 +4698,11 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
fuzzysearch@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/fuzzysearch/-/fuzzysearch-1.0.3.tgz#dffc80f6d6b04223f2226aa79dd194231096d008"
integrity sha1-3/yA9tawQiPyImqnndGUIxCW0Ag=
generic-names@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872"
@ -5579,6 +5608,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-promise@^2.1.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
is-regex@^1.0.4, is-regex@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
@ -6052,7 +6086,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3:
lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -6137,6 +6171,11 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
material-colors@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -9509,6 +9548,11 @@ vuex@^3.6.0:
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==
watch-size@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/watch-size/-/watch-size-2.0.0.tgz#096ee28d0365bd7ea03d9c8bf1f2f50a73be1474"
integrity sha512-M92R89dNoTPWyCD+HuUEDdhaDnh9jxPGOwlDc0u51jAgmjUvzqaEMynXSr3BaWs+QdHYk4KzibPy1TFtjLmOZQ==
watchpack-chokidar2@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"