Merge branch 'develop' into feature/keywords-rework

# Conflicts:
#	cookbook/static/vue/js/chunk-vendors.js
#	cookbook/static/vue/js/import_response_view.js
#	cookbook/static/vue/js/offline_view.js
#	cookbook/static/vue/js/recipe_search_view.js
#	cookbook/static/vue/js/recipe_view.js
#	cookbook/static/vue/js/supermarket_view.js
#	cookbook/static/vue/js/user_file_view.js
#	cookbook/templates/sw.js
#	cookbook/views/views.py
#	vue/src/components/RecipeCard.vue
#	vue/src/locales/en.json
This commit is contained in:
vabene1111 2021-06-30 14:57:33 +02:00
commit 28b67a2ba5
115 changed files with 14137 additions and 5122 deletions

View File

@ -83,7 +83,8 @@ REVERSE_PROXY_AUTH=0
# Default settings for spaces, apply per space and can be changed in the admin view
# SPACE_DEFAULT_MAX_RECIPES=0 # 0=unlimited recipes
# SPACE_DEFAULT_MAX_USERS=0 # 0=unlimited users per space
# SPACE_DEFAULT_FILES=1 # 1=can upload files (images, etc.) NOT IMPLEMENTED YET
# SPACE_DEFAULT_MAX_FILES=0 # Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.
# SPACE_DEFAULT_ALLOW_SHARING=1 # Allow users to share recipes with public links
# allow people to create accounts on your application instance (without an invite link)
# when unset: 0 (false)
@ -113,4 +114,9 @@ REVERSE_PROXY_AUTH=0
# SOCIAL_DEFAULT_ACCESS = 1
# if SOCIAL_DEFAULT_ACCESS is used, which group should be added
# SOCIAL_DEFAULT_GROUP=guest
# SOCIAL_DEFAULT_GROUP=guest
# Django session cookie settings. Can be changed to allow a single django application to authenticate several applications
# when running under the same database
# SESSION_COOKIE_DOMAIN=.example.com
# SESSION_COOKIE_NAME=sessionid # use this only to not interfere with non unified django applications under the same top level domain

View File

@ -9,18 +9,17 @@
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
<p align="center">
<img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=develop" >
<img src="https://img.shields.io/github/stars/vabene1111/recipes" >
<img src="https://img.shields.io/github/forks/vabene1111/recipes" >
<img src="https://img.shields.io/docker/pulls/vabene1111/recipes" >
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=master" ></a>
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
</p>
<p align="center">
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a>
<a href="https://docs.tandoor.dev/install/docker.html" target="_blank" rel="noopener noreferrer">Installation</a>
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Documentation</a>
<a href="https://app.tandoor.dev/" target="_blank" rel="noopener noreferrer">Demo</a>
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a>
</p>
![Preview](docs/preview.png)

View File

@ -31,14 +31,20 @@ admin.site.unregister(Group)
class SpaceAdmin(admin.ModelAdmin):
list_display = ('name', 'created_by', 'message')
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
search_fields = ('name', 'created_by__username')
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
date_hierarchy = 'created_at'
admin.site.register(Space, SpaceAdmin)
class UserPreferenceAdmin(admin.ModelAdmin):
list_display = ('name', 'space', 'theme', 'nav_color', 'default_page', 'search_style',)
list_display = ('name', 'space', 'theme', 'nav_color', 'default_page', 'search_style',) # TODO add new fields
search_fields = ('user__username', 'space__name')
list_filter = ('theme', 'nav_color', 'default_page', 'search_style')
date_hierarchy = 'created_at'
@staticmethod
def name(obj):
@ -50,6 +56,7 @@ admin.site.register(UserPreference, UserPreferenceAdmin)
class StorageAdmin(admin.ModelAdmin):
list_display = ('name', 'method')
search_fields = ('name',)
admin.site.register(Storage, StorageAdmin)
@ -57,6 +64,7 @@ admin.site.register(Storage, StorageAdmin)
class SyncAdmin(admin.ModelAdmin):
list_display = ('storage', 'path', 'active', 'last_checked')
search_fields = ('storage__name', 'path')
admin.site.register(Sync, SyncAdmin)
@ -102,6 +110,7 @@ admin.site.register(Keyword, KeywordAdmin)
class StepAdmin(admin.ModelAdmin):
list_display = ('name', 'type', 'order')
search_fields = ('name', 'type')
admin.site.register(Step, StepAdmin)
@ -120,6 +129,9 @@ def rebuild_index(modeladmin, request, queryset):
class RecipeAdmin(admin.ModelAdmin):
list_display = ('name', 'internal', 'created_by', 'storage')
search_fields = ('name', 'created_by__username')
list_filter = ('internal',)
date_hierarchy = 'created_at'
@staticmethod
def created_by(obj):
@ -137,6 +149,7 @@ admin.site.register(Food)
class IngredientAdmin(admin.ModelAdmin):
list_display = ('food', 'amount', 'unit')
search_fields = ('food__name', 'unit__name')
admin.site.register(Ingredient, IngredientAdmin)
@ -144,6 +157,8 @@ admin.site.register(Ingredient, IngredientAdmin)
class CommentAdmin(admin.ModelAdmin):
list_display = ('recipe', 'name', 'created_at')
search_fields = ('text', 'user__username')
date_hierarchy = 'created_at'
@staticmethod
def name(obj):
@ -162,6 +177,7 @@ admin.site.register(RecipeImport, RecipeImportAdmin)
class RecipeBookAdmin(admin.ModelAdmin):
list_display = ('name', 'user_name')
search_fields = ('name', 'created_by__username')
@staticmethod
def user_name(obj):
@ -191,6 +207,7 @@ admin.site.register(MealPlan, MealPlanAdmin)
class MealTypeAdmin(admin.ModelAdmin):
list_display = ('name', 'created_by', 'order')
search_fields = ('name', 'created_by__username')
admin.site.register(MealType, MealTypeAdmin)

View File

@ -7,6 +7,8 @@ from django.contrib import messages
from django.core.cache import caches
from gettext import gettext as _
from cookbook.models import InviteLink
class AllAuthCustomAdapter(DefaultAccountAdapter):
@ -14,7 +16,11 @@ class AllAuthCustomAdapter(DefaultAccountAdapter):
"""
Whether to allow sign ups.
"""
if request.resolver_match.view_name == 'account_signup' and not settings.ENABLE_SIGNUP:
signup_token = False
if 'signup_token' in request.session and InviteLink.objects.filter(valid_until__gte=datetime.datetime.today(), used_by=None, uuid=request.session['signup_token']).exists():
signup_token = True
if (request.resolver_match.view_name == 'account_signup' or request.resolver_match.view_name == 'socialaccount_signup') and not settings.ENABLE_SIGNUP and not signup_token:
return False
else:
return super(AllAuthCustomAdapter, self).is_open_for_signup(request)

View File

@ -0,0 +1,45 @@
import os
import sys
from PIL import Image
from io import BytesIO
def rescale_image_jpeg(image_object, base_width=720):
img = Image.open(image_object)
icc_profile = img.info.get('icc_profile') # remember color profile to not mess up colors
width_percent = (base_width / float(img.size[0]))
height = int((float(img.size[1]) * float(width_percent)))
img = img.resize((base_width, height), Image.ANTIALIAS)
img_bytes = BytesIO()
img.save(img_bytes, 'JPEG', quality=75, optimize=True, icc_profile=icc_profile)
return img_bytes
def rescale_image_png(image_object, base_width=720):
basewidth = 720
wpercent = (basewidth / float(image_object.size[0]))
hsize = int((float(image_object.size[1]) * float(wpercent)))
img = image_object.resize((basewidth, hsize), Image.ANTIALIAS)
im_io = BytesIO()
img.save(im_io, 'PNG', quality=70)
return img
def get_filetype(name):
try:
return os.path.splitext(name)[1]
except:
return '.jpeg'
def handle_image(request, image_object, filetype='.jpeg'):
if sys.getsizeof(image_object) / 8 > 500:
if filetype == '.jpeg':
return rescale_image_jpeg(image_object), filetype
if filetype == '.png':
return rescale_image_png(image_object), filetype
return image_object, filetype

View File

