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 # 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_RECIPES=0 # 0=unlimited recipes
# SPACE_DEFAULT_MAX_USERS=0 # 0=unlimited users per space # 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) # allow people to create accounts on your application instance (without an invite link)
# when unset: 0 (false) # when unset: 0 (false)
@ -114,3 +115,8 @@ REVERSE_PROXY_AUTH=0
# if SOCIAL_DEFAULT_ACCESS is used, which group should be added # 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> <h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
<p align="center"> <p align="center">
<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>
<img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=develop" > <a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
<img src="https://img.shields.io/github/stars/vabene1111/recipes" > <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>
<img src="https://img.shields.io/github/forks/vabene1111/recipes" > <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>
<img src="https://img.shields.io/docker/pulls/vabene1111/recipes" > <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>
<p align="center"> <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://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> </p>
![Preview](docs/preview.png) ![Preview](docs/preview.png)

View File

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

View File

@ -7,6 +7,8 @@ from django.contrib import messages
from django.core.cache import caches from django.core.cache import caches
from gettext import gettext as _ from gettext import gettext as _
from cookbook.models import InviteLink
class AllAuthCustomAdapter(DefaultAccountAdapter): class AllAuthCustomAdapter(DefaultAccountAdapter):
@ -14,7 +16,11 @@ class AllAuthCustomAdapter(DefaultAccountAdapter):
""" """
Whether to allow sign ups. 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 return False
else: else:
return super(AllAuthCustomAdapter, self).is_open_for_signup(request) 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 string
import unicodedata import unicodedata
@ -22,20 +23,16 @@ def parse_fraction(x):
def parse_amount(x): def parse_amount(x):
amount = 0 amount = 0
unit = '' unit = ''
note = ''
did_check_frac = False did_check_frac = False
end = 0 end = 0
while ( while (end < len(x) and (x[end] in string.digits
end < len(x) or (
and ( (x[end] == '.' or x[end] == ',' or x[end] == '/')
x[end] in string.digits and end + 1 < len(x)
or ( and x[end + 1] in string.digits
(x[end] == '.' or x[end] == ',' or x[end] == '/') ))):
and end + 1 < len(x)
and x[end + 1] in string.digits
)
)
):
end += 1 end += 1
if end > 0: if end > 0:
if "/" in x[:end]: if "/" in x[:end]:
@ -55,7 +52,11 @@ def parse_amount(x):
unit = x[end + 1:] unit = x[end + 1:]
except ValueError: except ValueError:
unit = x[end:] 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): def parse_ingredient_with_comma(tokens):
@ -106,6 +107,13 @@ def parse(x):
unit = '' unit = ''
ingredient = '' ingredient = ''
note = '' 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() tokens = x.split()
if len(tokens) == 1: if len(tokens) == 1:
@ -114,17 +122,17 @@ def parse(x):
else: else:
try: try:
# try to parse first argument as amount # 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 # 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 # three arguments if it already has a unit there can't be
# a fraction for the amount # a fraction for the amount
if len(tokens) > 2: if len(tokens) > 2:
try: try:
if not unit == '': 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 # 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 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]) amount += parse_fraction(tokens[1])
# assume that units can't end with a comma # assume that units can't end with a comma
if len(tokens) > 3 and not tokens[2].endswith(','): 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 to use second argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
try: try:
ingredient, note = parse_ingredient(tokens[2:]) ingredient, note = parse_ingredient(tokens[2:])
unit = tokens[1] if unit == '':
unit = tokens[1]
else:
note = tokens[1]
except ValueError: except ValueError:
ingredient, note = parse_ingredient(tokens[1:]) ingredient, note = parse_ingredient(tokens[1:])
else: else:
@ -158,11 +169,16 @@ def parse(x):
ingredient, note = parse_ingredient(tokens) ingredient, note = parse_ingredient(tokens)
except ValueError: except ValueError:
ingredient = ' '.join(tokens[1:]) ingredient = ' '.join(tokens[1:])
if unit_note not in note:
note += ' ' + unit_note
return amount, unit.strip(), ingredient.strip(), note.strip() return amount, unit.strip(), ingredient.strip(), note.strip()
# small utility functions to prevent emtpy unit/food creation # small utility functions to prevent emtpy unit/food creation
def get_unit(unit, space): def get_unit(unit, space):
if not unit:
return None
if len(unit) > 0: if len(unit) > 0:
u, created = Unit.objects.get_or_create(name=unit, space=space) u, created = Unit.objects.get_or_create(name=unit, space=space)
return u return u
@ -170,6 +186,8 @@ def get_unit(unit, space):
def get_food(food, space): def get_food(food, space):
if not food:
return None
if len(food) > 0: if len(food) > 0:
f, created = Food.objects.get_or_create(name=food, space=space) f, created = Food.objects.get_or_create(name=food, space=space)
return f return f

View File

