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
from cookbook.managers import DICTIONARY
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
def has_add_permission(self, request, obj=None): 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): def get_filetype(name):
try: try:
return os.path.splitext(name)[1] return os.path.splitext(name)[1]
except: except Exception:
return '.jpeg' return '.jpeg'

View File

@ -3,8 +3,6 @@ Source: https://djangosnippets.org/snippets/1703/
""" """
from django.conf import settings from django.conf import settings
from django.core.cache import caches 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 cookbook.models import ShareLink
from django.contrib import messages from django.contrib import messages
@ -64,7 +62,7 @@ def is_object_owner(user, obj):
return False return False
try: try:
return obj.get_owner() == user return obj.get_owner() == user
except: except Exception:
return False return False

View File

@ -4,7 +4,7 @@ from recipes import settings
from django.contrib.postgres.search import ( from django.contrib.postgres.search import (
SearchQuery, SearchRank, TrigramSimilarity 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 django.utils import translation
from cookbook.managers import DICTIONARY from cookbook.managers import DICTIONARY
@ -14,6 +14,7 @@ from cookbook.models import Food, Keyword, ViewLog
def search_recipes(request, queryset, params): def search_recipes(request, queryset, params):
search_prefs = request.user.searchpreference search_prefs = request.user.searchpreference
search_string = params.get('query', '') search_string = params.get('query', '')
search_ratings = params.getlist('ratings', [])
search_keywords = params.getlist('keywords', []) search_keywords = params.getlist('keywords', [])
search_foods = params.getlist('foods', []) search_foods = params.getlist('foods', [])
search_books = params.getlist('books', []) search_books = params.getlist('books', [])
@ -24,8 +25,10 @@ def search_recipes(request, queryset, params):
search_internal = params.get('internal', None) search_internal = params.get('internal', None)
search_random = params.get('random', False) search_random = params.get('random', False)
search_new = params.get('new', False)
search_last_viewed = int(params.get('last_viewed', 0)) 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: if search_last_viewed > 0:
last_viewed_recipes = ViewLog.objects.filter( last_viewed_recipes = ViewLog.objects.filter(
created_by=request.user, space=request.space, 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):]) return queryset.filter(pk__in=last_viewed_recipes[len(last_viewed_recipes) - min(len(last_viewed_recipes), search_last_viewed):])
queryset = queryset.annotate( orderby = []
new_recipe=Case(When( if search_new == 'true':
created_at__gte=(datetime.now() - timedelta(days=7)), then=Value(100)), queryset = queryset.annotate(
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_type = search_prefs.search or 'plain'
search_sort = None
if len(search_string) > 0: if len(search_string) > 0:
unaccent_include = search_prefs.unaccent.values_list('field', flat=True) unaccent_include = search_prefs.unaccent.values_list('field', flat=True)
@ -106,22 +112,30 @@ def search_recipes(request, queryset, params):
else: else:
query_filter = f 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 = ( search_rank = (
SearchRank('name_search_vector', search_query, cover_density=True) SearchRank('name_search_vector', search_query, cover_density=True)
+ SearchRank('desc_search_vector', search_query, cover_density=True) + SearchRank('desc_search_vector', search_query, cover_density=True)
+ SearchRank('steps__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) queryset = queryset.filter(query_filter).annotate(rank=search_rank)
orderby += ['-rank']
else: else:
queryset = queryset.filter(name__icontains=search_string) queryset = queryset.filter(query_filter)
if len(search_keywords) > 0: if len(search_keywords) > 0:
# TODO creating setting to include descendants of keywords a setting
if search_keywords_or == 'true': 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) queryset = queryset.filter(keywords__id__in=search_keywords)
else: else:
for k in search_keywords: # when performing an 'and' search returned recipes should include a parent OR any of its descedants
queryset = queryset.filter(keywords__id=k) # 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 len(search_foods) > 0:
if search_foods_or == 'true': if search_foods_or == 'true':
@ -144,11 +158,122 @@ def search_recipes(request, queryset, params):
if search_random == 'true': if search_random == 'true':
queryset = queryset.order_by("?") queryset = queryset.order_by("?")
elif search_sort == 'rank': else:
queryset = queryset.order_by('-rank') # TODO add order by user settings
orderby += ['name']
queryset = queryset.order_by(*orderby)
return queryset return queryset
# this returns a list of keywords in the queryset and how many times it appears def get_facet(qs, params):
# Keyword.objects.filter(recipe__in=queryset).annotate(kw_count=Count('recipe')) 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.urls import reverse
from django_scopes import scope, scopes_disabled from django_scopes import scope, scopes_disabled

View File

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

View File

@ -6,6 +6,7 @@ from cookbook.helper.mdx_urlize import UrlizeExtension
from jinja2 import Template, TemplateSyntaxError, UndefinedError from jinja2 import Template, TemplateSyntaxError, UndefinedError
from gettext import gettext as _ from gettext import gettext as _
class IngredientObject(object): class IngredientObject(object):
amount = "" amount = ""
unit = "" 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.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration 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): class Pepperplate(Integration):

View File

@ -1,10 +1,8 @@
import re 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 parse, get_food, get_unit
from cookbook.integration.integration import Integration 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): class ChefTap(Integration):

View File

@ -1,4 +1,3 @@
import json
import re import re
from io import BytesIO from io import BytesIO
from zipfile import ZipFile from zipfile import ZipFile
@ -6,7 +5,7 @@ from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration 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): class Chowdown(Integration):
@ -52,7 +51,7 @@ class Chowdown(Integration):
for k in tags.split(','): for k in tags.split(','):
print(f'adding keyword {k.strip()}') 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) recipe.keywords.add(keyword)
step = Step.objects.create( step = Step.objects.create(

View File

@ -1,7 +1,5 @@
import datetime import datetime
import json import json
import os
import re
import traceback import traceback
import uuid import uuid
from io import BytesIO, StringIO from io import BytesIO, StringIO
@ -16,7 +14,7 @@ from django_scopes import scope
from cookbook.forms import ImportExportBase from cookbook.forms import ImportExportBase
from cookbook.helper.image_processing import get_filetype from cookbook.helper.image_processing import get_filetype
from cookbook.models import Keyword, Recipe from cookbook.models import Keyword, Recipe
from recipes.settings import DEBUG from recipes.settings import DATABASES, DEBUG
class Integration: class Integration:
@ -33,8 +31,29 @@ class Integration:
""" """
self.request = request self.request = request
self.export_type = export_type self.export_type = export_type
# TODO add all import keywords under the importer root node name = f'Import {export_type}'
self.keyword = Keyword.objects.first() 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): def do_export(self, recipes):
""" """
@ -181,7 +200,7 @@ class Integration:
except BadZipFile: except BadZipFile:
il.msg += 'ERROR ' + _( il.msg += 'ERROR ' + _(
'Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n' 'Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n'
except: except Exception as e:
msg = 'ERROR ' + _( msg = 'ERROR ' + _(
'An unexpected error occurred during the import. Please make sure you have uploaded a valid file.') + '\n' '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) 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.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration 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): class Mealie(Integration):
@ -23,7 +23,8 @@ class Mealie(Integration):
name=recipe_json['name'].strip(), description=description, name=recipe_json['name'].strip(), description=description,
created_by=self.request.user, internal=True, space=self.request.space) created_by=self.request.user, internal=True, space=self.request.space)
# TODO parse times (given in PT2H3M ) # 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 ingredients_added = False
for s in recipe_json['recipe_instructions']: for s in recipe_json['recipe_instructions']:
@ -50,7 +51,7 @@ class Mealie(Integration):
step.ingredients.add(Ingredient.objects.create( step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space, food=f, unit=u, amount=amount, note=note, space=self.request.space,
)) ))
except: except Exception:
pass pass
recipe.steps.add(step) recipe.steps.add(step)
@ -59,7 +60,7 @@ class Mealie(Integration):
import_zip = ZipFile(f['file']) import_zip = ZipFile(f['file'])
try: try:
self.import_recipe_image(recipe, BytesIO(import_zip.read(f'recipes/{recipe_json["slug"]}/images/min-original.webp')), filetype=get_filetype(f'recipes/{recipe_json["slug"]}/images/original')) self.import_recipe_image(recipe, BytesIO(import_zip.read(f'recipes/{recipe_json["slug"]}/images/min-original.webp')), filetype=get_filetype(f'recipes/{recipe_json["slug"]}/images/original'))
except: except Exception:
pass pass
return recipe return recipe

View File

@ -1,11 +1,8 @@
import json
import re import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration 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): class MealMaster(Integration):

View File

@ -6,7 +6,7 @@ from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration 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): class NextcloudCookbook(Integration):
@ -25,6 +25,7 @@ class NextcloudCookbook(Integration):
servings=recipe_json['recipeYield'], space=self.request.space) servings=recipe_json['recipeYield'], space=self.request.space)
# TODO parse times (given in PT2H3M ) # 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 # TODO parse keywords
ingredients_added = False ingredients_added = False