@ -1,3 +1,4 @@
import re
import string
import unicodedata
@ -22,20 +23,16 @@ def parse_fraction(x):
def parse_amount(x):
amount = 0
unit = ''
note = ''
did_check_frac = False
end = 0
while (
end < len(x)
and (
x[end] in string.digits
or (
(x[end] == '.' or x[end] == ',' or x[end] == '/')
and end + 1 < len(x)
and x[end + 1] in string.digits
)
)
):
while (end < len(x) and (x[end] in string.digits
or (
(x[end] == '.' or x[end] == ',' or x[end] == '/')
and end + 1 < len(x)
and x[end + 1] in string.digits
))):
end += 1
if end > 0:
if "/" in x[:end]:
@ -55,7 +52,11 @@ def parse_amount(x):
unit = x[end + 1:]
except ValueError:
unit = x[end:]
return amount, unit
if unit.startswith('(') or unit.startswith('-'): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
unit = ''
note = x
return amount, unit, note
def parse_ingredient_with_comma(tokens):
@ -106,6 +107,13 @@ def parse(x):
unit = ''
ingredient = ''
note = ''
unit_note = ''
# if the string contains parenthesis early on remove it and place it at the end
# because its likely some kind of note
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', x):
match = re.search('\((.[^\(])+\)', x)
x = x[:match.start()] + x[match.end():] + ' ' + x[match.start():match.end()]
tokens = x.split()
if len(tokens) == 1:
@ -114,17 +122,17 @@ def parse(x):
else:
try:
# try to parse first argument as amount
amount, unit = parse_amount(tokens[0])
amount, unit, unit_note = parse_amount(tokens[0])
# only try to parse second argument as amount if there are at least
# three arguments if it already has a unit there can't be
# a fraction for the amount
if len(tokens) > 2:
try:
if not unit == '':
# a unit is already found, no need to try the second argument for a fraction # noqa: E501
# a unit is already found, no need to try the second argument for a fraction
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except # noqa: E501
raise ValueError
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½' # noqa: E501
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
amount += parse_fraction(tokens[1])
# assume that units can't end with a comma
if len(tokens) > 3 and not tokens[2].endswith(','):
@ -142,7 +150,10 @@ def parse(x):
# try to use second argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
try:
ingredient, note = parse_ingredient(tokens[2:])
unit = tokens[1]
if unit == '':
unit = tokens[1]
else:
note = tokens[1]
except ValueError:
ingredient, note = parse_ingredient(tokens[1:])
else:
@ -158,11 +169,16 @@ def parse(x):
ingredient, note = parse_ingredient(tokens)
except ValueError:
ingredient = ' '.join(tokens[1:])
if unit_note not in note:
note += ' ' + unit_note
return amount, unit.strip(), ingredient.strip(), note.strip()
# small utility functions to prevent emtpy unit/food creation
def get_unit(unit, space):
if not unit:
return None
if len(unit) > 0:
u, created = Unit.objects.get_or_create(name=unit, space=space)
return u
@ -170,6 +186,8 @@ def get_unit(unit, space):
def get_food(food, space):
if not food:
return None
if len(food) > 0:
f, created = Food.objects.get_or_create(name=food, space=space)
return f

View File

@ -1,6 +1,8 @@
"""
Source: https://djangosnippets.org/snippets/1703/
"""
from django.conf import settings
from django.core.cache import caches
from django.views.generic.detail import SingleObjectTemplateResponseMixin
from django.views.generic.edit import ModelFormMixin
@ -90,7 +92,18 @@ def share_link_valid(recipe, share):
:return: true if a share link with the given recipe and uuid exists
"""
try:
return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
CACHE_KEY = f'recipe_share_{recipe.pk}_{share}'
if c := caches['default'].get(CACHE_KEY, False):
return c
if link := ShareLink.objects.filter(recipe=recipe, uuid=share, abuse_blocked=False).first():
if 0 < settings.SHARING_LIMIT < link.request_count:
return False
link.request_count += 1
link.save()
caches['default'].set(CACHE_KEY, True, timeout=3)
return True
return False
except ValidationError:
return False
@ -121,15 +134,18 @@ class GroupRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not has_group_permission(request.user, self.groups_required):
if not request.user.is_authenticated:
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
messages.add_message(request, messages.ERROR,
_('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
else:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
messages.add_message(request, messages.ERROR,
_('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
try:
obj = self.get_object()
if obj.get_space() != request.space:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
messages.add_message(request, messages.ERROR,
_('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
except AttributeError:
pass
@ -141,17 +157,20 @@ class OwnerRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
messages.add_message(request, messages.ERROR,
_('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
else:
if not is_object_owner(request.user, self.get_object()):
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!'))
messages.add_message(request, messages.ERROR,
_('You cannot interact with this object as it is not owned by you!'))
return HttpResponseRedirect(reverse('index'))
try:
obj = self.get_object()
if obj.get_space() != request.space:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
messages.add_message(request, messages.ERROR,
_('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
except AttributeError:
pass

View File

@ -40,7 +40,7 @@ class Pepperplate(Integration):
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n'
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
)
for ingredient in ingredients:
@ -49,7 +49,7 @@ class Pepperplate(Integration):
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
recipe.steps.add(step)

View File

@ -38,7 +38,7 @@ class ChefTap(Integration):
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space, )
step = Step.objects.create(instruction='\n'.join(directions))
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space,)
if source_url != '':
step.instruction += '\n' + source_url
@ -50,7 +50,7 @@ class ChefTap(Integration):
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
recipe.steps.add(step)

View File

@ -3,6 +3,7 @@ import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
@ -54,7 +55,7 @@ class Chowdown(Integration):
recipe.keywords.add(keyword)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions)
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions), space=self.request.space,
)
for ingredient in ingredients:
@ -62,7 +63,7 @@ class Chowdown(Integration):
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
recipe.steps.add(step)
@ -71,7 +72,7 @@ class Chowdown(Integration):
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if re.match(f'^images/{image}$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename))
return recipe

View File

@ -1,9 +1,11 @@
import json
from io import BytesIO
from re import match
from zipfile import ZipFile
from rest_framework.renderers import JSONRenderer
from cookbook.helper.image_processing import get_filetype
from cookbook.integration.integration import Integration
from cookbook.serializer import RecipeExportSerializer
@ -15,8 +17,9 @@ class Default(Integration):
recipe_string = recipe_zip.read('recipe.json').decode("utf-8")
recipe = self.decode_recipe(recipe_string)
if 'image.png' in recipe_zip.namelist():
self.import_recipe_image(recipe, BytesIO(recipe_zip.read('image.png')))
images = list(filter(lambda v: match('image.*', v), recipe_zip.namelist()))
if images:
self.import_recipe_image(recipe, BytesIO(recipe_zip.read(images[0])), filetype=get_filetype(images[0]))
return recipe
def decode_recipe(self, string):

View File

@ -28,7 +28,7 @@ class Domestica(Integration):
recipe.save()
step = Step.objects.create(
instruction=file['directions']
instruction=file['directions'], space=self.request.space,
)
if file['source'] != '':
@ -40,12 +40,12 @@ class Domestica(Integration):
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
recipe.steps.add(step)
if file['image'] != '':
self.import_recipe_image(recipe, BytesIO(base64.b64decode(file['image'].replace('data:image/jpeg;base64,', ''))))
self.import_recipe_image(recipe, BytesIO(base64.b64decode(file['image'].replace('data:image/jpeg;base64,', ''))), filetype='.jpeg')
return recipe

View File

@ -1,5 +1,6 @@
import datetime
import json
import os
import re
import uuid
from io import BytesIO, StringIO
@ -12,6 +13,7 @@ from django.utils.translation import gettext as _
from django_scopes import scope
from cookbook.forms import ImportExportBase
from cookbook.helper.image_processing import get_filetype
from cookbook.models import Keyword, Recipe
@ -59,7 +61,7 @@ class Integration:
recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
recipe_stream.close()
try:
recipe_zip_obj.writestr('image.png', r.image.file.read())
recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read())
except ValueError:
pass
@ -107,35 +109,52 @@ class Integration:
for f in files:
if 'RecipeKeeper' in f['name']:
import_zip = ZipFile(f['file'])
file_list = []
for z in import_zip.filelist:
if self.import_file_name_filter(z):
data_list = self.split_recipe_file(import_zip.read(z.filename).decode('utf-8'))
for d in data_list:
recipe = self.get_recipe_from_file(d)
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
file_list.append(z)
il.total_recipes += len(file_list)
for z in file_list:
data_list = self.split_recipe_file(import_zip.read(z.filename).decode('utf-8'))
for d in data_list:
recipe = self.get_recipe_from_file(d)
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
il.imported_recipes += 1
il.save()
import_zip.close()
elif '.zip' in f['name'] or '.paprikarecipes' in f['name']:
import_zip = ZipFile(f['file'])
file_list = []
for z in import_zip.filelist:
if self.import_file_name_filter(z):
try:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
except Exception as e:
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
file_list.append(z)
il.total_recipes += len(file_list)
for z in file_list:
try:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
il.imported_recipes += 1
il.save()
except Exception as e:
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
import_zip.close()
elif '.json' in f['name'] or '.txt' in f['name']:
data_list = self.split_recipe_file(f['file'])
il.total_recipes += len(data_list)
for d in data_list:
try:
recipe = self.get_recipe_from_file(d)
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
il.imported_recipes += 1
il.save()
except Exception as e:
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
elif '.rtk' in f['name']:
@ -143,12 +162,16 @@ class Integration:
for z in import_zip.filelist:
if self.import_file_name_filter(z):
data_list = self.split_recipe_file(import_zip.read(z.filename).decode('utf-8'))
il.total_recipes += len(data_list)
for d in data_list:
try:
recipe = self.get_recipe_from_file(d)
recipe.keywords.add(self.keyword)
il.msg += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
il.imported_recipes += 1
il.save()
except Exception as e:
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
import_zip.close()
@ -160,6 +183,9 @@ class Integration:
except BadZipFile:
il.msg += 'ERROR ' + _(
'Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n'
except:
il.msg += 'ERROR ' + _(
'An unexpected error occurred during the import. Please make sure you have uploaded a valid file.') + '\n'
if len(self.ignored_recipes) > 0:
il.msg += '\n' + _(
@ -182,13 +208,14 @@ class Integration:
self.ignored_recipes.append(recipe.name)
@staticmethod
def import_recipe_image(recipe, image_file):
def import_recipe_image(recipe, image_file, filetype='.jpeg'):
"""
Adds an image to a recipe naming it correctly
:param recipe: Recipe object
:param image_file: ByteIO stream containing the image
:param filetype: type of file to write bytes to, default to .jpeg if unknown
"""
recipe.image = File(image_file, name=f'{uuid.uuid4()}_{recipe.pk}.png')
recipe.image = File(image_file, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}')
recipe.save()
def get_recipe_from_file(self, file):
@ -217,4 +244,3 @@ class Integration:
- data - string content for file to get created in export zip
"""
raise NotImplementedError('Method not implemented in integration')

View File