@ -1,6 +1,8 @@
""" """
Source: https://djangosnippets.org/snippets/1703/ 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.detail import SingleObjectTemplateResponseMixin
from django.views.generic.edit import ModelFormMixin 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 :return: true if a share link with the given recipe and uuid exists
""" """
try: 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: except ValidationError:
return False return False
@ -121,15 +134,18 @@ class GroupRequiredMixin(object):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not has_group_permission(request.user, self.groups_required): if not has_group_permission(request.user, self.groups_required):
if not request.user.is_authenticated: 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) return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
else: 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')) return HttpResponseRedirect(reverse_lazy('index'))
try: try:
obj = self.get_object() obj = self.get_object()
if obj.get_space() != request.space: 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')) return HttpResponseRedirect(reverse_lazy('index'))
except AttributeError: except AttributeError:
pass pass
@ -141,17 +157,20 @@ class OwnerRequiredMixin(object):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated: 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) return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
else: else:
if not is_object_owner(request.user, self.get_object()): 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')) return HttpResponseRedirect(reverse('index'))
try: try:
obj = self.get_object() obj = self.get_object()
if obj.get_space() != request.space: 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')) return HttpResponseRedirect(reverse_lazy('index'))
except AttributeError: except AttributeError:
pass 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) recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space)
step = Step.objects.create( step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n' instruction='\n'.join(directions) + '\n\n', space=self.request.space,
) )
for ingredient in ingredients: for ingredient in ingredients:
@ -49,7 +49,7 @@ class Pepperplate(Integration):
f = get_food(ingredient, self.request.space) f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space) u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create( 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) 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, ) 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 != '': if source_url != '':
step.instruction += '\n' + source_url step.instruction += '\n' + source_url
@ -50,7 +50,7 @@ class ChefTap(Integration):
f = get_food(ingredient, self.request.space) f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space) u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create( 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) recipe.steps.add(step)

View File

@ -3,6 +3,7 @@ import re
from io import BytesIO from io import BytesIO
from zipfile import ZipFile 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.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
@ -54,7 +55,7 @@ class Chowdown(Integration):
recipe.keywords.add(keyword) recipe.keywords.add(keyword)
step = Step.objects.create( 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: for ingredient in ingredients:
@ -62,7 +63,7 @@ class Chowdown(Integration):
f = get_food(ingredient, self.request.space) f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space) u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create( 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) recipe.steps.add(step)
@ -71,7 +72,7 @@ class Chowdown(Integration):
import_zip = ZipFile(f['file']) import_zip = ZipFile(f['file'])
for z in import_zip.filelist: for z in import_zip.filelist:
if re.match(f'^images/{image}$', z.filename): 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 return recipe

View File

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

View File