View File

@ -1,11 +1,8 @@
import json import json
import re
from django.utils.translation import gettext as _ from cookbook.helper.ingredient_parser import get_food, get_unit
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration 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): class OpenEats(Integration):

View File

@ -16,7 +16,7 @@ class Paprika(Integration):
raise NotImplementedError('Method not implemented in storage integration') raise NotImplementedError('Method not implemented in storage integration')
def get_recipe_from_file(self, file): def get_recipe_from_file(self, file):
with gzip.open(file, 'r') as recipe_zip: with gzip.open(file, 'r') as recipe_zip:
recipe_json = json.loads(recipe_zip.read().decode("utf-8")) recipe_json = json.loads(recipe_zip.read().decode("utf-8"))
recipe = Recipe.objects.create( recipe = Recipe.objects.create(

View File

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

View File

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

View File

@ -1,9 +1,7 @@
import base64
import json import json
from io import BytesIO from io import BytesIO
import requests import requests
from rest_framework.renderers import JSONRenderer
from cookbook.helper.ingredient_parser import parse, get_food, get_unit from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration 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.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration 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): 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.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration 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): class Safron(Integration):

View File

@ -13,7 +13,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-06-12 20:30+0200\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" "Last-Translator: Jesse <jesse.kamps@pm.me>\n"
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/" "Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/nl/>\n" "recipes-backend/nl/>\n"
@ -22,7 +22,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\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\filters.py:23 .\cookbook\templates\base.html:98
#: .\cookbook\templates\forms\edit_internal_recipe.html:246 #: .\cookbook\templates\forms\edit_internal_recipe.html:246
@ -453,7 +453,7 @@ msgstr "Bewerken"
#: .\cookbook\templates\meal_plan.html:277 #: .\cookbook\templates\meal_plan.html:277
#: .\cookbook\templates\recipes_table.html:90 #: .\cookbook\templates\recipes_table.html:90
msgid "Delete" msgid "Delete"
msgstr "Verwijderen" msgstr "Verwijder"
#: .\cookbook\templates\404.html:5 #: .\cookbook\templates\404.html:5
msgid "404 Error" msgid "404 Error"
@ -1629,7 +1629,7 @@ msgid ""
"To join an existing space either enter your invite token or click on the " "To join an existing space either enter your invite token or click on the "
"invite link the space owner send you." "invite link the space owner send you."
msgstr "" 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." "invoeren of op de uitnodingslink klikken die je ontvangen hebt."
#: .\cookbook\templates\no_space_info.html:48 #: .\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:49 .\cookbook\templates\system.html:64
#: .\cookbook\templates\system.html:80 .\cookbook\templates\system.html:95 #: .\cookbook\templates\system.html:80 .\cookbook\templates\system.html:95
msgid "Ok" msgid "Ok"
msgstr "Ok" msgstr "Oké"
#: .\cookbook\templates\system.html:51 #: .\cookbook\templates\system.html:51
msgid "" msgid ""
@ -2098,6 +2098,7 @@ msgstr "Bekijk recept gegevens"
#: .\cookbook\templates\url_import.html:147 #: .\cookbook\templates\url_import.html:147
msgid "Drag recipe attributes from the right into the appropriate box below." msgid "Drag recipe attributes from the right into the appropriate box below."
msgstr "" msgstr ""
"Sleep eigenschappen van het recept van rechts naar het juiste vlak beneden."
#: .\cookbook\templates\url_import.html:156 #: .\cookbook\templates\url_import.html:156
#: .\cookbook\templates\url_import.html:173 #: .\cookbook\templates\url_import.html:173
@ -2110,82 +2111,73 @@ msgstr ""
#: .\cookbook\templates\url_import.html:300 #: .\cookbook\templates\url_import.html:300
#: .\cookbook\templates\url_import.html:351 #: .\cookbook\templates\url_import.html:351
msgid "Clear Contents" msgid "Clear Contents"
msgstr "" msgstr "Wis inhoud"
#: .\cookbook\templates\url_import.html:158 #: .\cookbook\templates\url_import.html:158
msgid "Text dragged here will be appended to the name." 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 #: .\cookbook\templates\url_import.html:175
msgid "Text dragged here will be appended to the description." 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 #: .\cookbook\templates\url_import.html:192
msgid "Keywords dragged here will be appended to current list" 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 #: .\cookbook\templates\url_import.html:207
msgid "Image" msgid "Image"
msgstr "" msgstr "Afbeelding"
#: .\cookbook\templates\url_import.html:239 #: .\cookbook\templates\url_import.html:239
#, fuzzy
#| msgid "Preparation Time"
msgid "Prep Time" msgid "Prep Time"
msgstr "Bereidingstijd" msgstr "Voorbereidingstijd"
#: .\cookbook\templates\url_import.html:254 #: .\cookbook\templates\url_import.html:254
#, fuzzy
#| msgid "Time"
msgid "Cook Time" msgid "Cook Time"
msgstr "Tijd" msgstr "Kooktijd"
#: .\cookbook\templates\url_import.html:275 #: .\cookbook\templates\url_import.html:275
msgid "Ingredients dragged here will be appended to current list." 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 #: .\cookbook\templates\url_import.html:302
msgid "" msgid ""
"Recipe instructions dragged here will be appended to current instructions." "Recipe instructions dragged here will be appended to current instructions."
msgstr "" msgstr ""
"Hierheen gesleepte Recept instructies worden aan de huidige lijst toegevoegd."
#: .\cookbook\templates\url_import.html:325 #: .\cookbook\templates\url_import.html:325
#, fuzzy
#| msgid "Discovered Recipes"
msgid "Discovered Attributes" msgid "Discovered Attributes"
msgstr "Ontdekte recepten" msgstr "Ontdekte Eigenschappen"
#: .\cookbook\templates\url_import.html:327 #: .\cookbook\templates\url_import.html:327
msgid "" msgid ""
"Drag recipe attributes from below into the appropriate box on the left. " "Drag recipe attributes from below into the appropriate box on the left. "
"Click any node to display its full properties." "Click any node to display its full properties."
msgstr "" 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 #: .\cookbook\templates\url_import.html:344
#, fuzzy
#| msgid "Show as header"
msgid "Show Blank Field" msgid "Show Blank Field"
msgstr "Laat als kop zien" msgstr "Toon Leeg Veld"
#: .\cookbook\templates\url_import.html:349 #: .\cookbook\templates\url_import.html:349
msgid "Blank Field" msgid "Blank Field"
msgstr "" msgstr "Leeg Veld"
#: .\cookbook\templates\url_import.html:353 #: .\cookbook\templates\url_import.html:353
msgid "Items dragged to Blank Field will be appended." msgid "Items dragged to Blank Field will be appended."
msgstr "" msgstr "Naar Leeg Veld gesleepte items worden toegevoegd."
#: .\cookbook\templates\url_import.html:400 #: .\cookbook\templates\url_import.html:400
#, fuzzy
#| msgid "Delete Step"
msgid "Delete Text" msgid "Delete Text"
msgstr "Verwijder stap" msgstr "Verwijder tekst"
#: .\cookbook\templates\url_import.html:413 #: .\cookbook\templates\url_import.html:413
#, fuzzy
#| msgid "Delete Recipe"
msgid "Delete image" msgid "Delete image"
msgstr "Verwijder recept" msgstr "Verwijder afbeelding"
#: .\cookbook\templates\url_import.html:429 #: .\cookbook\templates\url_import.html:429
msgid "Recipe Name" msgid "Recipe Name"
@ -2261,7 +2253,7 @@ msgstr "Er is een fout opgetreden bij het synchroniseren met Opslag"
#: .\cookbook\views\api.py:649 #: .\cookbook\views\api.py:649
msgid "Nothing to do." msgid "Nothing to do."
msgstr "" msgstr "Niks te doen."
#: .\cookbook\views\api.py:664 #: .\cookbook\views\api.py:664
msgid "The requested site provided malformed data and cannot be read." msgid "The requested site provided malformed data and cannot be read."
@ -2282,26 +2274,24 @@ msgstr ""
"te importeren." "te importeren."
#: .\cookbook\views\api.py:694 #: .\cookbook\views\api.py:694
#, fuzzy
#| msgid "The requested page could not be found."
msgid "No useable data could 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 #: .\cookbook\views\api.py:710
msgid "I couldn't find anything to do." 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\data.py:30 .\cookbook\views\data.py:121
#: .\cookbook\views\edit.py:50 .\cookbook\views\import_export.py:67 #: .\cookbook\views\edit.py:50 .\cookbook\views\import_export.py:67
#: .\cookbook\views\new.py:32 #: .\cookbook\views\new.py:32
msgid "You have reached the maximum number of recipes for your space." 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\data.py:34 .\cookbook\views\data.py:125
#: .\cookbook\views\edit.py:54 .\cookbook\views\import_export.py:71 #: .\cookbook\views\edit.py:54 .\cookbook\views\import_export.py:71
#: .\cookbook\views\new.py:36 #: .\cookbook\views\new.py:36
msgid "You have more users than allowed in your space." 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 #: .\cookbook\views\data.py:103
#, python-format #, python-format
@ -2409,57 +2399,64 @@ msgstr "Er is een fout opgetreden bij het importeren van dit recept!"
#: .\cookbook\views\new.py:226 #: .\cookbook\views\new.py:226
msgid "Hello" msgid "Hello"
msgstr "" msgstr "Hallo"
#: .\cookbook\views\new.py:226 #: .\cookbook\views\new.py:226
msgid "You have been invited by " msgid "You have been invited by "
msgstr "" msgstr "Je bent uitgenodigd door "
#: .\cookbook\views\new.py:227 #: .\cookbook\views\new.py:227
msgid " to join their Tandoor Recipes space " msgid " to join their Tandoor Recipes space "
msgstr "" msgstr " om zijn/haar Tandoor Recepten ruimte "
#: .\cookbook\views\new.py:228 #: .\cookbook\views\new.py:228
msgid "Click the following link to activate your account: " 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 #: .\cookbook\views\new.py:229
msgid "" msgid ""
"If the link does not work use the following code to manually join the space: " "If the link does not work use the following code to manually join the space: "
msgstr "" msgstr ""
"Als de linkt niet werkt, gebruik dan de volgende code om handmatig tot de "
"ruimte toe te treden: "
#: .\cookbook\views\new.py:230 #: .\cookbook\views\new.py:230
msgid "The invitation is valid until " msgid "The invitation is valid until "
msgstr "" msgstr "De uitnodiging is geldig tot "
#: .\cookbook\views\new.py:231 #: .\cookbook\views\new.py:231
msgid "" msgid ""
"Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub " "Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub "
msgstr "" msgstr ""
"Tandoor Recepten is een Open Source recepten manager. Bekijk het op GitHub "
#: .\cookbook\views\new.py:234 #: .\cookbook\views\new.py:234
msgid "Tandoor Recipes Invite" msgid "Tandoor Recipes Invite"
msgstr "" msgstr "Tandoor Recepten uitnodiging"
#: .\cookbook\views\new.py:241 #: .\cookbook\views\new.py:241
msgid "Invite link successfully send to user." msgid "Invite link successfully send to user."
msgstr "" msgstr "Uitnodigingslink succesvol verstuurd naar gebruiker."
#: .\cookbook\views\new.py:244 #: .\cookbook\views\new.py:244
msgid "" msgid ""
"You have send to many emails, please share the link manually or wait a few " "You have send to many emails, please share the link manually or wait a few "
"hours." "hours."
msgstr "" msgstr ""
"Je hebt te veel e-mails verstuurd, deel de link handmatig of wacht enkele "
"uren."
#: .\cookbook\views\new.py:246 #: .\cookbook\views\new.py:246
msgid "Email to user could not be send, please share link manually." 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 #: .\cookbook\views\views.py:125
msgid "" msgid ""
"You have successfully created your own recipe space. Start by adding some " "You have successfully created your own recipe space. Start by adding some "
"recipes or invite other people to join you." "recipes or invite other people to join you."
msgstr "" 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 #: .\cookbook\views\views.py:173
msgid "You do not have the required permissions to perform this action!" 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!" msgstr "Onjuiste uitnodigingslink opgegeven!"
#: .\cookbook\views\views.py:441 #: .\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." 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 #: .\cookbook\views\views.py:452
msgid "Successfully joined space." msgid "Successfully joined space."
msgstr "" msgstr "Succesvol toegetreden tot ruimte."
#: .\cookbook\views\views.py:458 #: .\cookbook\views\views.py:458
msgid "Invite Link not valid or already used!" 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)) Step.objects.all().update(search_vector=SearchVector('instruction__unaccent', weight='B', config=language))
self.stdout.write(self.style.SUCCESS(_('Recipe index rebuild complete.'))) self.stdout.write(self.style.SUCCESS(_('Recipe index rebuild complete.')))
except: except Exception:
self.stdout.write(self.style.ERROR(_('Recipe index rebuild failed.'))) 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.aggregates import StringAgg
from django.contrib.postgres.search import ( from django.contrib.postgres.search import (
SearchQuery, SearchRank, SearchVector, TrigramSimilarity, SearchQuery, SearchRank, SearchVector,
) )
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils import translation from django.utils import translation
# TODO move this somewhere else and delete this file
DICTIONARY = { DICTIONARY = {
# TODO find custom dictionaries - maybe from here https://www.postgresql.org/message-id/CAF4Au4x6X_wSXFwsQYE8q5o0aQZANrvYjZJ8uOnsiHDnOVPPEg%40mail.gmail.com # TODO find custom dictionaries - maybe from here https://www.postgresql.org/message-id/CAF4Au4x6X_wSXFwsQYE8q5o0aQZANrvYjZJ8uOnsiHDnOVPPEg%40mail.gmail.com
# 'hy': 'Armenian', # 'hy': 'Armenian',
@ -22,8 +21,6 @@ DICTIONARY = {
} }
# TODO add search highlighting
# TODO add language support
# TODO add schedule index rebuild # TODO add schedule index rebuild
class RecipeSearchManager(models.Manager): class RecipeSearchManager(models.Manager):
def search(self, search_text, space): 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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('cookbook', '0141_keyword_to_tree'), ('cookbook', '0141_auto_20210713_1042'),
] ]
operations = [ operations = [
migrations.AddField( 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() 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 @staticmethod
def get_space_key(): def get_space_key():
return ('space',) return ('space',)
@ -217,8 +229,9 @@ class SupermarketCategory(models.Model, PermissionModelMixin):
return self.name return self.name
class Meta: class Meta:
# TODO according to this https://docs.djangoproject.com/en/3.1/ref/models/options/#unique-together should not be used constraints = [
unique_together = (('space', 'name'),) models.UniqueConstraint(fields=['space', 'name'], name='smc_unique_name_per_space')
]
class Supermarket(models.Model, PermissionModelMixin): class Supermarket(models.Model, PermissionModelMixin):
@ -233,8 +246,9 @@ class Supermarket(models.Model, PermissionModelMixin):
return self.name return self.name
class Meta: class Meta:
# TODO according to this https://docs.djangoproject.com/en/3.1/ref/models/options/#unique-together should not be used constraints = [
unique_together = (('space', 'name'),) models.UniqueConstraint(fields=['space', 'name'], name='sm_unique_name_per_space')
]
class SupermarketCategoryRelation(models.Model, PermissionModelMixin): class SupermarketCategoryRelation(models.Model, PermissionModelMixin):
@ -265,7 +279,7 @@ class SyncLog(models.Model, PermissionModelMixin):
class Keyword(ExportModelOperationsMixin('keyword'), MP_Node, 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'] node_order_by = ['name']
name = models.CharField(max_length=64) name = models.CharField(max_length=64)
icon = models.CharField(max_length=16, blank=True, null=True) 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) updated_at = models.DateTimeField(auto_now=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE) 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 = ' > ' _full_name_separator = ' > '
@ -291,19 +305,6 @@ class Keyword(ExportModelOperationsMixin('keyword'), MP_Node, PermissionModelMix
return self.get_parent().id return self.get_parent().id
return None 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 @property
def full_name(self): def full_name(self):
""" """
@ -337,6 +338,7 @@ class Keyword(ExportModelOperationsMixin('keyword'), MP_Node, PermissionModelMix
def get_num_children(self): def get_num_children(self):
return self.get_children().count() return self.get_children().count()
# use self.objects.get_or_create() instead
@classmethod @classmethod
def add_root(self, **kwargs): def add_root(self, **kwargs):
with scopes_disabled(): with scopes_disabled():
@ -344,7 +346,7 @@ class Keyword(ExportModelOperationsMixin('keyword'), MP_Node, PermissionModelMix
class Meta: class Meta:
constraints = [ 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']),) indexes = (Index(fields=['id', 'name']),)
@ -360,8 +362,9 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
return self.name return self.name
class Meta: class Meta:
# TODO according to this https://docs.djangoproject.com/en/3.1/ref/models/options/#unique-together should not be used constraints = [
unique_together = (('space', 'name'),) models.UniqueConstraint(fields=['space', 'name'], name='u_unique_name_per_space')
]
class Food(ExportModelOperationsMixin('food'), models.Model, PermissionModelMixin): class Food(ExportModelOperationsMixin('food'), models.Model, PermissionModelMixin):
@ -378,8 +381,9 @@ class Food(ExportModelOperationsMixin('food'), models.Model, PermissionModelMixi
return self.name return self.name
class Meta: class Meta:
# TODO according to this https://docs.djangoproject.com/en/3.1/ref/models/options/#unique-together should not be used constraints = [
unique_together = (('space', 'name'),) models.UniqueConstraint(fields=['space', 'name'], name='f_unique_name_per_space')
]
indexes = (Index(fields=['id', 'name']),) indexes = (Index(fields=['id', 'name']),)
@ -407,10 +411,11 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
TEXT = 'TEXT' TEXT = 'TEXT'
TIME = 'TIME' TIME = 'TIME'
FILE = 'FILE' FILE = 'FILE'
RECIPE = 'RECIPE'
name = models.CharField(max_length=128, default='', blank=True) name = models.CharField(max_length=128, default='', blank=True)
type = models.CharField( type = models.CharField(
choices=((TEXT, _('Text')), (TIME, _('Time')), (FILE, _('File')),), choices=((TEXT, _('Text')), (TIME, _('Time')), (FILE, _('File')), (RECIPE, _('Recipe')),),
default=TEXT, default=TEXT,
max_length=16 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) file = models.ForeignKey('UserFile', on_delete=models.PROTECT, null=True, blank=True)
show_as_header = models.BooleanField(default=True) show_as_header = models.BooleanField(default=True)
search_vector = SearchVectorField(null=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) space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space') objects = ScopedManager(space='space')
@ -561,8 +567,9 @@ class RecipeBookEntry(ExportModelOperationsMixin('book_entry'), models.Model, Pe
return None return None
class Meta: class Meta:
# TODO according to this https://docs.djangoproject.com/en/3.1/ref/models/options/#unique-together should not be used constraints = [
unique_together = (('recipe', 'book'),) models.UniqueConstraint(fields=['recipe', 'book'], name='rbe_unique_name_per_space')
]
class MealType(models.Model, PermissionModelMixin): class MealType(models.Model, PermissionModelMixin):

View File

@ -1,6 +1,5 @@
import io import io
import os import os
import tempfile
from datetime import datetime from datetime import datetime
from os import listdir from os import listdir
from os.path import isfile, join 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 # TODO move to separate class to cleanup
class RecipeSchema(AutoSchema): class RecipeSchema(AutoSchema):
def get_path_parameters(self, path, method): def get_path_parameters(self, path, method):
if not is_list_view(path, method, self.view): if not is_list_view(path, method, self.view):
return super(RecipeSchema, self).get_path_parameters(path, method) 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.', "description": 'true or false. returns the results in randomized order.',
'schema': {'type': 'string', }, '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 return parameters
# TODO move to separate class to cleanup
class TreeSchema(AutoSchema): class TreeSchema(AutoSchema):
def get_path_parameters(self, path, method): def get_path_parameters(self, path, method):

View File

@ -47,7 +47,7 @@ class CustomDecimalField(serializers.Field):
class SpaceFilterSerializer(serializers.ListSerializer): class SpaceFilterSerializer(serializers.ListSerializer):
def to_representation(self, data): 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 # if query is sliced it came from api request not nested serializer
return super().to_representation(data) return super().to_representation(data)
if self.child.Meta.model == User: if self.child.Meta.model == User:
@ -209,29 +209,30 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
return str(obj) return str(obj)
def get_image(self, 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: 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: if len(recipes) != 0:
return random.choice(recipes).image.url return random.choice(recipes).image.url
else: else:
return None return None
def count_recipes(self, obj): 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): def create(self, validated_data):
# since multi select tags dont have id's # since multi select tags dont have id's
# duplicate names might be routed to create # duplicate names might be routed to create
validated_data['name'] = validated_data['name'].strip()
validated_data['space'] = self.context['request'].space 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 return obj
class Meta: class Meta:
# list_serializer_class = SpaceFilterSerializer # list_serializer_class = SpaceFilterSerializer
model = Keyword model = Keyword
fields = ('id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at', 'updated_at') 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): class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
@ -264,7 +265,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial
fields = ('id', 'name') fields = ('id', 'name')
class SupermarketCategoryRelationSerializer(SpacedModelSerializer): class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
category = SupermarketCategorySerializer() category = SupermarketCategorySerializer()
class Meta: class Meta:
@ -284,7 +285,9 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False) supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
def create(self, validated_data): 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 return obj
def update(self, instance, validated_data): def update(self, instance, validated_data):
@ -318,6 +321,7 @@ class StepSerializer(WritableNestedModelSerializer):
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown') ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
ingredients_vue = serializers.SerializerMethodField('get_ingredients_vue') ingredients_vue = serializers.SerializerMethodField('get_ingredients_vue')
file = UserFileViewSerializer(allow_null=True, required=False) file = UserFileViewSerializer(allow_null=True, required=False)
step_recipe_data = serializers.SerializerMethodField('get_step_recipe_data')
def create(self, validated_data): def create(self, validated_data):
validated_data['space'] = self.context['request'].space validated_data['space'] = self.context['request'].space
@ -329,11 +333,27 @@ class StepSerializer(WritableNestedModelSerializer):
def get_ingredients_markdown(self, obj): def get_ingredients_markdown(self, obj):
return obj.get_instruction_render() 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: class Meta:
model = Step model = Step
fields = ( fields = (
'id', 'name', 'type', 'instruction', 'ingredients', 'ingredients_markdown', '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): def create(self, validated_data):
book = validated_data['book'] book = validated_data['book']
recipe = validated_data['recipe']
if not book.get_owner() == self.context['request'].user: if not book.get_owner() == self.context['request'].user:
raise NotFound(detail=None, code=None) 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: class Meta:
model = RecipeBookEntry 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 <a class="dropdown-item" href="{% url 'list_food' %}"><i
class="fas fa-leaf fa-fw"></i> {% trans 'Ingredients' %} class="fas fa-leaf fa-fw"></i> {% trans 'Ingredients' %}
</a> </a>
<a class="dropdown-item" href="{% url 'view_supermarket' %}"><i
class="fas fa-store-alt fa-fw"></i> {% trans 'Supermarket' %}
</a>
</div> </div>
</li> </li>
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'list_keyword,data_batch_edit' %}active{% endif %}"> <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="TEXT">{% trans 'Text' %}</option>
<option value="TIME">{% trans 'Time' %}</option> <option value="TIME">{% trans 'Time' %}</option>
<option value="FILE">{% trans 'File' %}</option> <option value="FILE">{% trans 'File' %}</option>
<option value="RECIPE">{% trans 'Recipe' %}</option>
</select> </select>
</div> </div>
</div> </div>
@ -214,7 +215,7 @@
:id="'id_step_' + step.id + '_time'"> :id="'id_step_' + step.id + '_time'">
</div> </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> <label :for="'id_step_' + step.id + '_file'">{% trans 'File' %}</label>
<multiselect <multiselect
v-tabindex v-tabindex
@ -235,6 +236,28 @@
@search-change="searchFiles"> @search-change="searchFiles">
</multiselect> </multiselect>
</div> </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> </div>
<template v-if="step.type == 'TEXT'"> <template v-if="step.type == 'TEXT'">
@ -524,6 +547,8 @@
units_loading: false, units_loading: false,
files: [], files: [],
files_loading: false, files_loading: false,
recipes: [],
recipes_loading: false,
message: '', message: '',
}, },
directives: { directives: {
@ -550,6 +575,7 @@
this.searchFoods('') this.searchFoods('')
this.searchKeywords('') this.searchKeywords('')
this.searchFiles('') this.searchFiles('')
this.searchRecipes('')
this._keyListener = function (e) { this._keyListener = function (e) {
if (e.code === "Space" && e.ctrlKey) { 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') 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) { searchUnits: function (query) {
this.units_loading = true this.units_loading = true
this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => { this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => {

View File

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

View File

@ -32,7 +32,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{% trans 'Join an existing space.' %}</h5> <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' %}"> <form method="POST" action="{% url 'view_no_space' %}">
{% csrf_token %} {% csrf_token %}
@ -49,7 +49,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{% trans 'Create your own recipe space.' %}</h5> <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' %}"> <form method="POST" action="{% url 'view_no_space' %}">
{% csrf_token %} {% csrf_token %}
{{ create_form | crispy }} {{ 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.models import Keyword
from cookbook.tests.conftest import get_random_recipe 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' LIST_URL = 'api:keyword-list'
DETAIL_URL = 'api:keyword-detail' DETAIL_URL = 'api:keyword-detail'
MOVE_URL = 'api:keyword-move' MOVE_URL = 'api:keyword-move'
@ -16,7 +25,7 @@ MERGE_URL = 'api:keyword-merge'
# TODO are there better ways to manage these fixtures? # TODO are there better ways to manage these fixtures?
@pytest.fixture() @pytest.fixture()
def obj_1(space_1): 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() @pytest.fixture()
@ -31,12 +40,12 @@ def obj_1_1_1(obj_1_1, space_1):
@pytest.fixture @pytest.fixture
def obj_2(space_1): 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() @pytest.fixture()
def obj_3(space_2): 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() @pytest.fixture()
@ -158,7 +167,6 @@ def test_add(arg, request, u1_s2):
assert r.status_code == 404 assert r.status_code == 404
@pytest.mark.django_db(transaction=True)
def test_add_duplicate(u1_s1, u1_s2, obj_1, obj_3): 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_s1.get(reverse(LIST_URL)).content)['count'] == 1
assert json.loads(u1_s2.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( r = u1_s1.put(
reverse(MOVE_URL, args=[obj_1.id, 9999]) reverse(MOVE_URL, args=[obj_1.id, 9999])
) )
assert r.status_code == 400 assert r.status_code == 404
# attempt to move to wrong space # attempt to move to wrong space
r = u1_s1.put( r = u1_s1.put(
reverse(MOVE_URL, args=[obj_1_1.id, obj_3.id]) 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 # run diagnostic to find problems - none should be found
with scopes_disabled(): with scopes_disabled():
@ -318,13 +326,13 @@ def test_merge(
r = u1_s1.put( r = u1_s1.put(
reverse(MERGE_URL, args=[obj_1_1.id, 9999]) 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 # attempt to move to wrong space
r = u1_s1.put( r = u1_s1.put(
reverse(MERGE_URL, args=[obj_2.id, obj_3.id]) 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 # attempt to merge with child
r = u1_s1.put( r = u1_s1.put(

View File

@ -4,12 +4,16 @@ import pytest
from django.urls import reverse from django.urls import reverse
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from cookbook.models import Food, Ingredient, Step, Recipe from cookbook.models import Recipe
LIST_URL = 'api:recipe-list' LIST_URL = 'api:recipe-list'
DETAIL_URL = 'api:recipe-detail' 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", [ @pytest.mark.parametrize("arg", [
['a_u', 403], ['a_u', 403],
['g1_s1', 200], ['g1_s1', 200],

View File

@ -11,7 +11,7 @@ LIST_URL = 'api:recipebookentry-list'
DETAIL_URL = 'api:recipebookentry-detail' DETAIL_URL = 'api:recipebookentry-detail'
@pytest.fixture() @pytest.fixture
def obj_1(space_1, u1_s1, recipe_1_s1): 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) 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}, {'book': obj_1.book.pk, 'recipe': obj_1.recipe.pk},
content_type='application/json' content_type='application/json'
) )
assert r.status_code == 400 assert r.status_code == 201
def test_delete(u1_s1, u1_s2, obj_1): 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.contrib.auth.models import User, Group
from django_scopes import scopes_disabled 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 # 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'recipe-book-entry', api.RecipeBookEntryViewSet)
router.register(r'supermarket', api.SupermarketViewSet) router.register(r'supermarket', api.SupermarketViewSet)
router.register(r'supermarket-category', api.SupermarketCategoryViewSet) 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'import-log', api.ImportLogViewSet)
router.register(r'bookmarklet-import', api.BookmarkletImportViewSet) router.register(r'bookmarklet-import', api.BookmarkletImportViewSet)
router.register(r'user-file', api.UserFileViewSet) router.register(r'user-file', api.UserFileViewSet)

View File

@ -4,9 +4,9 @@ import re
import uuid import uuid
import requests import requests
from PIL import Image
from annoying.decorators import ajax_request from annoying.decorators import ajax_request
from annoying.functions import get_object_or_None from annoying.functions import get_object_or_None
from collections import OrderedDict
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.postgres.search import TrigramSimilarity from django.contrib.postgres.search import TrigramSimilarity
@ -37,13 +37,13 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
group_required) group_required)
from cookbook.helper.recipe_html_import import get_recipe_from_source from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_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.helper.recipe_url_import import get_from_scraper
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan, from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
MealType, Recipe, RecipeBook, ShoppingList, MealType, Recipe, RecipeBook, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step, ShoppingListEntry, ShoppingListRecipe, Step,
Storage, Sync, SyncLog, Unit, UserPreference, 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.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@ -61,7 +61,7 @@ from cookbook.serializer import (FoodSerializer, IngredientSerializer,
UserNameSerializer, UserPreferenceSerializer, UserNameSerializer, UserPreferenceSerializer,
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer, ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer,
RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer, RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer,
BookmarkletImportSerializer, SupermarketCategorySerializer, UserFileSerializer) BookmarkletImportSerializer, SupermarketCategorySerializer, UserFileSerializer, SupermarketCategoryRelationSerializer)
class StandardFilterMixin(ViewSetMixin): class StandardFilterMixin(ViewSetMixin):
@ -144,18 +144,18 @@ class TreeMixin(FuzzyFilterMixin):
except self.model.DoesNotExist: except self.model.DoesNotExist:
self.queryset = self.model.objects.none() self.queryset = self.model.objects.none()
if root == 0: 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: 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: elif tree:
if tree.isnumeric(): if tree.isnumeric():
try: 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: except Keyword.DoesNotExist:
self.queryset = self.model.objects.none() self.queryset = self.model.objects.none()
else: else:
return super().get_queryset() 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.action(detail=True, url_path='move/(?P<parent>[^/.]+)', methods=['PUT'],)
@decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer)) @decorators.renderer_classes((TemplateHTMLRenderer, JSONRenderer))
@ -166,7 +166,7 @@ class TreeMixin(FuzzyFilterMixin):
child = self.model.objects.get(pk=pk, space=self.request.space) child = self.model.objects.get(pk=pk, space=self.request.space)
except (self.model.DoesNotExist): except (self.model.DoesNotExist):
content = {'error': True, 'msg': _(f'No {self.basename} with id {child} exists')} content = {'error': True, 'msg': _(f'No {self.basename} with id {child} exists')}
return Response(content, status=status.HTTP_400_BAD_REQUEST) return Response(content, status=status.HTTP_404_NOT_FOUND)
parent = int(parent) parent = int(parent)
# parent 0 is root of the tree # 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) parent = self.model.objects.get(pk=parent, space=self.request.space)
except (self.model.DoesNotExist): except (self.model.DoesNotExist):
content = {'error': True, 'msg': _(f'No {self.basename} with id {parent} exists')} 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: try:
with scopes_disabled(): with scopes_disabled():
@ -204,7 +204,7 @@ class TreeMixin(FuzzyFilterMixin):
source = self.model.objects.get(pk=pk, space=self.request.space) source = self.model.objects.get(pk=pk, space=self.request.space)
except (self.model.DoesNotExist): except (self.model.DoesNotExist):
content = {'error': True, 'msg': _(f'No {self.basename} with id {pk} exists')} 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: if int(target) == source.id:
content = {'error': True, 'msg': _('Cannot merge with the same object!')} 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) target = self.model.objects.get(pk=target, space=self.request.space)
except (self.model.DoesNotExist): except (self.model.DoesNotExist):
content = {'error': True, 'msg': _(f'No {self.basename} with id {target} exists')} 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: try:
if target in source.get_descendants_and_self(): if target in source.get_descendants_and_self():
content = {'error': True, 'msg': _('Cannot merge with child object!')} content = {'error': True, 'msg': _('Cannot merge with child object!')}
return Response(content, status=status.HTTP_403_FORBIDDEN) 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) 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() return super().get_queryset()
class KeywordViewSet(viewsets.ModelViewSet, TreeMixin): class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMixin):
# TODO check if fuzzyfilter is conflicting - may also need to create 'tree filter' mixin 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 queryset = Keyword.objects
model = Keyword model = Keyword
serializer_class = KeywordSerializer serializer_class = KeywordSerializer
@ -438,6 +447,19 @@ class RecipePagination(PageNumberPagination):
page_size_query_param = 'page_size' page_size_query_param = 'page_size'
max_page_size = 100 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): class RecipeViewSet(viewsets.ModelViewSet):
queryset = Recipe.objects queryset = Recipe.objects
@ -679,7 +701,7 @@ def share_link(request, pk):
def log_cooking(request, recipe_id): def log_cooking(request, recipe_id):
recipe = get_object_or_None(Recipe, id=recipe_id) recipe = get_object_or_None(Recipe, id=recipe_id)
if recipe: 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 servings = request.GET['s'] if 's' in request.GET else None
if servings and re.match(r'^([1-9])+$', servings): if servings and re.match(r'^([1-9])+$', servings):
log.servings = int(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 gettext as _
from django.utils.translation import ngettext from django.utils.translation import ngettext
from django_tables2 import RequestConfig from django_tables2 import RequestConfig
from PIL import Image, UnidentifiedImageError from PIL import UnidentifiedImageError
from requests.exceptions import MissingSchema from requests.exceptions import MissingSchema
from cookbook.forms import BatchEditForm, SyncForm from cookbook.forms import BatchEditForm, SyncForm
@ -150,12 +150,8 @@ def import_url(request):
all_keywords = Keyword.get_tree() all_keywords = Keyword.get_tree()
for kw in data['keywords']: for kw in data['keywords']:
q = all_keywords.filter(name=kw['text'], space=request.space) k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space)
if len(q) != 0: recipe.keywords.add(k)
recipe.keywords.add(q[0])
elif data['all_keywords']:
k = Keyword.add_root(name=kw['text'], space=request.space)
recipe.keywords.add(k)
for ing in data['recipeIngredient']: for ing in data['recipeIngredient']:
ingredient = Ingredient(space=request.space,) ingredient = Ingredient(space=request.space,)

View File

@ -63,7 +63,7 @@ def get_integration(request, export_type):
@group_required('user') @group_required('user')
def import_recipe(request): def import_recipe(request):
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.')) messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('index'))

View File

@ -3,12 +3,12 @@ import json
import requests import requests
from django.db.models import Q from django.db.models import Q
from django.http import JsonResponse 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 django.views.decorators.csrf import csrf_exempt
from cookbook.helper.ingredient_parser import parse, get_unit, get_food from cookbook.helper.ingredient_parser import parse, get_unit, get_food
from cookbook.helper.permission_helper import group_required 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') @group_required('user')
@ -58,7 +58,7 @@ def hook(request, token):
) )
) )
return JsonResponse({'data': data['message']['text']}) return JsonResponse({'data': data['message']['text']})
except: except Exception:
pass pass
return JsonResponse({}) return JsonResponse({})

View File

@ -15,6 +15,8 @@ from django.db.models import Avg, Q
from django.db.models import Sum from django.db.models import Sum
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.http import JsonResponse 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.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
@ -27,6 +29,8 @@ from cookbook.filters import RecipeFilter
from cookbook.forms import (CommentForm, Recipe, User, from cookbook.forms import (CommentForm, Recipe, User,
UserCreateForm, UserNameForm, UserPreference, UserCreateForm, UserNameForm, UserPreference,
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm, SearchPreferenceForm) UserPreferenceForm, SpaceJoinForm, SpaceCreateForm, SearchPreferenceForm)
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm,
SearchPreferenceForm)
from cookbook.helper.ingredient_parser import parse from cookbook.helper.ingredient_parser import parse
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan, from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
@ -122,6 +126,7 @@ def no_space(request):
max_users=settings.SPACE_DEFAULT_MAX_USERS, max_users=settings.SPACE_DEFAULT_MAX_USERS,
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING, allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
) )
request.user.userpreference.space = created_space request.user.userpreference.space = created_space
request.user.userpreference.save() request.user.userpreference.save()
request.user.groups.add(Group.objects.filter(name='admin').get()) request.user.groups.add(Group.objects.filter(name='admin').get())
@ -141,7 +146,7 @@ def no_space(request):
if 'signup_token' in request.session: if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')])) 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() join_form = SpaceJoinForm()
return render(request, 'no_space_info.html', {'create_form': create_form, 'join_form': join_form}) 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 ## 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` 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` Move it to the `/var/www` directory: `mv recipes /var/www`
Install project requirements: `pip3.9 install -r requirements.txt`
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 ## Setup postgresql
Run `sudo -u postgres psql` `sudo -u postgres psql`
In the psql console: In the psql console:
@ -37,12 +53,18 @@ ALTER USER djangouser WITH SUPERUSER;
Download the `.env` configuration file and **edit it accordingly**. Download the `.env` configuration file and **edit it accordingly**.
```shell ```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 ## 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` Execute `/python3.9 manage.py migrate`
@ -67,10 +89,11 @@ After=network.target
Type=simple Type=simple
Restart=always Restart=always
RestartSec=3 RestartSec=3
User=recipes
Group=www-data Group=www-data
WorkingDirectory=/media/data/recipes WorkingDirectory=/var/www/recipes
EnvironmentFile=/media/data/recipes/.env EnvironmentFile=/var/www/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 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] [Install]
WantedBy=multi-user.target 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 *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 ### 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: And enter these lines:
@ -95,20 +118,21 @@ server {
#error_log /var/log/nginx/error.log; #error_log /var/log/nginx/error.log;
# serve media files # serve media files
location /static { location /staticfiles {
alias /media/data/recipes/staticfiles; alias /var/www/recipes/staticfiles;
} }
location /media { location /mediafiles {
alias /media/data/recipes/mediafiles; alias /var/www/recipes/mediafiles;
} }
location / { 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. *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; listen 80;
server_name localhost; server_name localhost;
client_max_body_size 16M; client_max_body_size 128M;
# serve media files # serve media files
location /media/ { location /media/ {

View File

@ -15,18 +15,15 @@ import os
import re import re
from django.contrib import messages from django.contrib import messages
from django.contrib.staticfiles.storage import staticfiles_storage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from dotenv import load_dotenv # from dotenv import load_dotenv
from webpack_loader.loader import WebpackLoader # load_dotenv()
load_dotenv()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Get vars from .env files # Get vars from .env files
SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv('SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV' 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))) 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_ACCESS = bool(int(os.getenv('SOCIAL_DEFAULT_ACCESS', False)))
SOCIAL_DEFAULT_GROUP = os.getenv('SOCIAL_DEFAULT_GROUP', 'guest') 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 = { CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', '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 cryptography==3.4.7
django-annoying==0.10.6 django-annoying==0.10.6
django-autocomplete-light==3.8.2 django-autocomplete-light==3.8.2
@ -9,19 +9,19 @@ django-filter==2.4.0
django-tables2==2.4.0 django-tables2==2.4.0
djangorestframework==3.12.4 djangorestframework==3.12.4
drf-writable-nested==0.6.3 drf-writable-nested==0.6.3
bleach==3.3.0 bleach==3.3.1
bleach-allowlist==1.0.3 bleach-allowlist==1.0.3
gunicorn==20.1.0 gunicorn==20.1.0
lxml==4.6.3 lxml==4.6.3
Markdown==3.3.4 Markdown==3.3.4
Pillow==8.2.0 Pillow==8.3.1
psycopg2-binary==2.9.1 psycopg2-binary==2.9.1
python-dotenv==0.18.0 python-dotenv==0.18.0
requests==2.25.1 requests==2.26.0
simplejson==3.17.2 simplejson==3.17.3
six==1.16.0 six==1.16.0
webdavclient3==3.14.5 webdavclient3==3.14.5
whitenoise==5.2.0 whitenoise==5.3.0
icalendar==4.0.7 icalendar==4.0.7
pyyaml==5.4.1 pyyaml==5.4.1
uritemplate==3.0.1 uritemplate==3.0.1
@ -30,14 +30,14 @@ microdata==0.7.1
Jinja2==3.0.1 Jinja2==3.0.1
django-webpack-loader==1.1.0 django-webpack-loader==1.1.0
django-js-reverse==0.9.1 django-js-reverse==0.9.1
django-allauth==0.44.0 django-allauth==0.45.0
recipe-scrapers==13.3.0 recipe-scrapers==13.3.4
django-scopes==1.2.0 django-scopes==1.2.0
pytest==6.2.4 pytest==6.2.4
pytest-django==4.4.0 pytest-django==4.4.0
django-cors-headers==3.7.0 django-cors-headers==3.7.0
django-treebeard==4.5.1 django-treebeard==4.5.1
django-storages==1.11.1 django-storages==1.11.1
boto3==1.17.102 boto3==1.18.4
django-prometheus==2.1.0 django-prometheus==2.1.0
django-hCaptcha==0.1.0 django-hCaptcha==0.1.0

View File

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

View File

@ -6,7 +6,7 @@
</div> </div>
<div class="col-xl-8 col-12"> <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" :class="{'vh-100' : show_split}">
<!-- <div class="container-fluid d-flex flex-column flex-grow-1 vh-100"> --> <!-- <div class="container-fluid d-flex flex-column flex-grow-1 vh-100"> -->
<!-- expanded options box --> <!-- expanded options box -->
@ -450,10 +450,11 @@ export default {
let parent = {} let parent = {}
let pageSize = 200 let pageSize = 200
let keyword = String(kw.id) let keyword = String(kw.id)
console.log(apiClient.listRecipes)
apiClient.listRecipes( apiClient.listRecipes(
undefined, keyword, undefined, undefined, undefined, undefined, undefined, keyword, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, pageSize undefined, undefined, undefined, undefined, undefined, pageSize, undefined
).then(result => { ).then(result => {
if (col == 'left') { if (col == 'left') {
parent = this.findKeyword(this.keywords, kw.id) 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" <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> v-bind:placeholder="$t('Search')"></b-input>
<b-input-group-append> <b-input-group-append>
<b-button v-b-toggle.collapse_advanced_search <b-button variant="light"
v-bind:class="{'btn-primary': !isAdvancedSettingsSet(), 'btn-danger': isAdvancedSettingsSet()}" v-b-tooltip.hover :title="$t('Random Recipes')"
class="shadow-none btn"><i @click="openRandom()">
class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i><i <i class="fas fa-dice-five" style="font-size: 1.5em"></i>
class="fas fa-caret-up"
v-if="settings.advanced_search_visible"></i>
</b-button> </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-append>
</b-input-group> </b-input-group>
</div> </div>
@ -73,6 +80,21 @@
></b-form-input> ></b-form-input>
</b-form-group> </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 <b-form-group
v-bind:label="$t('Meal_Plan')" v-bind:label="$t('Meal_Plan')"
label-for="popover-input-2" label-for="popover-input-2"
@ -85,6 +107,24 @@
size="sm" size="sm"
></b-form-checkbox> ></b-form-checkbox>
</b-form-group> </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>
<div class="row" style="margin-top: 1vh"> <div class="row" style="margin-top: 1vh">
<div class="col-12"> <div class="col-12">
@ -103,12 +143,16 @@
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2"> <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" :initial_selection="settings.search_keywords"
search_function="listKeywords" label="label" search_function="listKeywords" label="label"
:tree_api="true" :tree_api="true"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0" 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-append>
<b-input-group-text> <b-input-group-text>
<b-form-checkbox v-model="settings.search_keywords_or" name="check-button" <b-form-checkbox v-model="settings.search_keywords_or" name="check-button"
@ -179,7 +223,7 @@
<div class="row"> <div class="row">
<div class="col col-md-12 text-right" style="margin-top: 2vh"> <div class="col col-md-12 text-right" style="margin-top: 2vh">
<span class="text-muted"> <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> class="fas fa-times-circle"></i> {{ $t('Reset') }}</a>
</span> </span>
</div> </div>
@ -187,10 +231,10 @@
<div class="row"> <div class="row">
<div class="col col-md-12"> <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 <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" <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" :meal_plan="m" :footer_text="m.meal_type_name"
footer_icon="far fa-calendar-alt"></recipe-card> footer_icon="far fa-calendar-alt"></recipe-card>
@ -204,12 +248,12 @@
</div> </div>
</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"> <div class="col col-md-12">
<b-pagination pills <b-pagination pills
v-model="settings.pagination_page" v-model="settings.pagination_page"
:total-rows="pagination_count" :total-rows="pagination_count"
per-page="25" :per-page="settings.page_count"
@change="pageChange" @change="pageChange"
align="center"> align="center">
@ -223,8 +267,6 @@
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
@ -246,6 +288,8 @@ import LoadingSpinner from "@/components/LoadingSpinner";
import {ApiApiFactory} from "@/utils/openapi/api.ts"; import {ApiApiFactory} from "@/utils/openapi/api.ts";
import RecipeCard from "@/components/RecipeCard"; import RecipeCard from "@/components/RecipeCard";
import GenericMultiselect from "@/components/GenericMultiselect"; import GenericMultiselect from "@/components/GenericMultiselect";
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
@ -254,10 +298,11 @@ let SETTINGS_COOKIE_NAME = 'search_settings'
export default { export default {
name: 'RecipeSearchView', name: 'RecipeSearchView',
mixins: [ResolveUrlMixin], mixins: [ResolveUrlMixin],
components: {GenericMultiselect, RecipeCard}, components: {GenericMultiselect, RecipeCard, Treeselect},
data() { data() {
return { return {
recipes: [], recipes: [],
facets: [],
meal_plans: [], meal_plans: [],
last_viewed_recipes: [], last_viewed_recipes: [],
@ -275,11 +320,13 @@ export default {
advanced_search_visible: false, advanced_search_visible: false,
show_meal_plan: true, show_meal_plan: true,
recently_viewed: 5, recently_viewed: 5,
sort_by_new: true,
pagination_page: 1, pagination_page: 1,
page_count: 25,
}, },
pagination_count: 0, pagination_count: 0,
random_search: false,
} }
}, },
@ -339,15 +386,18 @@ export default {
'settings.search_input': _debounce(function () { 'settings.search_input': _debounce(function () {
this.refreshData(false) this.refreshData(false)
}, 300), }, 300),
'settings.page_count': _debounce(function () {
this.refreshData(false)
}, 300),
}, },
methods: { methods: {
refreshData: function (page_load) { refreshData: function (random) {
this.random_search = random
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
apiClient.listRecipes( apiClient.listRecipes(
this.settings.search_input, this.settings.search_input,
this.settings.search_keywords.map(function (A) { this.settings.search_keywords,
return A["id"];
}),
this.settings.search_foods.map(function (A) { this.settings.search_foods.map(function (A) {
return A["id"]; return A["id"];
}), }),
@ -359,14 +409,20 @@ export default {
this.settings.search_books_or, this.settings.search_books_or,
this.settings.search_internal, this.settings.search_internal,
undefined, random,
this.settings.sort_by_new,
this.settings.pagination_page, this.settings.pagination_page,
this.settings.page_count
).then(result => { ).then(result => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.pagination_count = result.data.count this.pagination_count = result.data.count
this.recipes = result.data.results this.recipes = result.data.results
this.facets = result.data.facets
}) })
}, },
openRandom: function () {
this.refreshData(true)
},
loadMealPlan: function () { loadMealPlan: function () {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
@ -389,7 +445,7 @@ export default {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
if (this.settings.recently_viewed > 0) { if (this.settings.recently_viewed > 0) {
apiClient.listRecipes(undefined, undefined, undefined, undefined, undefined, undefined, 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 this.last_viewed_recipes = result.data.results
}) })
} else { } else {
@ -411,10 +467,18 @@ export default {
}, },
pageChange: function (page) { pageChange: function (page) {
this.settings.pagination_page = page this.settings.pagination_page = page
this.refreshData() this.refreshData(false)
}, },
isAdvancedSettingsSet() { isAdvancedSettingsSet() {
return ((this.settings.search_keywords.length + this.settings.search_foods.length + this.settings.search_books.length) > 0) 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> <h2>{{ $t('Supermarket') }}</h2>
<multiselect v-model="selected_supermarket" track-by="id" label="name" <multiselect v-model="selected_supermarket" track-by="id" label="name"
:options="supermarkets"> :options="supermarkets" @input="selectedSupermarketChanged">
</multiselect> </multiselect>
<b-button class="btn btn-primary btn-block" style="margin-top: 1vh" v-b-modal.modal-supermarket> <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="row">
<div class="col col-md-6"> <div class="col col-md-6">
<h4>{{ $t('Categories') }} <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> </h4>
<draggable :list="categories" group="supermarket_categories" <draggable :list="selectable_categories" group="supermarket_categories"
:empty-insert-threshold="10"> :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> <button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
</div> </div>
@ -41,9 +43,9 @@
<div class="col col-md-6"> <div class="col col-md-6">
<h4>{{ $t('Selected') }} {{ $t('Categories') }}</h4> <h4>{{ $t('Selected') }} {{ $t('Categories') }}</h4>
<draggable :list="selected_categories" group="supermarket_categories" <draggable :list="supermarket_categories" group="supermarket_categories"
:empty-insert-threshold="10"> :empty-insert-threshold="10" @change="selectedCategoriesChanged">
<div v-for="c in selected_categories" :key="c.id"> <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> <button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
</div> </div>
@ -61,9 +63,9 @@
</b-modal> </b-modal>
<b-modal id="modal-category" v-bind:title="$t('Category')" @ok="categoryModalOk()"> <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') }} {{ $t('Name') }}
<b-input v-model="selected_supermarket.name"></b-input> <b-input v-model="selected_category.name"></b-input>
</label> </label>
</b-modal> </b-modal>
@ -107,12 +109,15 @@ export default {
categories: [], categories: [],
selected_supermarket: {}, selected_supermarket: {},
selected_categories: [], selected_category: {},
selectable_categories: [],
supermarket_categories: [],
} }
}, },
mounted() { mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE this.$i18n.locale = window.CUSTOM_LOCALE
console.log('LOADED')
this.loadInitial() this.loadInitial()
}, },
methods: { methods: {
@ -123,8 +128,45 @@ export default {
}) })
apiClient.listSupermarketCategorys().then(results => { apiClient.listSupermarketCategorys().then(results => {
this.categories = results.data 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 () { supermarketModalOk: function () {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
if (this.selected_supermarket.new) { if (this.selected_supermarket.new) {
@ -139,14 +181,13 @@ export default {
}, },
categoryModalOk: function () { categoryModalOk: function () {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
if (this.selected_supermarket.new) { if (this.selected_category.new) {
apiClient.createSupermarket({name: this.selected_supermarket.name}).then(results => { apiClient.createSupermarketCategory({name: this.selected_category.name}).then(results => {
this.selected_supermarket = undefined this.selected_category = {}
this.loadInitial() this.loadInitial()
}) })
} else { } 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> <div row>
<b-card no-body d-flex flex-column :class="{'border border-primary' : over, 'shake': isError}" <b-card no-body d-flex flex-column :class="{'border border-primary' : over, 'shake': isError}"
refs="keywordCard" refs="keywordCard"
style="height: 10vh;" :style="{'cursor:grab' : draggle}" style="height: 10vh;" :style="{'cursor:grab' : draggable}"
@dragover.prevent @dragover.prevent
@dragenter.prevent @dragenter.prevent
:draggable="draggable" :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> <template>
<div> <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="row" v-if="recipe.steps.length > 1">
<div class="col col-md-8"> <div class="col col-md-8">
<h5 class="text-primary"> <h5 class="text-primary">
@ -22,15 +22,21 @@
</div> </div>
<div class="col col-md-4" style="text-align: right"> <div class="col col-md-4" style="text-align: right">
<b-button @click="details_visible = !details_visible" style="border: none; background: none" <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> <i class="far fa-check-circle"></i>
</b-button> </b-button>
</div> </div>
</div> </div>
</template>
<template v-if="step.type === 'TEXT'">
<b-collapse id="collapse-1" v-model="details_visible"> <b-collapse id="collapse-1" v-model="details_visible">
<div class="row"> <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"> <table class="table table-sm">
<!-- eslint-disable vue/no-v-for-template-key-on-child --> <!-- eslint-disable vue/no-v-for-template-key-on-child -->
<template v-for="i in step.ingredients"> <template v-for="i in step.ingredients">
@ -65,7 +71,8 @@
<div class="col-md-2" style="text-align: right"> <div class="col-md-2" style="text-align: right">
<b-button @click="details_visible = !details_visible" style="border: none; background: none" <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> <i class="far fa-check-circle"></i>
</b-button> </b-button>
</div> </div>
@ -89,13 +96,31 @@
<img :src="step.file.file" style="max-width: 50vw; max-height: 50vh"> <img :src="step.file.file" style="max-width: 50vw; max-height: 50vh">
</div> </div>
<div v-else> <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> </div>
</template> </template>
</div> </div>
</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 !== ''"> <div v-if="start_time !== ''">
<b-popover <b-popover
:target="`id_reactive_popover_${step.id}`" :target="`id_reactive_popover_${step.id}`"
@ -139,6 +164,8 @@ import {GettextMixin} from "@/utils/utils";
import CompileComponent from "@/components/CompileComponent"; import CompileComponent from "@/components/CompileComponent";
import Vue from "vue"; import Vue from "vue";
import moment from "moment"; import moment from "moment";
import Keywords from "@/components/Keywords";
import {ResolveUrlMixin} from "@/utils/utils";
Vue.prototype.moment = moment Vue.prototype.moment = moment
@ -146,6 +173,7 @@ export default {
name: 'Step', name: 'Step',
mixins: [ mixins: [
GettextMixin, GettextMixin,
ResolveUrlMixin,
], ],
components: { components: {
Ingredient, Ingredient,
@ -157,6 +185,10 @@ export default {
index: Number, index: Number,
recipe: Object, recipe: Object,
start_time: String, start_time: String,
force_ingredients: {
type: Boolean,
default: false
}
}, },
data() { data() {
return { return {

View File

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

View File

@ -53,5 +53,26 @@
"Supermarket": "Supermarkt", "Supermarket": "Supermarkt",
"Categories": "Categorieën", "Categories": "Categorieën",
"Category": "Categorie", "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 * @memberof RecipeSteps
*/ */
file?: StepFile | null; 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 { export enum RecipeStepsTypeEnum {
Text = 'TEXT', Text = 'TEXT',
Time = 'TIME', Time = 'TIME',
File = 'FILE' File = 'FILE',
Recipe = 'RECIPE'
} }
/** /**
@ -1593,6 +1606,18 @@ export interface Step {
* @memberof Step * @memberof Step
*/ */
file?: StepFile | null; 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 { export enum StepTypeEnum {
Text = 'TEXT', Text = 'TEXT',
Time = 'TIME', Time = 'TIME',
File = 'FILE' File = 'FILE',
Recipe = 'RECIPE'
} }
/** /**
@ -1851,6 +1877,37 @@ export interface SupermarketCategory {
*/ */
name: string; 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 * @export
@ -2756,6 +2813,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions, 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] * @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); setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.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} [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} [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} [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} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page. * @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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/`; const localVarPath = `/api/recipe/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -4124,6 +4248,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
localVarQueryParameter['random'] = random; localVarQueryParameter['random'] = random;
} }
if (_new !== undefined) {
localVarQueryParameter['new'] = _new;
}
if (page !== undefined) { if (page !== undefined) {
localVarQueryParameter['page'] = page; 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); setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -5297,6 +5454,43 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions, 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. * @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); setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -6995,6 +7222,43 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions, 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. * @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); const localVarAxiosArgs = await localVarAxiosParamCreator.createSupermarketCategory(supermarketCategory, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); 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] * @param {Sync} [sync]
@ -7625,6 +7899,16 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.destroySupermarketCategory(id, options); const localVarAxiosArgs = await localVarAxiosParamCreator.destroySupermarketCategory(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); 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. * @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} [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} [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} [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} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page. * @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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>> { 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, page, pageSize, options); 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); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@ -7846,6 +8131,15 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.listStorages(options); const localVarAxiosArgs = await localVarAxiosParamCreator.listStorages(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); 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. * @param {*} [options] Override http request option.
@ -8149,6 +8443,17 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateSupermarketCategory(id, supermarketCategory, options); const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateSupermarketCategory(id, supermarketCategory, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); 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. * @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); const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveSupermarketCategory(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); 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. * @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); const localVarAxiosArgs = await localVarAxiosParamCreator.updateSupermarketCategory(id, supermarketCategory, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); 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. * @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> { createSupermarketCategory(supermarketCategory?: SupermarketCategory, options?: any): AxiosPromise<SupermarketCategory> {
return localVarFp.createSupermarketCategory(supermarketCategory, options).then((request) => request(axios, basePath)); 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] * @param {Sync} [sync]
@ -9095,6 +9430,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
destroySupermarketCategory(id: string, options?: any): AxiosPromise<void> { destroySupermarketCategory(id: string, options?: any): AxiosPromise<void> {
return localVarFp.destroySupermarketCategory(id, options).then((request) => request(axios, basePath)); 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. * @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} [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} [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} [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} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page. * @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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> { 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, page, pageSize, options).then((request) => request(axios, basePath)); 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>> { listStorages(options?: any): AxiosPromise<Array<Storage>> {
return localVarFp.listStorages(options).then((request) => request(axios, basePath)); 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. * @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> { partialUpdateSupermarketCategory(id: string, supermarketCategory?: SupermarketCategory, options?: any): AxiosPromise<SupermarketCategory> {
return localVarFp.partialUpdateSupermarketCategory(id, supermarketCategory, options).then((request) => request(axios, basePath)); 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. * @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> { retrieveSupermarketCategory(id: string, options?: any): AxiosPromise<SupermarketCategory> {
return localVarFp.retrieveSupermarketCategory(id, options).then((request) => request(axios, basePath)); 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. * @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> { updateSupermarketCategory(id: string, supermarketCategory?: SupermarketCategory, options?: any): AxiosPromise<SupermarketCategory> {
return localVarFp.updateSupermarketCategory(id, supermarketCategory, options).then((request) => request(axios, basePath)); 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. * @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)); 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] * @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)); 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. * @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} [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} [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} [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} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page. * @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
* @memberof ApiApi * @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) { 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, page, pageSize, options).then((request) => request(this.axios, this.basePath)); 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)); 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. * @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)); 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. * @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)); 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. * @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)); 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. * @param {string} id A unique integer value identifying this sync.

View File

@ -79,7 +79,10 @@ module.exports = {
priority: 1 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 //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" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q== 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": "@rollup/plugin-babel@^5.2.0":
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879" 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: dependencies:
babylon "^6.18.0" 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: babel-loader@^8.1.0:
version "8.2.2" version "8.2.2"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81" 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" readable-stream "^2.0.0"
stream-shift "^1.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: easy-stack@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/easy-stack/-/easy-stack-1.0.1.tgz#8afe4264626988cabb11f3c704ccd0c835411066" 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" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= 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: generic-names@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872" 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: dependencies:
isobject "^3.0.1" 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: is-regex@^1.0.4, is-regex@^1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" 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" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= 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" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -6137,6 +6171,11 @@ map-visit@^1.0.0:
dependencies: dependencies:
object-visit "^1.0.0" 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: md5.js@^1.3.4:
version "1.3.5" version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" 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" resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw== 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: watchpack-chokidar2@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"