@ -3,6 +3,7 @@ import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
@ -11,7 +12,7 @@ from cookbook.models import Recipe, Step, Food, Unit, Ingredient
class Mealie(Integration):
def import_file_name_filter(self, zip_info_object):
return re.match(r'^recipes/([A-Za-z\d-])+.json$', zip_info_object.filename)
return re.match(r'^recipes/([A-Za-z\d-])+/([A-Za-z\d-])+.json$', zip_info_object.filename)
def get_recipe_from_file(self, file):
recipe_json = json.loads(file.getvalue().decode("utf-8"))
@ -25,9 +26,9 @@ class Mealie(Integration):
# TODO parse times (given in PT2H3M )
ingredients_added = False
for s in recipe_json['recipeInstructions']:
for s in recipe_json['recipe_instructions']:
step = Step.objects.create(
instruction=s['text']
instruction=s['text'], space=self.request.space,
)
if not ingredients_added:
ingredients_added = True
@ -35,21 +36,31 @@ class Mealie(Integration):
if len(recipe_json['description'].strip()) > 500:
step.instruction = recipe_json['description'].strip() + '\n\n' + step.instruction
for ingredient in recipe_json['recipeIngredient']:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
))
for ingredient in recipe_json['recipe_ingredient']:
try:
if ingredient['food']:
f = get_food(ingredient['food'], self.request.space)
u = get_unit(ingredient['unit'], self.request.space)
amount = ingredient['quantity']
note = ingredient['note']
else:
amount, unit, ingredient, note = parse(ingredient['note'])
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
except:
pass
recipe.steps.add(step)
for f in self.files:
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if re.match(f'^images/{recipe_json["slug"]}.jpg$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
try:
self.import_recipe_image(recipe, BytesIO(import_zip.read(f'recipes/{recipe_json["slug"]}/images/min-original.webp')), filetype=get_filetype(f'recipes/{recipe_json["slug"]}/images/original'))
except:
pass
return recipe

View File

@ -44,7 +44,7 @@ class MealMaster(Integration):
recipe.keywords.add(keyword)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n'
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
)
for ingredient in ingredients:
@ -53,7 +53,7 @@ class MealMaster(Integration):
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
recipe.steps.add(step)

View File

@ -3,6 +3,7 @@ import re
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
@ -29,7 +30,7 @@ class NextcloudCookbook(Integration):
ingredients_added = False
for s in recipe_json['recipeInstructions']:
step = Step.objects.create(
instruction=s
instruction=s, space=self.request.space,
)
if not ingredients_added:
if len(recipe_json['description'].strip()) > 500:
@ -42,7 +43,7 @@ class NextcloudCookbook(Integration):
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
recipe.steps.add(step)
@ -51,7 +52,7 @@ class NextcloudCookbook(Integration):
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if re.match(f'^Recipes/{recipe.name}/full.jpg$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename))
return recipe

View File

@ -24,13 +24,13 @@ class OpenEats(Integration):
if file["source"] != '':
instructions += file["source"]
step = Step.objects.create(instruction=instructions)
step = Step.objects.create(instruction=instructions, space=self.request.space,)
for ingredient in file['ingredients']:
f = get_food(ingredient['food'], self.request.space)
u = get_unit(ingredient['unit'], self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=ingredient['amount']
food=f, unit=u, amount=ingredient['amount'], space=self.request.space,
))
recipe.steps.add(step)

View File

@ -55,7 +55,7 @@ class Paprika(Integration):
pass
step = Step.objects.create(
instruction=instructions
instruction=instructions, space=self.request.space,
)
if len(recipe_json['description'].strip()) > 500:
@ -73,7 +73,7 @@ class Paprika(Integration):
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
except AttributeError:
pass
@ -81,6 +81,6 @@ class Paprika(Integration):
recipe.steps.add(step)
if recipe_json.get("photo_data", None):
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])))
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])), filetype='.jpeg')
return recipe

View File

@ -7,6 +7,7 @@ from zipfile import ZipFile
import imghdr
from django.utils.translation import gettext as _
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
@ -25,7 +26,7 @@ class RecetteTek(Integration):
recipe_list = [r for r in recipe_json]
return recipe_list
def get_recipe_from_file(self, file):
# Create initial recipe with just a title and a decription
@ -44,7 +45,7 @@ class RecetteTek(Integration):
if not instructions:
instructions = ''
step = Step.objects.create(instruction=instructions)
step = Step.objects.create(instruction=instructions, space=self.request.space,)
# Append the original import url to the step (if it exists)
try:
@ -53,7 +54,7 @@ class RecetteTek(Integration):
step.save()
except Exception as e:
print(recipe.name, ': failed to import source url ', str(e))
try:
# Process the ingredients. Assumes 1 ingredient per line.
for ingredient in file['ingredients'].split('\n'):
@ -62,7 +63,7 @@ class RecetteTek(Integration):
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
except Exception as e:
print(recipe.name, ': failed to parse recipe ingredients ', str(e))
@ -96,7 +97,7 @@ class RecetteTek(Integration):
recipe.waiting_time = int(file['cookingTime'])
except Exception as e:
print(recipe.name, ': failed to parse cooking time ', str(e))
recipe.save()
# Import the recipe keywords
@ -110,20 +111,20 @@ class RecetteTek(Integration):
pass
# TODO: Parse Nutritional Information
# Import the original image from the zip file, if we cannot do that, attempt to download it again.
try:
if file['pictures'][0] !='':
if file['pictures'][0] != '':
image_file_name = file['pictures'][0].split('/')[-1]
for f in self.files:
if '.rtk' in f['name']:
import_zip = ZipFile(f['file'])
self.import_recipe_image(recipe, BytesIO(import_zip.read(image_file_name)))
self.import_recipe_image(recipe, BytesIO(import_zip.read(image_file_name)), filetype=get_filetype(image_file_name))
else:
if file['originalPicture'] != '':
response=requests.get(file['originalPicture'])
response = requests.get(file['originalPicture'])
if imghdr.what(BytesIO(response.content)) != None:
self.import_recipe_image(recipe, BytesIO(response.content))
self.import_recipe_image(recipe, BytesIO(response.content), filetype=get_filetype(file['originalPicture']))
else:
raise Exception("Original image failed to download.")
except Exception as e:

View File

@ -41,7 +41,7 @@ class RecipeKeeper(Integration):
except AttributeError:
pass
step = Step.objects.create(instruction='')
step = Step.objects.create(instruction='', space=self.request.space,)
for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"):
if ingredient.text == "":
@ -50,7 +50,7 @@ class RecipeKeeper(Integration):
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
for s in file.find("div", {"itemprop": "recipeDirections"}).find_all("p"):
@ -70,7 +70,7 @@ class RecipeKeeper(Integration):
for f in self.files:
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipe-photo").get("src"))))
self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipe-photo").get("src"))), filetype='.jpeg')
except Exception as e:
pass

View File

@ -36,7 +36,7 @@ class RecipeSage(Integration):
ingredients_added = False
for s in file['recipeInstructions']:
step = Step.objects.create(
instruction=s['text']
instruction=s['text'], space=self.request.space,
)
if not ingredients_added:
ingredients_added = True
@ -46,7 +46,7 @@ class RecipeSage(Integration):
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
recipe.steps.add(step)

View File

@ -43,7 +43,7 @@ class RezKonv(Integration):
recipe.keywords.add(keyword)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n'
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
)
for ingredient in ingredients:
@ -52,7 +52,7 @@ class RezKonv(Integration):
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
recipe.steps.add(step)

View File