@ -28,7 +28,7 @@ class Domestica(Integration):
recipe.save() recipe.save()
step = Step.objects.create( step = Step.objects.create(
instruction=file['directions'] instruction=file['directions'], space=self.request.space,
) )
if file['source'] != '': if file['source'] != '':
@ -40,12 +40,12 @@ class Domestica(Integration):
f = get_food(ingredient, self.request.space) f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space) u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create( 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) recipe.steps.add(step)
if file['image'] != '': 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 return recipe

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,7 +55,7 @@ class Paprika(Integration):
pass pass
step = Step.objects.create( step = Step.objects.create(
instruction=instructions instruction=instructions, space=self.request.space,
) )
if len(recipe_json['description'].strip()) > 500: if len(recipe_json['description'].strip()) > 500:
@ -73,7 +73,7 @@ class Paprika(Integration):
f = get_food(ingredient, self.request.space) f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space) u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create( 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: except AttributeError:
pass pass
@ -81,6 +81,6 @@ class Paprika(Integration):
recipe.steps.add(step) recipe.steps.add(step)
if recipe_json.get("photo_data", None): 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 return recipe

View File

@ -7,6 +7,7 @@ from zipfile import ZipFile
import imghdr import imghdr
from django.utils.translation import gettext as _ 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.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.integration.integration import Integration from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
@ -44,7 +45,7 @@ class RecetteTek(Integration):
if not instructions: if not instructions:
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) # Append the original import url to the step (if it exists)
try: try:
@ -62,7 +63,7 @@ class RecetteTek(Integration):
f = get_food(ingredient, self.request.space) f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space) u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create( 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: except Exception as e:
print(recipe.name, ': failed to parse recipe ingredients ', str(e)) print(recipe.name, ': failed to parse recipe ingredients ', str(e))
@ -113,17 +114,17 @@ class RecetteTek(Integration):
# Import the original image from the zip file, if we cannot do that, attempt to download it again. # Import the original image from the zip file, if we cannot do that, attempt to download it again.
try: try:
if file['pictures'][0] !='': if file['pictures'][0] != '':
image_file_name = file['pictures'][0].split('/')[-1] image_file_name = file['pictures'][0].split('/')[-1]
for f in self.files: for f in self.files:
if '.rtk' in f['name']: if '.rtk' in f['name']:
import_zip = ZipFile(f['file']) 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: else:
if file['originalPicture'] != '': if file['originalPicture'] != '':
response=requests.get(file['originalPicture']) response = requests.get(file['originalPicture'])
if imghdr.what(BytesIO(response.content)) != None: if imghdr.what(BytesIO(response.content)) != None:
self.import_recipe_image(recipe, BytesIO(response.content)) self.import_recipe_image(recipe, BytesIO(response.content), filetype=get_filetype(file['originalPicture']))
else: else:
raise Exception("Original image failed to download.") raise Exception("Original image failed to download.")
except Exception as e: except Exception as e:

View File

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

View File

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

View File

@ -43,7 +43,7 @@ class RezKonv(Integration):
recipe.keywords.add(keyword) recipe.keywords.add(keyword)
step = Step.objects.create( step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n' instruction='\n'.join(directions) + '\n\n', space=self.request.space,
) )
for ingredient in ingredients: for ingredient in ingredients:
@ -52,7 +52,7 @@ class RezKonv(Integration):
f = get_food(ingredient, self.request.space) f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space) u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create( 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) 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, ) 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: for ingredient in ingredients:
amount, unit, ingredient, note = parse(ingredient) amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space) f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space) u = get_unit(unit, self.request.space)
step.ingredients.add(Ingredient.objects.create( 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) 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): class Space(ExportModelOperationsMixin('space'), models.Model):
name = models.CharField(max_length=128, default='Default') name = models.CharField(max_length=128, default='Default')
created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True) 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) message = models.CharField(max_length=512, default='', blank=True)
max_recipes = models.IntegerField(default=0) 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_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) max_users = models.IntegerField(default=0)
allow_sharing = models.BooleanField(default=True)
demo = models.BooleanField(default=False) demo = models.BooleanField(default=False)
def __str__(self): def __str__(self):
@ -157,6 +159,7 @@ class UserPreference(models.Model, PermissionModelMixin):
shopping_auto_sync = models.IntegerField(default=5) shopping_auto_sync = models.IntegerField(default=5)
sticky_navbar = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT) 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) space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
objects = ScopedManager(space='space') objects = ScopedManager(space='space')
@ -388,14 +391,8 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
no_amount = models.BooleanField(default=False) no_amount = models.BooleanField(default=False)
order = models.IntegerField(default=0) order = models.IntegerField(default=0)
objects = ScopedManager(space='step__recipe__space') space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@staticmethod
def get_space_key():
return 'step', 'recipe', 'space'
def get_space(self):
return self.step_set.first().recipe_set.first().space
def __str__(self): def __str__(self):
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food) 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) show_as_header = models.BooleanField(default=True)
search_vector = SearchVectorField(null=True) search_vector = SearchVectorField(null=True)
objects = ScopedManager(space='recipe__space') space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@staticmethod
def get_space_key():
return 'recipe', 'space'
def get_space(self):
return self.recipe_set.first().space
def get_instruction_render(self): def get_instruction_render(self):
from cookbook.helper.template_helper import render_instructions 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 max_length=512, default="", null=True, blank=True
) )
objects = ScopedManager(space='recipe__space') space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@staticmethod
def get_space_key():
return 'recipe', 'space'
def get_space(self):
return self.recipe_set.first().space
def __str__(self): def __str__(self):
return 'Nutrition' return f'Nutrition {self.pk}'
class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModelMixin): 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): class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE) recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
uuid = models.UUIDField(default=uuid.uuid4) 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_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
@ -773,6 +760,10 @@ class ImportLog(models.Model, PermissionModelMixin):
running = models.BooleanField(default=True) running = models.BooleanField(default=True)
msg = models.TextField(default="") msg = models.TextField(default="")
keyword = models.ForeignKey(Keyword, null=True, blank=True, on_delete=models.SET_NULL) 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_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE) 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 gettext import gettext as _
from django.contrib.auth.models import User 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, from drf_writable_nested import (UniqueFieldsMixin,
WritableNestedModelSerializer) WritableNestedModelSerializer)
from rest_framework import serializers from rest_framework import serializers
@ -301,6 +301,10 @@ class IngredientSerializer(WritableNestedModelSerializer):
unit = UnitSerializer(allow_null=True) unit = UnitSerializer(allow_null=True)
amount = CustomDecimalField() amount = CustomDecimalField()
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta: class Meta:
model = Ingredient model = Ingredient
fields = ( fields = (
@ -313,7 +317,11 @@ class StepSerializer(WritableNestedModelSerializer):
ingredients = IngredientSerializer(many=True) ingredients = IngredientSerializer(many=True)
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown') ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
ingredients_vue = serializers.SerializerMethodField('get_ingredients_vue') ingredients_vue = serializers.SerializerMethodField('get_ingredients_vue')
file = UserFileViewSerializer(allow_null=True) 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): def get_ingredients_vue(self, obj):
return obj.get_instruction_render() return obj.get_instruction_render()
@ -330,13 +338,34 @@ class StepSerializer(WritableNestedModelSerializer):
class NutritionInformationSerializer(serializers.ModelSerializer): class NutritionInformationSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta: class Meta:
model = NutritionInformation model = NutritionInformation
fields = ('id', 'carbohydrates', 'fats', 'proteins', 'calories', 'source') 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) keywords = KeywordLabelSerializer(many=True)
rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
def create(self, validated_data): def create(self, validated_data):
pass pass
@ -349,22 +378,24 @@ class RecipeOverviewSerializer(WritableNestedModelSerializer):
fields = ( fields = (
'id', 'name', 'description', 'image', 'keywords', 'working_time', 'id', 'name', 'description', 'image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at', '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'] read_only_fields = ['image', 'created_by', 'created_at']
class RecipeSerializer(WritableNestedModelSerializer): class RecipeSerializer(RecipeBaseSerializer):
nutrition = NutritionInformationSerializer(allow_null=True, required=False) nutrition = NutritionInformationSerializer(allow_null=True, required=False)
steps = StepSerializer(many=True) steps = StepSerializer(many=True)
keywords = KeywordSerializer(many=True) keywords = KeywordSerializer(many=True)
rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
class Meta: class Meta:
model = Recipe model = Recipe
fields = ( fields = (
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time', 'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at', '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'] read_only_fields = ['image', 'created_by', 'created_at']
@ -538,7 +569,7 @@ class ImportLogSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ImportLog 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',) read_only_fields = ('created_by',)
@ -596,6 +627,10 @@ class IngredientExportSerializer(WritableNestedModelSerializer):
unit = UnitExportSerializer(allow_null=True) unit = UnitExportSerializer(allow_null=True)
amount = CustomDecimalField() amount = CustomDecimalField()
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta: class Meta:
model = Ingredient model = Ingredient
fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount') fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount')
@ -604,6 +639,10 @@ class IngredientExportSerializer(WritableNestedModelSerializer):
class StepExportSerializer(WritableNestedModelSerializer): class StepExportSerializer(WritableNestedModelSerializer):
ingredients = IngredientExportSerializer(many=True) ingredients = IngredientExportSerializer(many=True)
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class Meta: class Meta:
model = Step model = Step
fields = ('name', 'type', 'instruction', 'ingredients', 'time', 'order', 'show_as_header') 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 box-shadow: none
} }
.modal-content {
width: 500px
}
@media (max-width: 575px) {
.modal-content {
width: 300px
}
}
.modal-content .modal-header { .modal-content .modal-header {
justify-content: center; justify-content: center;
border: none 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 title %}{% trans "E-mail Addresses" %}{% endblock %}
{% block content %} {% 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> <h3>{% trans "E-mail Addresses" %}</h3>
{% if user.emailaddress_set.all %} {% if user.emailaddress_set.all %}
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p> <p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>

View File

@ -44,13 +44,13 @@
{% if socialaccount_providers %} {% if socialaccount_providers %}
<div class="row" style="margin-top: 2vh"> <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> <h5>{% trans "Social Login" %}</h5>
<span>{% trans 'You can use any of the following providers to sign in.' %}</span> <span>{% trans 'You can use any of the following providers to sign in.' %}</span>
<br/> <br/>
<br/> <br/>
<ul class="socialaccount_providers"> <ul class="socialaccount_providers list-unstyled">
{% include "socialaccount/snippets/provider_list.html" with process="login" %} {% include "socialaccount/snippets/provider_list.html" with process="login" %}
</ul> </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> </button>
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %} {% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="/" aria-label="Tandoor"> <a class="navbar-brand p-0 me-2 justify-content-center" href="/" aria-label="Tandoor">
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;"> <img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
</a> </a>
{% endif %} {% endif %}
<div class="collapse navbar-collapse" id="navbarText"> <div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
@ -152,12 +152,18 @@
<a class="dropdown-item" href="{% url 'view_history' %}"><i <a class="dropdown-item" href="{% url 'view_history' %}"><i
class="fas fa-history"></i> {% trans 'History' %}</a> class="fas fa-history"></i> {% trans 'History' %}</a>
{% if request.user == request.space.created_by %} {% 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 %} {% 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 %} {% if user.is_superuser %}
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'view_system' %}"><i <a class="dropdown-item" href="{% url 'list_keyword' %}"><i
class="fas fa-server fa-fw"></i> {% trans 'System' %}</a> class="fas fa-tags"></i> {% trans 'Keywords' %}</a>
<a class="dropdown-item" href="{% url 'admin:index' %}"><i <a class="dropdown-item" href="{% url 'admin:index' %}"><i
class="fas fa-user-shield fa-fw"></i> {% trans 'Admin' %}</a> class="fas fa-user-shield fa-fw"></i> {% trans 'Admin' %}</a>
{% endif %} {% endif %}
@ -192,10 +198,7 @@
</div> </div>
{% endif %} {% endif %}
<br/> <div class="container-fluid mt-2 mt-md-5 mt-xl-5 mt-lg-5" id="id_base_container">
<br/>
<div class="container-fluid" id="id_base_container">
<div class="row"> <div class="row">
<div class="col-xl-2 d-none d-xl-block"> <div class="col-xl-2 d-none d-xl-block">
{% block content_xl_left %} {% block content_xl_left %}

View File

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

View File

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

View File

@ -12,9 +12,11 @@
{% block content %} {% block content %}
<h3> <nav aria-label="breadcrumb">
{% trans 'Settings' %} <ol class="breadcrumb">
</h3> <li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
</ol>
</nav>
<!-- Nav tabs --> <!-- Nav tabs -->
<ul class="nav nav-tabs" id="myTab" role="tablist" style="margin-bottom: 2vh"> <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> class="fas fa-save"></i> {% trans 'Save' %}</button>
</form> </form>
<h4>{% trans 'Password Settings' %}</h4> <h4>{% trans 'Account 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 '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> <a href="{% url 'socialaccount_connections' %}" class="btn btn-primary">{% trans 'Social' %}</a>
<br/><br/>
<h4>{% trans 'Social' %}</h4>
<a href="{% url 'socialaccount_connections' %}" class="btn btn-primary">{% trans 'Manage Social Accounts' %}</a>
<br/> <br/>
<br/> <br/>
<br/> <br/>

View File

@ -4,6 +4,14 @@
{% block head_title %}{% trans "Account Connections" %}{% endblock %} {% block head_title %}{% trans "Account Connections" %}{% endblock %}
{% block content %} {% 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> <h3>{% trans "Account Connections" %}</h3>
{% if form.accounts %} {% if form.accounts %}
@ -46,7 +54,7 @@
<h4>{% trans 'Add a 3rd Party Account' %}</h4> <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" %} {% include "socialaccount/snippets/provider_list.html" with process="connect" %}
</ul> </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 %} {% 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> <a href="https://tandoor.dev/manage">{% trans 'Manage Subscription' %}</a>{% endif %}</small></h3>
<br/> <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: if not user.is_authenticated:
return '' return ''
rating = recipe.cooklog_set \ rating = recipe.cooklog_set \
.filter(created_by=user, rating__gte=0) \ .filter(created_by=user, rating__gt=0) \
.aggregate(Avg('rating')) .aggregate(Avg('rating'))
if rating['rating__avg']: 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>' rating_stars = rating_stars + '<i class="fas fa-star fa-xs"></i>'
if rating['rating__avg'] % 1 >= 0.5: 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>' rating_stars += '</span>'

View File

@ -1,10 +1,11 @@
import json import json
import pytest import pytest
from django.db.models import Subquery, OuterRef
from django.urls import reverse from django.urls import reverse
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from cookbook.models import Ingredient from cookbook.models import Ingredient, Step
LIST_URL = 'api:ingredient-list' LIST_URL = 'api:ingredient-list'
DETAIL_URL = 'api:ingredient-detail' 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(): with scopes_disabled():
recipe_1_s1.space = space_2 recipe_1_s1.space = space_2
recipe_1_s1.save() 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_s1.get(reverse(LIST_URL)).content)) == 0
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 10 assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 10

View File

@ -1,10 +1,11 @@
import json import json
import pytest import pytest
from django.db.models import Subquery, OuterRef
from django.urls import reverse from django.urls import reverse
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from cookbook.models import Step from cookbook.models import Step, Ingredient
LIST_URL = 'api:step-list' LIST_URL = 'api:step-list'
DETAIL_URL = 'api:step-detail' 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(): with scopes_disabled():
recipe_1_s1.space = space_2 recipe_1_s1.space = space_2
recipe_1_s1.save() 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_s1.get(reverse(LIST_URL)).content)) == 0
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 2 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, internal=True,
) )
s1 = 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(), ) s2 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), space=space_1, )
r.steps.add(s1) r.steps.add(s1)
r.steps.add(s2) 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, ), food=Food.objects.create(name=uuid.uuid4(), space=space_1, ),
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ), unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
note=uuid.uuid4(), 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, ), food=Food.objects.create(name=uuid.uuid4(), space=space_1, ),
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ), unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
note=uuid.uuid4(), note=uuid.uuid4(),
space=space_1,
) )
) )