@ -43,14 +43,14 @@ class Safron(Integration):
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space, )
step = Step.objects.create(instruction='\n'.join(directions))
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space,)
for ingredient in ingredients:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
recipe.steps.add(step)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-06-12 18:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0131_auto_20210608_1929'),
]
operations = [
migrations.AddField(
model_name='sharelink',
name='request_count',
field=models.IntegerField(default=0),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-06-12 18:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0132_sharelink_request_count'),
]
operations = [
migrations.AddField(
model_name='sharelink',
name='abuse_blocked',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-06-15 19:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0133_sharelink_abuse_blocked'),
]
operations = [
migrations.AddField(
model_name='space',
name='allow_sharing',
field=models.BooleanField(default=True),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.2.4 on 2021-06-15 20:10
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0134_space_allow_sharing'),
]
operations = [
migrations.AddField(
model_name='ingredient',
name='space',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
migrations.AddField(
model_name='nutritioninformation',
name='space',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
migrations.AddField(
model_name='step',
name='space',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.4 on 2021-06-17 11:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0135_auto_20210615_2210'),
]
operations = [
migrations.AddField(
model_name='importlog',
name='imported_recipes',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='importlog',
name='total_recipes',
field=models.IntegerField(default=0),
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 3.2.4 on 2021-06-17 13:01
from django.db import migrations
from django.db.models import Subquery, OuterRef
from django_scopes import scopes_disabled
from django.db import migrations, models
import django.db.models.deletion
def migrate_spaces(apps, schema_editor):
with scopes_disabled():
Recipe = apps.get_model('cookbook', 'Recipe')
Step = apps.get_model('cookbook', 'Step')
Ingredient = apps.get_model('cookbook', 'Ingredient')
NutritionInformation = apps.get_model('cookbook', 'NutritionInformation')
Step.objects.filter(recipe__isnull=True).delete()
Ingredient.objects.filter(step__recipe__isnull=True).delete()
NutritionInformation.objects.filter(recipe__isnull=True).delete()
Step.objects.update(space=Subquery(Step.objects.filter(pk=OuterRef('pk')).values('recipe__space')[:1]))
Ingredient.objects.update(space=Subquery(Ingredient.objects.filter(pk=OuterRef('pk')).values('step__recipe__space')[:1]))
NutritionInformation.objects.update(space=Subquery(NutritionInformation.objects.filter(pk=OuterRef('pk')).values('recipe__space')[:1]))
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0136_auto_20210617_1343'),
]
operations = [
migrations.RunPython(migrate_spaces),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 3.2.4 on 2021-06-17 14:02
from django.db import migrations
from django.db.models import Subquery, OuterRef
from django_scopes import scopes_disabled
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0137_auto_20210617_1501'),
]
operations = [
migrations.AlterField(
model_name='ingredient',
name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
migrations.AlterField(
model_name='nutritioninformation',
name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
migrations.AlterField(
model_name='step',
name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.4 on 2021-06-22 16:14
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0138_auto_20210617_1602'),
]
operations = [
migrations.AddField(
model_name='space',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.4 on 2021-06-22 16:19
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0139_space_created_at'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@ -70,10 +70,12 @@ class PermissionModelMixin:
class Space(ExportModelOperationsMixin('space'), models.Model):
name = models.CharField(max_length=128, default='Default')
created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
created_at = models.DateTimeField(auto_now_add=True)
message = models.CharField(max_length=512, default='', blank=True)
max_recipes = models.IntegerField(default=0)
max_file_storage_mb = models.IntegerField(default=0, help_text=_('Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.'))
max_users = models.IntegerField(default=0)
allow_sharing = models.BooleanField(default=True)
demo = models.BooleanField(default=False)
def __str__(self):
@ -157,6 +159,7 @@ class UserPreference(models.Model, PermissionModelMixin):
shopping_auto_sync = models.IntegerField(default=5)
sticky_navbar = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
objects = ScopedManager(space='space')
@ -388,14 +391,8 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
no_amount = models.BooleanField(default=False)
order = models.IntegerField(default=0)
objects = ScopedManager(space='step__recipe__space')
@staticmethod
def get_space_key():
return 'step', 'recipe', 'space'
def get_space(self):
return self.step_set.first().recipe_set.first().space
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food)
@ -424,14 +421,8 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
show_as_header = models.BooleanField(default=True)
search_vector = SearchVectorField(null=True)
objects = ScopedManager(space='recipe__space')
@staticmethod
def get_space_key():
return 'recipe', 'space'
def get_space(self):
return self.recipe_set.first().space
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def get_instruction_render(self):
from cookbook.helper.template_helper import render_instructions
@ -453,17 +444,11 @@ class NutritionInformation(models.Model, PermissionModelMixin):
max_length=512, default="", null=True, blank=True
)
objects = ScopedManager(space='recipe__space')
@staticmethod
def get_space_key():
return 'recipe', 'space'
def get_space(self):
return self.recipe_set.first().space
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return 'Nutrition'
return f'Nutrition {self.pk}'
class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModelMixin):
@ -690,6 +675,8 @@ class ShoppingList(ExportModelOperationsMixin('shopping_list'), models.Model, Pe
class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
uuid = models.UUIDField(default=uuid.uuid4)
request_count = models.IntegerField(default=0)
abuse_blocked = models.BooleanField(default=False)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
@ -773,6 +760,10 @@ class ImportLog(models.Model, PermissionModelMixin):
running = models.BooleanField(default=True)
msg = models.TextField(default="")
keyword = models.ForeignKey(Keyword, null=True, blank=True, on_delete=models.SET_NULL)
total_recipes = models.IntegerField(default=0)
imported_recipes = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)

View File

@ -3,7 +3,7 @@ from decimal import Decimal
from gettext import gettext as _
from django.contrib.auth.models import User
from django.db.models import QuerySet, Sum
from django.db.models import QuerySet, Sum, Avg
from drf_writable_nested import (UniqueFieldsMixin,
WritableNestedModelSerializer)
from rest_framework import serializers
@ -301,6 +301,10 @@ class IngredientSerializer(WritableNestedModelSerializer):
unit = UnitSerializer(allow_null=True)
amount = CustomDecimalField()
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta:
model = Ingredient
fields = (
@ -313,7 +317,11 @@ class StepSerializer(WritableNestedModelSerializer):
ingredients = IngredientSerializer(many=True)
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
ingredients_vue = serializers.SerializerMethodField('get_ingredients_vue')
file = UserFileViewSerializer(allow_null=True)
file = UserFileViewSerializer(allow_null=True, required=False)
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
def get_ingredients_vue(self, obj):
return obj.get_instruction_render()
@ -330,13 +338,34 @@ class StepSerializer(WritableNestedModelSerializer):
class NutritionInformationSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta:
model = NutritionInformation
fields = ('id', 'carbohydrates', 'fats', 'proteins', 'calories', 'source')
class RecipeOverviewSerializer(WritableNestedModelSerializer):
class RecipeBaseSerializer(WritableNestedModelSerializer):
def get_recipe_rating(self, obj):
rating = obj.cooklog_set.filter(created_by=self.context['request'].user, rating__gt=0).aggregate(Avg('rating'))
if rating['rating__avg']:
return rating['rating__avg']
return 0
def get_recipe_last_cooked(self, obj):
last = obj.cooklog_set.filter(created_by=self.context['request'].user).last()
if last:
return last.created_at
return None
class RecipeOverviewSerializer(RecipeBaseSerializer):
keywords = KeywordLabelSerializer(many=True)
rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
def create(self, validated_data):
pass
@ -349,22 +378,24 @@ class RecipeOverviewSerializer(WritableNestedModelSerializer):
fields = (
'id', 'name', 'description', 'image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'file_path'
'internal', 'servings', 'servings_text', 'rating', 'last_cooked',
)
read_only_fields = ['image', 'created_by', 'created_at']
class RecipeSerializer(WritableNestedModelSerializer):
class RecipeSerializer(RecipeBaseSerializer):
nutrition = NutritionInformationSerializer(allow_null=True, required=False)
steps = StepSerializer(many=True)
keywords = KeywordSerializer(many=True)
rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
class Meta:
model = Recipe
fields = (
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'nutrition', 'servings', 'file_path', 'servings_text',
'internal', 'nutrition', 'servings', 'file_path', 'servings_text', 'rating', 'last_cooked',
)
read_only_fields = ['image', 'created_by', 'created_at']
@ -538,7 +569,7 @@ class ImportLogSerializer(serializers.ModelSerializer):
class Meta:
model = ImportLog
fields = ('id', 'type', 'msg', 'running', 'keyword', 'created_by', 'created_at')
fields = ('id', 'type', 'msg', 'running', 'keyword', 'total_recipes', 'imported_recipes', 'created_by', 'created_at')
read_only_fields = ('created_by',)
@ -596,6 +627,10 @@ class IngredientExportSerializer(WritableNestedModelSerializer):
unit = UnitExportSerializer(allow_null=True)
amount = CustomDecimalField()
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta:
model = Ingredient
fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount')
@ -604,6 +639,10 @@ class IngredientExportSerializer(WritableNestedModelSerializer):
class StepExportSerializer(WritableNestedModelSerializer):
ingredients = IngredientExportSerializer(many=True)
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta:
model = Step
fields = ('name', 'type', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')

File diff suppressed because it is too large Load Diff

View File

@ -10402,16 +10402,6 @@ footer a:hover {
box-shadow: none
}
.modal-content {
width: 500px
}
@media (max-width: 575px) {
.modal-content {
width: 300px
}
}
.modal-content .modal-header {
justify-content: center;
border: none

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

@ -6,6 +6,14 @@
{% block title %}{% trans "E-mail Addresses" %}{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans 'Email' %}</li>
</ol>
</nav>
<h3>{% trans "E-mail Addresses" %}</h3>
{% if user.emailaddress_set.all %}
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>

View File

@ -44,13 +44,13 @@
{% if socialaccount_providers %}
<div class="row" style="margin-top: 2vh">
<div class="col-6 offset-3">
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
<h5>{% trans "Social Login" %}</h5>
<span>{% trans 'You can use any of the following providers to sign in.' %}</span>
<br/>
<br/>
<ul class="socialaccount_providers">
<ul class="socialaccount_providers list-unstyled">
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
</ul>

View File

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load i18n %}
{% block head_title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans 'Password' %}</li>
</ol>
</nav>
<h1>{% trans "Change Password" %}</h1>
<form method="POST" action="{% url 'account_change_password' %}" class="password_change">
{% csrf_token %}
{{ form | crispy }}
<button type="submit" name="action" class="btn btn-success">{% trans "Change Password" %}</button>
<a href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
</form>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load i18n %}
{% block head_title %}{% trans "Set Password" %}{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans 'Password' %}</li>
</ol>
</nav>
<h1>{% trans "Set Password" %}</h1>
<form method="POST" action="{% url 'account_set_password' %}" class="password_set">
{% csrf_token %}
{{ form | crispy }}
<input type="submit" class="btn btn-primary" name="action" value="{% trans 'Set Password' %}"/>
</form>
{% endblock %}

View File

@ -67,9 +67,9 @@
</button>
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="/" aria-label="Tandoor">
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
</a>
<a class="navbar-brand p-0 me-2 justify-content-center" href="/" aria-label="Tandoor">
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
</a>
{% endif %}
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto">
@ -152,12 +152,18 @@
<a class="dropdown-item" href="{% url 'view_history' %}"><i
class="fas fa-history"></i> {% trans 'History' %}</a>
{% if request.user == request.space.created_by %}
<a class="dropdown-item" href="{% url 'view_space' %}"><i class="fas fa-server fa-fw"></i> {% trans 'Space Settings' %}</a>
<a class="dropdown-item" href="{% url 'view_space' %}"><i
class="fas fa-server fa-fw"></i> {% trans 'Space Settings' %}</a>
{% endif %}
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'view_system' %}"><i
class="fas fa-server fa-fw"></i> {% trans 'System' %}</a>
<a class="dropdown-item" href="{% url 'admin:index' %}"><i
class="fas fa-user-shield fa-fw"></i> {% trans 'Admin' %}</a>
{% if user.is_superuser %}
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'view_system' %}"><i
class="fas fa-server fa-fw"></i> {% trans 'System' %}</a>
<a class="dropdown-item" href="{% url 'list_keyword' %}"><i
class="fas fa-tags"></i> {% trans 'Keywords' %}</a>
<a class="dropdown-item" href="{% url 'admin:index' %}"><i
class="fas fa-user-shield fa-fw"></i> {% trans 'Admin' %}</a>
{% endif %}
@ -192,10 +198,7 @@
</div>
{% endif %}
<br/>
<br/>
<div class="container-fluid" id="id_base_container">
<div class="container-fluid mt-2 mt-md-5 mt-xl-5 mt-lg-5" id="id_base_container">
<div class="row">
<div class="col-xl-2 d-none d-xl-block">
{% block content_xl_left %}

View File

@ -67,7 +67,7 @@
<img src="{% if recipe.image %}{{ recipe.image.url }}{% endif %}" id="id_image"
class="img img-fluid img-responsive"
style="max-height: 20vh">
<input type="file" @change="imageChanged">
<input class="mt-2" type="file" @change="imageChanged">
</div>
<div class="col-md-6">
<label for="id_name"> {% trans 'Preparation Time' %}</label>

View File

@ -701,10 +701,10 @@
let first = true
for (let se of this.shopping_list) {
if (first) {
url += `?r=[${se.recipe},${se.servings}]`
url += `?r=[${se.recipe.id},${se.servings}]`
first = false
} else {
url += `&r=[${se.recipe},${se.servings}]`
url += `&r=[${se.recipe.id},${se.servings}]`
}
}
return url

View File

@ -12,9 +12,11 @@
{% block content %}
<h3>
{% trans 'Settings' %}
</h3>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
</ol>
</nav>
<!-- Nav tabs -->
<ul class="nav nav-tabs" id="myTab" role="tablist" style="margin-bottom: 2vh">
@ -56,23 +58,12 @@
class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
<h4>{% trans 'Password Settings' %}</h4>
<form action="." method="post">
{% csrf_token %}
{{ password_form|crispy }}
<button class="btn btn-success" type="submit" name="password_form"><i
class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
<h4>{% trans 'Account Settings' %}</h4>
<h4>{% trans 'Email Settings' %}</h4>
<a href="{% url 'account_email'%}" class="btn btn-primary">{% trans 'Emails' %}</a>
<a href="{% url 'account_change_password'%}" class="btn btn-primary">{% trans 'Password' %}</a>
<a href="{% url 'account_email'%}" class="btn btn-primary">{% trans 'Manage Email Settings' %}</a>
<br/><br/>
<h4>{% trans 'Social' %}</h4>
<a href="{% url 'socialaccount_connections' %}" class="btn btn-primary">{% trans 'Manage Social Accounts' %}</a>
<a href="{% url 'socialaccount_connections' %}" class="btn btn-primary">{% trans 'Social' %}</a>
<br/>
<br/>
<br/>

View File

@ -4,6 +4,14 @@
{% block head_title %}{% trans "Account Connections" %}{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans 'Social' %}</li>
</ol>
</nav>
<h3>{% trans "Account Connections" %}</h3>
{% if form.accounts %}
@ -46,7 +54,7 @@
<h4>{% trans 'Add a 3rd Party Account' %}</h4>
<ul class="socialaccount_providers">
<ul class="socialaccount_providers list-unstyled">
{% include "socialaccount/snippets/provider_list.html" with process="connect" %}
</ul>

View File

@ -0,0 +1,60 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load i18n %}
{% block head_title %}{% trans "Signup" %}{% endblock %}
{% block content %}
<h1>{% trans "Sign Up" %}</h1>
<p>{% blocktrans with provider_name=account.get_provider.name site_name=site.name %}You are about to use your
{{ provider_name }} account to login to
{{ site_name }}. As a final step, please complete the following form:{% endblocktrans %}</p>
<form class="signup" id="signup_form" method="post" action="{% url 'socialaccount_signup' %}">
{% csrf_token %}
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
{% endif %}
<div class="form-group">
{{ form.username |as_crispy_field }}
</div>
<div class="form-group">
{{ form.email |as_crispy_field }}
</div>
<div class="form-group">
{{ form.email2 |as_crispy_field }}
</div>
{% if TERMS_URL != '' or PRIVACY_URL != '' %}
<div class="form-group">
{{ form.terms |as_crispy_field }}
<small>
{% trans 'I accept the follwoing' %}
{% if TERMS_URL != '' %}
<a href="{{ TERMS_URL }}" target="_blank"
rel="noreferrer nofollow">{% trans 'Terms and Conditions' %}</a>
{% endif %}
{% if TERMS_URL != '' or PRIVACY_URL != '' %}
{% trans 'and' %}
{% endif %}
{% if PRIVACY_URL != '' %}
<a href="{{ PRIVACY_URL }}" target="_blank"
rel="noreferrer nofollow">{% trans 'Privacy Policy' %}</a>
{% endif %}
</small>
</div>
{% endif %}
{% if CAPTCHA_ENABLED %}
<div class="form-group">
{{ form.captcha.errors }}
{{ form.captcha }}
</div>
{% endif %}
<button type="submit" class="btn btn-primary">{% trans "Sign Up" %} &raquo;</button>
</form>
{% endblock %}

View File

@ -0,0 +1,133 @@
{% load i18n %}
{% load socialaccount %}
{% get_providers as socialaccount_providers %}
{% for provider in socialaccount_providers %}
{% if provider.id == "openid" %}
{% for brand in provider.get_brands %}
<li>
<a title="{{ brand.name }}"
class="socialaccount_provider {{ provider.id }} {{ brand.id }}"
href="{% provider_login_url provider.id openid=brand.openid_url process=process %}"
>{{ brand.name }}</a>
</li>
{% endfor %}
{% endif %}
<li CLASS="mb-1">
{% if provider.id == 'discord' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn discord-login-button btn-social"><i
class="fab fa-discord"></i> {% trans 'Sign in using' %} Discord
</button>
</a>
{% elif provider.id == 'github' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-github"><i
class="fab fa-github"></i> {% trans 'Sign in using' %} Github
</button>
</a>
{% elif provider.id == 'reddit' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-reddit"><i
class="fab fa-reddit"></i> {% trans 'Sign in using' %} Reddit
</button>
</a>
{% elif provider.id == 'twitter' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-twitter"><i
class="fab fa-twitter"></i> {% trans 'Sign in using' %} Twitter
</button>
</a>
{% elif provider.id == 'dropbox' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-dropbox"><i
class="fab fa-dropbox"></i> {% trans 'Sign in using' %} Dropbox
</button>
</a>
{% elif provider.id == 'google' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-google"><i
class="fab fa-google"></i> {% trans 'Sign in using' %} Google
</button>
</a>
{% elif provider.id == 'facebook' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-facebook"><i
class="fab fa-facebook"></i> {% trans 'Sign in using' %} Facebook
</button>
</a>
{% elif provider.id == 'instagram' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-instagram"><i
class="fab fa-instagram"></i> {% trans 'Sign in using' %} Instagram
</button>
</a>
{% elif provider.id == 'flickr' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-flickr"><i
class="fab fa-flickr"></i> {% trans 'Sign in using' %} Flickr
</button>
</a>
{% elif provider.id == 'apple' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-apple"><i
class="fab fa-apple"></i> {% trans 'Sign in using' %} Apple
</button>
</a>
{% elif provider.id == 'pinterest' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-pinterest"><i
class="fab fa-pinterest"></i> {% trans 'Sign in using' %} Pinterest
</button>
</a>
{% elif provider.id == 'windowslive' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-microsoft"><i
class="fab fa-microsoft"></i> {% trans 'Sign in using' %} Microsoft Live
</button>
</a>
{% elif provider.id == 'yahoo' %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-yahoo"><i
class="fab fa-yahoo"></i> {% trans 'Sign in using' %} Yahoo
</button>
</a>
{% else %}
<a title="{{ provider.name }}"
class="socialaccount_provider {{ provider.id }}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">
<button class="btn btn-social btn-success"><i
class="fas fa-sign-in-alt"></i> {% trans 'Sign in using' %} {{ provider.name }}
</button>
</a>
{% endif %}
</li>
{% endfor %}

View File

@ -14,7 +14,13 @@
{% block content %}
<h3>{{ request.space.name }} <small>{% if HOSTED %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'view_space' %}">{% trans 'Space Settings' %}</a></li>
</ol>
</nav>
<h3><span class="text-muted">{% trans 'Space:' %}</span> {{ request.space.name }} <small>{% if HOSTED %}
<a href="https://tandoor.dev/manage">{% trans 'Manage Subscription' %}</a>{% endif %}</small></h3>
<br/>

File diff suppressed because one or more lines are too long

View File

@ -55,7 +55,7 @@ def recipe_rating(recipe, user):
if not user.is_authenticated:
return ''
rating = recipe.cooklog_set \
.filter(created_by=user, rating__gte=0) \
.filter(created_by=user, rating__gt=0) \
.aggregate(Avg('rating'))
if rating['rating__avg']:
@ -64,7 +64,7 @@ def recipe_rating(recipe, user):
rating_stars = rating_stars + '<i class="fas fa-star fa-xs"></i>'
if rating['rating__avg'] % 1 >= 0.5:
rating_stars = rating_stars + '<i class="fas fa-star-half-alt fa-xs"></i>' # noqa: E501
rating_stars = rating_stars + '<i class="fas fa-star-half-alt fa-xs"></i>'
rating_stars += '</span>'

View File

@ -1,10 +1,11 @@
import json
import pytest
from django.db.models import Subquery, OuterRef
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Ingredient
from cookbook.models import Ingredient, Step
LIST_URL = 'api:ingredient-list'
DETAIL_URL = 'api:ingredient-detail'
@ -28,6 +29,8 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
with scopes_disabled():
recipe_1_s1.space = space_2
recipe_1_s1.save()
Step.objects.update(space=Subquery(Step.objects.filter(pk=OuterRef('pk')).values('recipe__space')[:1]))
Ingredient.objects.update(space=Subquery(Ingredient.objects.filter(pk=OuterRef('pk')).values('step__recipe__space')[:1]))
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 10

View File

@ -1,10 +1,11 @@
import json
import pytest
from django.db.models import Subquery, OuterRef
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Step
from cookbook.models import Step, Ingredient
LIST_URL = 'api:step-list'
DETAIL_URL = 'api:step-detail'
@ -28,6 +29,8 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
with scopes_disabled():
recipe_1_s1.space = space_2
recipe_1_s1.save()
Step.objects.update(space=Subquery(Step.objects.filter(pk=OuterRef('pk')).values('recipe__space')[:1]))
Ingredient.objects.update(space=Subquery(Ingredient.objects.filter(pk=OuterRef('pk')).values('step__recipe__space')[:1]))
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 2

View File

@ -51,8 +51,8 @@ def get_random_recipe(space_1, u1_s1):
internal=True,
)
s1 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), )
s2 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), )
s1 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), space=space_1, )
s2 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), space=space_1, )
r.steps.add(s1)
r.steps.add(s2)
@ -64,6 +64,7 @@ def get_random_recipe(space_1, u1_s1):
food=Food.objects.create(name=uuid.uuid4(), space=space_1, ),
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
note=uuid.uuid4(),
space=space_1,
)
)
@ -73,6 +74,7 @@ def get_random_recipe(space_1, u1_s1):
food=Food.objects.create(name=uuid.uuid4(), space=space_1, ),
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
note=uuid.uuid4(),
space=space_1,
)
)

View File

@ -773,7 +773,7 @@ COOKPAD = {
"text": "Water",
"id": 49092
},
"note": "",
"note": "2-3",
"original": "2-3 c Water"
},
{
@ -1498,10 +1498,10 @@ GIALLOZAFFERANO = {
"id": 64900
},
"ingredient": {
"text": "Pane (raffermo o secco) 80 g",
"text": "Pane 80 g",
"id": 24720
},
"note": "",
"note": "raffermo o secco",
"original": "Pane (raffermo o secco) 80 g"
},
{

View File

@ -53,7 +53,12 @@ def test_ingredient_parser():
"50 g smör eller margarin": (50, "g", "smör eller margarin", ""),
"3,5 l Wasser": (3.5, "l", "Wasser", ""),
"3.5 l Wasser": (3.5, "l", "Wasser", ""),
"400 g Karotte(n)": (400, "g", "Karotte(n)", "")
"400 g Karotte(n)": (400, "g", "Karotte(n)", ""),
"400g unsalted butter": (400, "g", "butter", "unsalted"),
"2L Wasser": (2, "L", "Wasser", ""),
"1 (16 ounce) package dry lentils, rinsed": (1, "package", "dry lentils, rinsed", "16 ounce"),
"2-3 c Water": (2, "c", "Water", "2-3"),
"Pane (raffermo o secco) 80 g": (0, "", "Pane 80 g", "raffermo o secco"), #TODO this is actually not a good result but currently expected
}
# for German you could say that if an ingredient does not have
# an amount # and it starts with a lowercase letter, then that

View File

@ -64,6 +64,7 @@ urlpatterns = [
path('history/', views.history, name='view_history'),
path('supermarket/', views.supermarket, name='view_supermarket'),
path('files/', views.files, name='view_files'),
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
path('test/', views.test, name='view_test'),
path('test2/', views.test2, name='view_test2'),
@ -105,6 +106,7 @@ urlpatterns = [
path('api/recipe-from-source/', api.recipe_from_source, name='api_recipe_from_source'),
path('api/backup/', api.get_backup, name='api_backup'),
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'),

View File

@ -16,6 +16,7 @@ from django.db.models import Q
from django.http import FileResponse, HttpResponse, JsonResponse
from django_scopes import scopes_disabled
from django.shortcuts import redirect, get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext as _
from icalendar import Calendar, Event
from recipe_scrapers import scrape_me, WebsiteNotImplementedError, NoSchemaFoundInWildMode
@ -28,6 +29,7 @@ from rest_framework.response import Response
from rest_framework.viewsets import ViewSetMixin
from treebeard.exceptions import PathOverflow, InvalidMoveToDescendant, InvalidPosition
from cookbook.helper.image_processing import handle_image
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
CustomIsOwner, CustomIsShare,
@ -41,7 +43,7 @@ from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
MealType, Recipe, RecipeBook, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step,
Storage, Sync, SyncLog, Unit, UserPreference,
ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile)
ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile, ShareLink)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
@ -482,16 +484,9 @@ class RecipeViewSet(viewsets.ModelViewSet):
if serializer.is_valid():
serializer.save()
img = Image.open(obj.image)
basewidth = 720
wpercent = (basewidth / float(img.size[0]))
hsize = int((float(img.size[1]) * float(wpercent)))
img = img.resize((basewidth, hsize), Image.ANTIALIAS)
im_io = io.BytesIO()
img.save(im_io, 'PNG', quality=70)
obj.image = File(im_io, name=f'{uuid.uuid4()}_{obj.pk}.png')
img, filetype = handle_image(request, obj.image)
obj.image = File(img, name=f'{uuid.uuid4()}_{obj.pk}{filetype}')
obj.save()
return Response(serializer.data)
@ -669,6 +664,16 @@ def sync_all(request):
return redirect('list_recipe_import')
@group_required('user')
def share_link(request, pk):
if request.space.allow_sharing:
recipe = get_object_or_404(Recipe, pk=pk, space=request.space)
link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space)
return JsonResponse({'pk': pk, 'share': link.uuid, 'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
else:
return JsonResponse({'error': 'sharing_disabled'}, status=403)
@group_required('user')
@ajax_request
def log_cooking(request, recipe_id):

View File

@ -17,6 +17,7 @@ from PIL import Image, UnidentifiedImageError
from requests.exceptions import MissingSchema
from cookbook.forms import BatchEditForm, SyncForm
from cookbook.helper.image_processing import handle_image
from cookbook.helper.permission_helper import group_required, has_group_permission
from cookbook.helper.recipe_url_import import parse_cooktime
from cookbook.models import (Comment, Food, Ingredient, Keyword, Recipe,
@ -142,7 +143,7 @@ def import_url(request):
)
step = Step.objects.create(
instruction=data['recipeInstructions'],
instruction=data['recipeInstructions'], space=request.space,
)
recipe.steps.add(step)
@ -157,7 +158,7 @@ def import_url(request):
recipe.keywords.add(k)
for ing in data['recipeIngredient']:
ingredient = Ingredient()
ingredient = Ingredient(space=request.space,)
if ing['ingredient']['text'] != '':
ingredient.food, f_created = Food.objects.get_or_create(
@ -188,23 +189,20 @@ def import_url(request):
if 'image' in data and data['image'] != '' and data['image'] is not None:
try:
response = requests.get(data['image'])
img = Image.open(BytesIO(response.content))
# todo move image processing to dedicated function
basewidth = 720
wpercent = (basewidth / float(img.size[0]))
hsize = int((float(img.size[1]) * float(wpercent)))
img = img.resize((basewidth, hsize), Image.ANTIALIAS)
im_io = BytesIO()
img.save(im_io, 'PNG', quality=70)
img, filetype = handle_image(request, BytesIO(response.content))
recipe.image = File(
im_io, name=f'{uuid.uuid4()}_{recipe.pk}.png'
img, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}'
)
recipe.save()
except UnidentifiedImageError:
except UnidentifiedImageError as e:
print(e)
pass
except MissingSchema:
except MissingSchema as e:
print(e)
pass
except Exception as e:
print(e)
pass
return HttpResponse(reverse('view_recipe', args=[recipe.pk]))

View File

@ -41,7 +41,7 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
obj.space = self.request.space
obj.internal = True
obj.save()
obj.steps.add(Step.objects.create())
obj.steps.add(Step.objects.create(space=self.request.space))
return HttpResponseRedirect(reverse('edit_recipe', kwargs={'pk': obj.pk}))
def get_success_url(self):

View File

@ -13,7 +13,7 @@ from django.contrib.auth.models import Group
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.db.models import Avg, Q, Sum
from django.http import HttpResponseRedirect
from django.http import HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse, reverse_lazy
from django.utils import timezone
@ -26,11 +26,13 @@ from cookbook.filters import RecipeFilter
from cookbook.forms import (CommentForm, Recipe, User,
UserCreateForm, UserNameForm, UserPreference,
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm,
SearchPreferenceForm, AllAuthSignupForm)
SearchPreferenceForm, AllAuthSignupForm,
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm, AllAuthSignupForm)
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
RecipeBook, RecipeBookEntry, ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit,
Food, UserFile)
Food, UserFile, ShareLink)
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
ViewLogTable, InviteLinkTable)
from cookbook.views.data import Object
@ -116,15 +118,17 @@ def no_space(request):
created_space = Space.objects.create(
name=create_form.cleaned_data['name'],
created_by=request.user,
allow_files=settings.SPACE_DEFAULT_FILES,
max_file_storage_mb=settings.SPACE_DEFAULT_MAX_FILES,
max_recipes=settings.SPACE_DEFAULT_MAX_RECIPES,
max_users=settings.SPACE_DEFAULT_MAX_USERS,
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
)
request.user.userpreference.space = created_space
request.user.userpreference.save()
request.user.groups.add(Group.objects.filter(name='admin').get())
messages.add_message(request, messages.SUCCESS, _('You have successfully created your own recipe space. Start by adding some recipes or invite other people to join you.'))
messages.add_message(request, messages.SUCCESS,
_('You have successfully created your own recipe space. Start by adding some recipes or invite other people to join you.'))
return HttpResponseRedirect(reverse('index'))
if join_form.is_valid():
@ -241,10 +245,12 @@ def supermarket(request):
@group_required('user')
def files(request):
try:
current_file_size_mb = UserFile.objects.filter(space=request.space).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
current_file_size_mb = UserFile.objects.filter(space=request.space).aggregate(Sum('file_size_kb'))[
'file_size_kb__sum'] / 1000
except TypeError:
current_file_size_mb = 0
return render(request, 'files.html', {'current_file_size_mb': current_file_size_mb, 'max_file_size_mb': request.space.max_file_storage_mb})
return render(request, 'files.html',
{'current_file_size_mb': current_file_size_mb, 'max_file_size_mb': request.space.max_file_storage_mb})
@group_required('user')
@ -266,7 +272,9 @@ def meal_plan_entry(request, pk):
@group_required('user')
def latest_shopping_list(request):
sl = ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).filter(finished=False, pace=request.space).order_by('-created_at').first()
sl = ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).filter(finished=False,
space=request.space).order_by(
'-created_at').first()
if sl:
return HttpResponseRedirect(reverse('view_shopping', kwargs={'pk': sl.pk}) + '?edit=true')
@ -303,8 +311,6 @@ def user_settings(request):
active_tab = 'account'
user_name_form = UserNameForm(instance=request.user)
password_form = PasswordChangeForm(request.user)
password_form.fields['old_password'].widget.attrs.pop("autofocus", None)
if request.method == "POST":
if 'preference_form' in request.POST:
@ -400,7 +406,6 @@ def user_settings(request):
return render(request, 'settings.html', {
'preference_form': preference_form,
'user_name_form': user_name_form,
'password_form': password_form,
'api_token': api_token,
'search_form': search_form,
'active_tab': active_tab
@ -490,7 +495,8 @@ def invite_link(request, token):
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
if request.user.is_authenticated:
if request.user.userpreference.space:
messages.add_message(request, messages.WARNING, _('You are already member of a space and therefore cannot join this one.'))
messages.add_message(request, messages.WARNING,
_('You are already member of a space and therefore cannot join this one.'))
return HttpResponseRedirect(reverse('index'))
link.used_by = request.user
@ -533,7 +539,8 @@ def space(request):
counts.recipes_no_keyword = Recipe.objects.filter(keywords=None, space=request.space).count()
invite_links = InviteLinkTable(InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=request.space).all())
invite_links = InviteLinkTable(
InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=request.space).all())
RequestConfig(request, paginate={'per_page': 25}).configure(invite_links)
return render(request, 'space.html', {'space_users': space_users, 'counts': counts, 'invite_links': invite_links})
@ -567,6 +574,19 @@ def space_change_member(request, user_id, space_id, group):
return HttpResponseRedirect(reverse('view_space'))
def report_share_abuse(request, token):
if not settings.SHARING_ABUSE:
messages.add_message(request, messages.WARNING,
_('Reporting share links is not enabled for this instance. Please notify the page administrator to report problems.'))
else:
if link := ShareLink.objects.filter(uuid=token).first():
link.abuse_blocked = True
link.save()
messages.add_message(request, messages.WARNING,
_('Recipe sharing link has been disabled! For additional information please contact the page administrator.'))
return HttpResponseRedirect(reverse('index'))
def markdown_info(request):
return render(request, 'markdown_info.html', {})
@ -587,6 +607,7 @@ def offline(request):
def test(request):
if not settings.DEBUG:
return HttpResponseRedirect(reverse('index'))
return JsonResponse(parse('Pane (raffermo o secco) 80 g'), safe=False)
def test2(request):

View File

@ -172,6 +172,7 @@ This zip file can simply be imported into Tandoor.
OpenEats does not provide any way to export the data using the interface. Luckily it is relatively easy to export it from the command line.
You need to run the command `python manage.py dumpdata recipe ingredient` inside of the application api container.
If you followed the default installation method you can use the following command `docker-compose -f docker-prod.yml run --rm --entrypoint 'sh' api ./manage.py dumpdata recipe ingredient`.
This command might also work `docker exec -it openeats_api_1 ./manage.py dumpdata recipe ingredient > recipe_ingredients.json`
Store the outputted json string in a `.json` file and simply import it using the importer. The file should look something like this
```json

View File

@ -36,7 +36,7 @@
</p>
<p align="center">
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a>
<a href="https://docs.tandoor.dev/install/docker.html" rel="noopener noreferrer">Installation</a>
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Documentation</a>
<a href="https://app.tandoor.dev/" target="_blank" rel="noopener noreferrer">Demo</a>
</p>
@ -88,4 +88,4 @@ Over the time tons of features have been added making this the most comprehensiv
I am just a single developer with many other interests and obligations so development and support might be slow at times,
but I try my best to constantly improve this application.
If you have any wishes, feature requests, problems or ideas feel free to open an issue on GitHub.
If you have any wishes, feature requests, problems or ideas feel free to open an issue on GitHub.

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,42 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,42 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr "Englisch"
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr "Deutsch"
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,42 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,42 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,42 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,42 +17,42 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,42 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -19,42 +19,42 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : "
"2);\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,42 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,42 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,42 +17,42 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,42 +18,42 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,42 +17,42 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:220
#: .\recipes\settings.py:303
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:221
#: .\recipes\settings.py:304
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:222
#: .\recipes\settings.py:305
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:223
#: .\recipes\settings.py:306
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:224
#: .\recipes\settings.py:307
msgid "English"
msgstr ""
#: .\recipes\settings.py:225
#: .\recipes\settings.py:308
msgid "French"
msgstr ""
#: .\recipes\settings.py:226
#: .\recipes\settings.py:309
msgid "German"
msgstr ""
#: .\recipes\settings.py:227
#: .\recipes\settings.py:310
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:228
#: .\recipes\settings.py:311
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:229
#: .\recipes\settings.py:312
msgid "Spanish"
msgstr ""

View File

@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.0/ref/settings/
"""
import ast
import json
import os
import re
@ -32,7 +33,8 @@ SOCIAL_DEFAULT_GROUP = os.getenv('SOCIAL_DEFAULT_GROUP', 'guest')
SPACE_DEFAULT_MAX_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0))
SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0))
SPACE_DEFAULT_FILES = bool(int(os.getenv('SPACE_DEFAULT_FILES', True)))
SPACE_DEFAULT_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 0))
SPACE_DEFAULT_ALLOW_SHARING = bool(int(os.getenv('SPACE_DEFAULT_ALLOW_SHARING', True)))
INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
@ -66,6 +68,9 @@ DJANGO_TABLES2_PAGE_RANGE = 8
HCAPTCHA_SITEKEY = os.getenv('HCAPTCHA_SITEKEY', '')
HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
SHARING_ABUSE = bool(int(os.getenv('SHARING_ABUSE', False)))
SHARING_LIMIT = int(os.getenv('SHARING_LIMIT', 0))
ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm'
TERMS_URL = os.getenv('TERMS_URL', '')
@ -111,15 +116,24 @@ INSTALLED_APPS = [
]
SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split(',') if os.getenv('SOCIAL_PROVIDERS') else []
SOCIALACCOUNT_EMAIL_VERIFICATION = 'none'
INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS
ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE = True
ACCOUNT_MAX_EMAIL_ADDRESSES = 3
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 90
ACCOUNT_LOGOUT_ON_GET = True
SOCIALACCOUNT_PROVIDERS = ast.literal_eval(
os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
try:
SOCIALACCOUNT_PROVIDERS = ast.literal_eval(
os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
except ValueError:
SOCIALACCOUNT_PROVIDERS = json.loads(
os.getenv('SOCIALACCOUNT_PROVIDERS').replace("'", '"') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
SESSION_COOKIE_DOMAIN = os.getenv('SESSION_COOKIE_DOMAIN', None)
SESSION_COOKIE_NAME = os.getenv('SESSION_COOKIE_NAME', 'sessionid')
ENABLE_SIGNUP = bool(int(os.getenv('ENABLE_SIGNUP', False)))

View File

@ -3,7 +3,7 @@ cryptography==3.4.7
django-annoying==0.10.6
django-autocomplete-light==3.8.2
django-cleanup==5.2.0
django-crispy-forms==1.11.2
django-crispy-forms==1.12.0
django-emoji-picker==0.0.6
django-filter==2.4.0
django-tables2==2.4.0
@ -15,8 +15,8 @@ gunicorn==20.1.0
lxml==4.6.3
Markdown==3.3.4
Pillow==8.2.0
psycopg2-binary==2.8.6
python-dotenv==0.17.1
psycopg2-binary==2.9.1
python-dotenv==0.18.0
requests==2.25.1
simplejson==3.17.2
six==1.16.0
@ -28,16 +28,16 @@ uritemplate==3.0.1
beautifulsoup4==4.9.3
microdata==0.7.1
Jinja2==3.0.1
django-webpack-loader==1.0.0
django-webpack-loader==1.1.0
django-js-reverse==0.9.1
django-allauth==0.44.0
recipe-scrapers==13.2.7
recipe-scrapers==13.3.0
django-scopes==1.2.0
pytest==6.2.4
pytest-django==4.4.0
django-cors-headers==3.7.0
django-treebeard==4.5.1
django-storages==1.11.1
boto3==1.17.90
boto3==1.17.102
django-prometheus==2.1.0
django-hCaptcha==0.1.0

View File

@ -44,7 +44,7 @@
"eslint-plugin-vue": "^7.10.0",
"typescript": "~4.3.2",
"vue-cli-plugin-i18n": "^2.1.1",
"webpack-bundle-tracker": "1.0.0",
"webpack-bundle-tracker": "1.1.0",
"workbox-expiration": "^6.0.2",
"workbox-navigation-preload": "^6.0.2",
"workbox-precaching": "^6.0.2",

View File

@ -1,54 +1,42 @@
<template>
<div id="app">
<div class="row">
<div class="col col-md-12">
<h2>{{ $t('Import') }}</h2>
</div>
</div>
<br/>
<br/>
<template v-if="import_info !== undefined">
<template v-if="import_info.running" style="text-align: center;">
<div class="row">
<div class="col col-md-12">
</div>
</div>
<loading-spinner></loading-spinner>
<br/>
<br/>
<template v-if="import_info.running">
<h5 style="text-align: center">{{ $t('Importing') }}...</h5>
<b-progress :max="import_info.total_recipes">
<b-progress-bar :value="import_info.imported_recipes" :label="`${import_info.imported_recipes}/${import_info.total_recipes}`"></b-progress-bar>
</b-progress>
<loading-spinner :size="25"></loading-spinner>
</template>
<template v-else>
<div class="row">
<div class="col col-md-12">
<span>{{ $t('Import_finished') }}! </span>
<a :href="`${resolveDjangoUrl('view_search') }?keyword=${import_info.keyword.id}`"
v-if="import_info.keyword !== null">{{ $t('View_Recipes') }}</a>
</div>
<div class="row">
<div class="col col-md-12" v-if="!import_info.running">
<span>{{ $t('Import_finished') }}! </span>
<a :href="`${resolveDjangoUrl('view_search') }?keyword=${import_info.keyword.id}`"
v-if="import_info.keyword !== null">{{ $t('View_Recipes') }}</a>
</div>
</div>
<br/>
<br/>
<div class="row">
<div class="col col-md-12">
<label for="id_textarea">{{ $t('Information') }}</label>
<textarea id="id_textarea" class="form-control" style="height: 50vh" v-html="import_info.msg"
disabled></textarea>
<div class="row">
<div class="col col-md-12">
<label for="id_textarea">{{ $t('Information') }}</label>
<textarea id="id_textarea" ref="output_text" class="form-control" style="height: 50vh"
v-html="import_info.msg"
disabled></textarea>
</div>
</div>
</template>
</div>
<br/>
<br/>
</template>
</div>
@ -90,6 +78,8 @@ export default {
setInterval(() => {
if ((this.import_id !== null) && window.navigator.onLine && this.import_info.running) {
this.refreshData()
let el = this.$refs.output_text
el.scrollTop = el.scrollHeight;
}
}, 5000)
@ -100,6 +90,7 @@ export default {
apiClient.retrieveImportLog(this.import_id).then(result => {
this.import_info = result.data
})
}
}

View File

@ -15,7 +15,9 @@
<b-input-group class="mt-3">
<b-input class="form-control" v-model="settings.search_input" v-bind:placeholder="$t('Search')"></b-input>
<b-input-group-append>
<b-button v-b-toggle.collapse_advanced_search variant="primary" class="shadow-none"><i
<b-button v-b-toggle.collapse_advanced_search
v-bind:class="{'btn-primary': !isAdvancedSettingsSet(), 'btn-danger': isAdvancedSettingsSet()}"
class="shadow-none btn"><i
class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i><i class="fas fa-caret-up"
v-if="settings.advanced_search_visible"></i>
</b-button>
@ -37,21 +39,16 @@
:href="resolveDjangoUrl('data_import_url')">{{ $t('Import') }}</a>
</div>
<div class="col-md-3" style="margin-top: 1vh">
<button class="btn btn-primary btn-block text-uppercase" @click="resetSearch">
{{ $t('Reset_Search') }}
<button class="btn btn-block text-uppercase" v-b-tooltip.hover :title="$t('show_only_internal')"
v-bind:class="{'btn-success':settings.search_internal, 'btn-primary':!settings.search_internal}"
@click="settings.search_internal = !settings.search_internal;refreshData()">
{{ $t('Internal') }}
</button>
</div>
<div class="col-md-2" style="position: relative; margin-top: 1vh">
<b-form-checkbox v-model="settings.search_internal" name="check-button"
@change="refreshData(false)"
class="shadow-none"
style="position:relative;top: 50%; transform: translateY(-50%);" switch>
{{ $t('show_only_internal') }}
</b-form-checkbox>
</div>
<div class="col-md-1" style="position: relative; margin-top: 1vh">
<button id="id_settings_button" class="btn btn-primary btn-block"><i class="fas fa-cog"></i>
<div class="col-md-3" style="position: relative; margin-top: 1vh">
<button id="id_settings_button" class="btn btn-primary btn-block text-uppercase"><i
class="fas fa-cog"></i>
</button>
</div>
@ -181,7 +178,16 @@
</div>
</div>
<div class="row" style="margin-top: 2vh">
<div class="row">
<div class="col col-md-12 text-right" style="margin-top: 2vh">
<span class="text-muted">
{{ $t('Page') }} {{ settings.pagination_page }}/{{ pagination_count }} <a href="#" @click="resetSearch"><i
class="fas fa-times-circle"></i> {{ $t('Reset') }}</a>
</span>
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
@ -200,10 +206,16 @@
</div>
</div>
<div class="row" style="margin-top: 2vh; text-align: center">
<div class="row" style="margin-top: 2vh">
<div class="col col-md-12">
<b-button @click="loadMore()" class="btn-block btn-success" v-if="pagination_more">{{ $t('Load_More') }}
</b-button>
<b-pagination pills
v-model="settings.pagination_page"
:total-rows="pagination_count"
per-page="25"
@change="pageChange"
align="center">
</b-pagination>
</div>
</div>
@ -239,6 +251,8 @@ import GenericMultiselect from "@/components/GenericMultiselect";
Vue.use(BootstrapVue)
let SETTINGS_COOKIE_NAME = 'search_settings'
export default {
name: 'RecipeSearchView',
mixins: [ResolveUrlMixin],
@ -249,6 +263,7 @@ export default {
meal_plans: [],
last_viewed_recipes: [],
settings_loaded: false,
settings: {
search_input: '',
search_internal: false,
@ -262,19 +277,33 @@ export default {
advanced_search_visible: false,
show_meal_plan: true,
recently_viewed: 5,
pagination_page: 1,
},
pagination_more: true,
pagination_page: 1,
pagination_count: 0,
}
},
mounted() {
this.$nextTick(function () {
if (this.$cookies.isKey('search_settings_v2')) {
this.settings = this.$cookies.get("search_settings_v2")
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
let cookie_val = this.$cookies.get(SETTINGS_COOKIE_NAME)
for (let i of Object.keys(cookie_val)) {
this.$set(this.settings, i, cookie_val[i])
}
//TODO i have no idea why the above code does not suffice to update the
//TODO pagination UI element as $set should update all values reactively but it does not
setTimeout(function () {
this.$set(this.settings, 'pagination_page', 0)
}.bind(this), 50)
setTimeout(function () {
this.$set(this.settings, 'pagination_page', cookie_val['pagination_page'])
}.bind(this), 51)
}
let urlParams = new URLSearchParams(window.location.search);
let apiClient = new ApiApiFactory()
@ -284,7 +313,7 @@ export default {
let keyword = {id: x, name: 'loading'}
this.settings.search_keywords.push(keyword)
apiClient.retrieveKeyword(x).then(result => {
this.$set(this.settings.search_keywords,this.settings.search_keywords.indexOf(keyword), result.data)
this.$set(this.settings.search_keywords, this.settings.search_keywords.indexOf(keyword), result.data)
})
}
}
@ -299,7 +328,7 @@ export default {
watch: {
settings: {
handler() {
this.$cookies.set("search_settings_v2", this.settings, -1)
this.$cookies.set(SETTINGS_COOKIE_NAME, this.settings, -1)
},
deep: true
},
@ -333,16 +362,11 @@ export default {
this.settings.search_internal,
undefined,
this.pagination_page,
this.settings.pagination_page,
).then(result => {
this.pagination_more = (result.data.next !== null)
if (page_load) {
for (let x of result.data.results) {
this.recipes.push(x)
}
} else {
this.recipes = result.data.results
}
window.scrollTo(0, 0);
this.pagination_count = result.data.count
this.recipes = result.data.results
})
},
loadMealPlan: function () {
@ -384,11 +408,15 @@ export default {
this.settings.search_keywords = []
this.settings.search_foods = []
this.settings.search_books = []
this.settings.pagination_page = 1
this.refreshData(false)
},
loadMore: function (page) {
this.pagination_page++
this.refreshData(true)
pageChange: function (page) {
this.settings.pagination_page = page
this.refreshData()
},
isAdvancedSettingsSet() {
return ((this.settings.search_keywords.length + this.settings.search_foods.length + this.settings.search_books.length) > 0)
}
}
}

Some files were not shown because too many files have changed in this diff Show More