View File

@ -773,7 +773,7 @@ COOKPAD = {
"text": "Water", "text": "Water",
"id": 49092 "id": 49092
}, },
"note": "", "note": "2-3",
"original": "2-3 c Water" "original": "2-3 c Water"
}, },
{ {
@ -1498,10 +1498,10 @@ GIALLOZAFFERANO = {
"id": 64900 "id": 64900
}, },
"ingredient": { "ingredient": {
"text": "Pane (raffermo o secco) 80 g", "text": "Pane 80 g",
"id": 24720 "id": 24720
}, },
"note": "", "note": "raffermo o secco",
"original": "Pane (raffermo o secco) 80 g" "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", ""), "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", ""),
"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 # for German you could say that if an ingredient does not have
# an amount # and it starts with a lowercase letter, then that # 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('history/', views.history, name='view_history'),
path('supermarket/', views.supermarket, name='view_supermarket'), path('supermarket/', views.supermarket, name='view_supermarket'),
path('files/', views.files, name='view_files'), 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('test/', views.test, name='view_test'),
path('test2/', views.test2, name='view_test2'), 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/recipe-from-source/', api.recipe_from_source, name='api_recipe_from_source'),
path('api/backup/', api.get_backup, name='api_backup'), 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/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/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), 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.http import FileResponse, HttpResponse, JsonResponse
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from django.shortcuts import redirect, get_object_or_404 from django.shortcuts import redirect, get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from icalendar import Calendar, Event from icalendar import Calendar, Event
from recipe_scrapers import scrape_me, WebsiteNotImplementedError, NoSchemaFoundInWildMode 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 rest_framework.viewsets import ViewSetMixin
from treebeard.exceptions import PathOverflow, InvalidMoveToDescendant, InvalidPosition 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.ingredient_parser import parse
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
CustomIsOwner, CustomIsShare, CustomIsOwner, CustomIsShare,
@ -41,7 +43,7 @@ from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
MealType, Recipe, RecipeBook, ShoppingList, MealType, Recipe, RecipeBook, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step, ShoppingListEntry, ShoppingListRecipe, Step,
Storage, Sync, SyncLog, Unit, UserPreference, Storage, Sync, SyncLog, Unit, UserPreference,
ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile) ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile, ShareLink)
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@ -482,16 +484,9 @@ class RecipeViewSet(viewsets.ModelViewSet):
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
img = Image.open(obj.image)
basewidth = 720 img, filetype = handle_image(request, obj.image)
wpercent = (basewidth / float(img.size[0])) obj.image = File(img, name=f'{uuid.uuid4()}_{obj.pk}{filetype}')
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')
obj.save() obj.save()
return Response(serializer.data) return Response(serializer.data)
@ -669,6 +664,16 @@ def sync_all(request):
return redirect('list_recipe_import') 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') @group_required('user')
@ajax_request @ajax_request
def log_cooking(request, recipe_id): def log_cooking(request, recipe_id):

View File

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

View File

@ -41,7 +41,7 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
obj.space = self.request.space obj.space = self.request.space
obj.internal = True obj.internal = True
obj.save() 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})) return HttpResponseRedirect(reverse('edit_recipe', kwargs={'pk': obj.pk}))
def get_success_url(self): 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.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Avg, Q, Sum 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.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
@ -26,11 +26,13 @@ from cookbook.filters import RecipeFilter
from cookbook.forms import (CommentForm, Recipe, User, from cookbook.forms import (CommentForm, Recipe, User,
UserCreateForm, UserNameForm, UserPreference, UserCreateForm, UserNameForm, UserPreference,
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm, 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.helper.permission_helper import group_required, share_link_valid, has_group_permission
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan, from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
RecipeBook, RecipeBookEntry, ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit, RecipeBook, RecipeBookEntry, ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit,
Food, UserFile) Food, UserFile, ShareLink)
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall, from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
ViewLogTable, InviteLinkTable) ViewLogTable, InviteLinkTable)
from cookbook.views.data import Object from cookbook.views.data import Object
@ -116,15 +118,17 @@ def no_space(request):
created_space = Space.objects.create( created_space = Space.objects.create(
name=create_form.cleaned_data['name'], name=create_form.cleaned_data['name'],
created_by=request.user, 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_recipes=settings.SPACE_DEFAULT_MAX_RECIPES,
max_users=settings.SPACE_DEFAULT_MAX_USERS, max_users=settings.SPACE_DEFAULT_MAX_USERS,
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
) )
request.user.userpreference.space = created_space request.user.userpreference.space = created_space
request.user.userpreference.save() request.user.userpreference.save()
request.user.groups.add(Group.objects.filter(name='admin').get()) request.user.groups.add(Group.objects.filter(name='admin').get())
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')) return HttpResponseRedirect(reverse('index'))
if join_form.is_valid(): if join_form.is_valid():
@ -241,10 +245,12 @@ def supermarket(request):
@group_required('user') @group_required('user')
def files(request): def files(request):
try: 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: except TypeError:
current_file_size_mb = 0 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') @group_required('user')
@ -266,7 +272,9 @@ def meal_plan_entry(request, pk):
@group_required('user') @group_required('user')
def latest_shopping_list(request): 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: if sl:
return HttpResponseRedirect(reverse('view_shopping', kwargs={'pk': sl.pk}) + '?edit=true') return HttpResponseRedirect(reverse('view_shopping', kwargs={'pk': sl.pk}) + '?edit=true')
@ -303,8 +311,6 @@ def user_settings(request):
active_tab = 'account' active_tab = 'account'
user_name_form = UserNameForm(instance=request.user) 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 request.method == "POST":
if 'preference_form' in request.POST: if 'preference_form' in request.POST:
@ -400,7 +406,6 @@ def user_settings(request):
return render(request, 'settings.html', { return render(request, 'settings.html', {
'preference_form': preference_form, 'preference_form': preference_form,
'user_name_form': user_name_form, 'user_name_form': user_name_form,
'password_form': password_form,
'api_token': api_token, 'api_token': api_token,
'search_form': search_form, 'search_form': search_form,
'active_tab': active_tab '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 link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
if request.user.is_authenticated: if request.user.is_authenticated:
if request.user.userpreference.space: 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')) return HttpResponseRedirect(reverse('index'))
link.used_by = request.user 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() 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) RequestConfig(request, paginate={'per_page': 25}).configure(invite_links)
return render(request, 'space.html', {'space_users': space_users, 'counts': counts, 'invite_links': 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')) 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): def markdown_info(request):
return render(request, 'markdown_info.html', {}) return render(request, 'markdown_info.html', {})
@ -587,6 +607,7 @@ def offline(request):
def test(request): def test(request):
if not settings.DEBUG: if not settings.DEBUG:
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('index'))
return JsonResponse(parse('Pane (raffermo o secco) 80 g'), safe=False)
def test2(request): 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. 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. 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`. 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 Store the outputted json string in a `.json` file and simply import it using the importer. The file should look something like this
```json ```json

View File

@ -36,7 +36,7 @@
</p> </p>
<p align="center"> <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://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/" target="_blank" rel="noopener noreferrer">Demo</a>
</p> </p>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,42 +17,42 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:220 #: .\recipes\settings.py:303
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:221 #: .\recipes\settings.py:304
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:222 #: .\recipes\settings.py:305
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:223 #: .\recipes\settings.py:306
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:224 #: .\recipes\settings.py:307
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:225 #: .\recipes\settings.py:308
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:226 #: .\recipes\settings.py:309
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:227 #: .\recipes\settings.py:310
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:228 #: .\recipes\settings.py:311
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:229 #: .\recipes\settings.py:312
msgid "Spanish" msgid "Spanish"
msgstr "" 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/ https://docs.djangoproject.com/en/2.0/ref/settings/
""" """
import ast import ast
import json
import os import os
import re 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_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0))
SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 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'] 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_SITEKEY = os.getenv('HCAPTCHA_SITEKEY', '')
HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '') 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' ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm'
TERMS_URL = os.getenv('TERMS_URL', '') 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 [] SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split(',') if os.getenv('SOCIAL_PROVIDERS') else []
SOCIALACCOUNT_EMAIL_VERIFICATION = 'none'
INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS
ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE = True ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE = True
ACCOUNT_MAX_EMAIL_ADDRESSES = 3 ACCOUNT_MAX_EMAIL_ADDRESSES = 3
ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 90 ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 90
ACCOUNT_LOGOUT_ON_GET = True
SOCIALACCOUNT_PROVIDERS = ast.literal_eval( try:
os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}') 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))) 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-annoying==0.10.6
django-autocomplete-light==3.8.2 django-autocomplete-light==3.8.2
django-cleanup==5.2.0 django-cleanup==5.2.0
django-crispy-forms==1.11.2 django-crispy-forms==1.12.0
django-emoji-picker==0.0.6 django-emoji-picker==0.0.6
django-filter==2.4.0 django-filter==2.4.0
django-tables2==2.4.0 django-tables2==2.4.0
@ -15,8 +15,8 @@ gunicorn==20.1.0
lxml==4.6.3 lxml==4.6.3
Markdown==3.3.4 Markdown==3.3.4
Pillow==8.2.0 Pillow==8.2.0
psycopg2-binary==2.8.6 psycopg2-binary==2.9.1
python-dotenv==0.17.1 python-dotenv==0.18.0
requests==2.25.1 requests==2.25.1
simplejson==3.17.2 simplejson==3.17.2
six==1.16.0 six==1.16.0
@ -28,16 +28,16 @@ uritemplate==3.0.1
beautifulsoup4==4.9.3 beautifulsoup4==4.9.3
microdata==0.7.1 microdata==0.7.1
Jinja2==3.0.1 Jinja2==3.0.1
django-webpack-loader==1.0.0 django-webpack-loader==1.1.0
django-js-reverse==0.9.1 django-js-reverse==0.9.1
django-allauth==0.44.0 django-allauth==0.44.0
recipe-scrapers==13.2.7 recipe-scrapers==13.3.0
django-scopes==1.2.0 django-scopes==1.2.0
pytest==6.2.4 pytest==6.2.4
pytest-django==4.4.0 pytest-django==4.4.0
django-cors-headers==3.7.0 django-cors-headers==3.7.0
django-treebeard==4.5.1 django-treebeard==4.5.1
django-storages==1.11.1 django-storages==1.11.1
boto3==1.17.90 boto3==1.17.102
django-prometheus==2.1.0 django-prometheus==2.1.0
django-hCaptcha==0.1.0 django-hCaptcha==0.1.0

View File

@ -44,7 +44,7 @@
"eslint-plugin-vue": "^7.10.0", "eslint-plugin-vue": "^7.10.0",
"typescript": "~4.3.2", "typescript": "~4.3.2",
"vue-cli-plugin-i18n": "^2.1.1", "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-expiration": "^6.0.2",
"workbox-navigation-preload": "^6.0.2", "workbox-navigation-preload": "^6.0.2",
"workbox-precaching": "^6.0.2", "workbox-precaching": "^6.0.2",

View File

@ -1,54 +1,42 @@
<template> <template>
<div id="app"> <div id="app">
<div class="row">
<div class="col col-md-12">
<h2>{{ $t('Import') }}</h2>
</div>
</div>
<br/>
<br/> <br/>
<template v-if="import_info !== undefined"> <template v-if="import_info !== undefined">
<template v-if="import_info.running" style="text-align: center;"> <template v-if="import_info.running">
<div class="row">
<div class="col col-md-12">
</div>
</div>
<loading-spinner></loading-spinner>
<br/>
<br/>
<h5 style="text-align: center">{{ $t('Importing') }}...</h5> <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>
<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>
</div>
<br/> <br/>
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
<label for="id_textarea">{{ $t('Information') }}</label> <label for="id_textarea">{{ $t('Information') }}</label>
<textarea id="id_textarea" class="form-control" style="height: 50vh" v-html="import_info.msg" <textarea id="id_textarea" ref="output_text" class="form-control" style="height: 50vh"
disabled></textarea> v-html="import_info.msg"
disabled></textarea>
</div>
</div> </div>
</div>
<br/>
</template> <br/>
</template> </template>
</div> </div>
@ -90,6 +78,8 @@ export default {
setInterval(() => { setInterval(() => {
if ((this.import_id !== null) && window.navigator.onLine && this.import_info.running) { if ((this.import_id !== null) && window.navigator.onLine && this.import_info.running) {
this.refreshData() this.refreshData()
let el = this.$refs.output_text
el.scrollTop = el.scrollHeight;
} }
}, 5000) }, 5000)
@ -100,6 +90,7 @@ export default {
apiClient.retrieveImportLog(this.import_id).then(result => { apiClient.retrieveImportLog(this.import_id).then(result => {
this.import_info = result.data this.import_info = result.data
}) })
} }
} }

View File

@ -15,7 +15,9 @@
<b-input-group class="mt-3"> <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 class="form-control" v-model="settings.search_input" v-bind:placeholder="$t('Search')"></b-input>
<b-input-group-append> <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" 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> v-if="settings.advanced_search_visible"></i>
</b-button> </b-button>
@ -37,21 +39,16 @@
:href="resolveDjangoUrl('data_import_url')">{{ $t('Import') }}</a> :href="resolveDjangoUrl('data_import_url')">{{ $t('Import') }}</a>
</div> </div>
<div class="col-md-3" style="margin-top: 1vh"> <div class="col-md-3" style="margin-top: 1vh">
<button class="btn btn-primary btn-block text-uppercase" @click="resetSearch"> <button class="btn btn-block text-uppercase" v-b-tooltip.hover :title="$t('show_only_internal')"
{{ $t('Reset_Search') }} v-bind:class="{'btn-success':settings.search_internal, 'btn-primary':!settings.search_internal}"
@click="settings.search_internal = !settings.search_internal;refreshData()">
{{ $t('Internal') }}
</button> </button>
</div> </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"> <div class="col-md-3" style="position: relative; margin-top: 1vh">
<button id="id_settings_button" class="btn btn-primary btn-block"><i class="fas fa-cog"></i> <button id="id_settings_button" class="btn btn-primary btn-block text-uppercase"><i
class="fas fa-cog"></i>
</button> </button>
</div> </div>
@ -181,7 +178,16 @@
</div> </div>
</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 class="col col-md-12">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
@ -200,10 +206,16 @@
</div> </div>
</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"> <div class="col col-md-12">
<b-button @click="loadMore()" class="btn-block btn-success" v-if="pagination_more">{{ $t('Load_More') }} <b-pagination pills
</b-button> v-model="settings.pagination_page"
:total-rows="pagination_count"
per-page="25"
@change="pageChange"
align="center">
</b-pagination>
</div> </div>
</div> </div>
@ -239,6 +251,8 @@ import GenericMultiselect from "@/components/GenericMultiselect";
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
let SETTINGS_COOKIE_NAME = 'search_settings'
export default { export default {
name: 'RecipeSearchView', name: 'RecipeSearchView',
mixins: [ResolveUrlMixin], mixins: [ResolveUrlMixin],
@ -249,6 +263,7 @@ export default {
meal_plans: [], meal_plans: [],
last_viewed_recipes: [], last_viewed_recipes: [],
settings_loaded: false,
settings: { settings: {
search_input: '', search_input: '',
search_internal: false, search_internal: false,
@ -262,19 +277,33 @@ export default {
advanced_search_visible: false, advanced_search_visible: false,
show_meal_plan: true, show_meal_plan: true,
recently_viewed: 5, recently_viewed: 5,
pagination_page: 1,
}, },
pagination_more: true, pagination_count: 0,
pagination_page: 1,
} }
}, },
mounted() { mounted() {
this.$nextTick(function () { this.$nextTick(function () {
if (this.$cookies.isKey('search_settings_v2')) { if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
this.settings = this.$cookies.get("search_settings_v2") 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 urlParams = new URLSearchParams(window.location.search);
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
@ -284,7 +313,7 @@ export default {
let keyword = {id: x, name: 'loading'} let keyword = {id: x, name: 'loading'}
this.settings.search_keywords.push(keyword) this.settings.search_keywords.push(keyword)
apiClient.retrieveKeyword(x).then(result => { 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: { watch: {
settings: { settings: {
handler() { handler() {
this.$cookies.set("search_settings_v2", this.settings, -1) this.$cookies.set(SETTINGS_COOKIE_NAME, this.settings, -1)
}, },
deep: true deep: true
}, },
@ -333,16 +362,11 @@ export default {
this.settings.search_internal, this.settings.search_internal,
undefined, undefined,
this.pagination_page, this.settings.pagination_page,
).then(result => { ).then(result => {
this.pagination_more = (result.data.next !== null) window.scrollTo(0, 0);
if (page_load) { this.pagination_count = result.data.count
for (let x of result.data.results) { this.recipes = result.data.results
this.recipes.push(x)
}
} else {
this.recipes = result.data.results
}
}) })
}, },
loadMealPlan: function () { loadMealPlan: function () {
@ -384,11 +408,15 @@ export default {
this.settings.search_keywords = [] this.settings.search_keywords = []
this.settings.search_foods = [] this.settings.search_foods = []
this.settings.search_books = [] this.settings.search_books = []
this.settings.pagination_page = 1
this.refreshData(false) this.refreshData(false)
}, },
loadMore: function (page) { pageChange: function (page) {
this.pagination_page++ this.settings.pagination_page = page
this.refreshData(true) 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