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:
commit
28b67a2ba5
@ -83,7 +83,8 @@ REVERSE_PROXY_AUTH=0
|
||||
# Default settings for spaces, apply per space and can be changed in the admin view
|
||||
# SPACE_DEFAULT_MAX_RECIPES=0 # 0=unlimited recipes
|
||||
# SPACE_DEFAULT_MAX_USERS=0 # 0=unlimited users per space
|
||||
# SPACE_DEFAULT_FILES=1 # 1=can upload files (images, etc.) NOT IMPLEMENTED YET
|
||||
# SPACE_DEFAULT_MAX_FILES=0 # Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.
|
||||
# SPACE_DEFAULT_ALLOW_SHARING=1 # Allow users to share recipes with public links
|
||||
|
||||
# allow people to create accounts on your application instance (without an invite link)
|
||||
# when unset: 0 (false)
|
||||
@ -113,4 +114,9 @@ REVERSE_PROXY_AUTH=0
|
||||
# SOCIAL_DEFAULT_ACCESS = 1
|
||||
|
||||
# if SOCIAL_DEFAULT_ACCESS is used, which group should be added
|
||||
# SOCIAL_DEFAULT_GROUP=guest
|
||||
# SOCIAL_DEFAULT_GROUP=guest
|
||||
|
||||
# Django session cookie settings. Can be changed to allow a single django application to authenticate several applications
|
||||
# when running under the same database
|
||||
# SESSION_COOKIE_DOMAIN=.example.com
|
||||
# SESSION_COOKIE_NAME=sessionid # use this only to not interfere with non unified django applications under the same top level domain
|
15
README.md
15
README.md
@ -9,18 +9,17 @@
|
||||
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
|
||||
|
||||
<p align="center">
|
||||
|
||||
<img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=develop" >
|
||||
<img src="https://img.shields.io/github/stars/vabene1111/recipes" >
|
||||
<img src="https://img.shields.io/github/forks/vabene1111/recipes" >
|
||||
<img src="https://img.shields.io/docker/pulls/vabene1111/recipes" >
|
||||
|
||||
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=master" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
|
||||
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
|
||||
<a href="https://docs.tandoor.dev/install/docker.html" target="_blank" rel="noopener noreferrer">Installation</a> •
|
||||
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Documentation</a> •
|
||||
<a href="https://app.tandoor.dev/" target="_blank" rel="noopener noreferrer">Demo</a>
|
||||
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
@ -31,14 +31,20 @@ admin.site.unregister(Group)
|
||||
|
||||
|
||||
class SpaceAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'created_by', 'message')
|
||||
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||
search_fields = ('name', 'created_by__username')
|
||||
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
|
||||
admin.site.register(Space, SpaceAdmin)
|
||||
|
||||
|
||||
class UserPreferenceAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'space', 'theme', 'nav_color', 'default_page', 'search_style',)
|
||||
list_display = ('name', 'space', 'theme', 'nav_color', 'default_page', 'search_style',) # TODO add new fields
|
||||
search_fields = ('user__username', 'space__name')
|
||||
list_filter = ('theme', 'nav_color', 'default_page', 'search_style')
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
@staticmethod
|
||||
def name(obj):
|
||||
@ -50,6 +56,7 @@ admin.site.register(UserPreference, UserPreferenceAdmin)
|
||||
|
||||
class StorageAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'method')
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
admin.site.register(Storage, StorageAdmin)
|
||||
@ -57,6 +64,7 @@ admin.site.register(Storage, StorageAdmin)
|
||||
|
||||
class SyncAdmin(admin.ModelAdmin):
|
||||
list_display = ('storage', 'path', 'active', 'last_checked')
|
||||
search_fields = ('storage__name', 'path')
|
||||
|
||||
|
||||
admin.site.register(Sync, SyncAdmin)
|
||||
@ -102,6 +110,7 @@ admin.site.register(Keyword, KeywordAdmin)
|
||||
|
||||
class StepAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'type', 'order')
|
||||
search_fields = ('name', 'type')
|
||||
|
||||
|
||||
admin.site.register(Step, StepAdmin)
|
||||
@ -120,6 +129,9 @@ def rebuild_index(modeladmin, request, queryset):
|
||||
|
||||
class RecipeAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'internal', 'created_by', 'storage')
|
||||
search_fields = ('name', 'created_by__username')
|
||||
list_filter = ('internal',)
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
@staticmethod
|
||||
def created_by(obj):
|
||||
@ -137,6 +149,7 @@ admin.site.register(Food)
|
||||
|
||||
class IngredientAdmin(admin.ModelAdmin):
|
||||
list_display = ('food', 'amount', 'unit')
|
||||
search_fields = ('food__name', 'unit__name')
|
||||
|
||||
|
||||
admin.site.register(Ingredient, IngredientAdmin)
|
||||
@ -144,6 +157,8 @@ admin.site.register(Ingredient, IngredientAdmin)
|
||||
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
list_display = ('recipe', 'name', 'created_at')
|
||||
search_fields = ('text', 'user__username')
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
@staticmethod
|
||||
def name(obj):
|
||||
@ -162,6 +177,7 @@ admin.site.register(RecipeImport, RecipeImportAdmin)
|
||||
|
||||
class RecipeBookAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'user_name')
|
||||
search_fields = ('name', 'created_by__username')
|
||||
|
||||
@staticmethod
|
||||
def user_name(obj):
|
||||
@ -191,6 +207,7 @@ admin.site.register(MealPlan, MealPlanAdmin)
|
||||
|
||||
class MealTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'created_by', 'order')
|
||||
search_fields = ('name', 'created_by__username')
|
||||
|
||||
|
||||
admin.site.register(MealType, MealTypeAdmin)
|
||||
|
@ -7,6 +7,8 @@ from django.contrib import messages
|
||||
from django.core.cache import caches
|
||||
from gettext import gettext as _
|
||||
|
||||
from cookbook.models import InviteLink
|
||||
|
||||
|
||||
class AllAuthCustomAdapter(DefaultAccountAdapter):
|
||||
|
||||
@ -14,7 +16,11 @@ class AllAuthCustomAdapter(DefaultAccountAdapter):
|
||||
"""
|
||||
Whether to allow sign ups.
|
||||
"""
|
||||
if request.resolver_match.view_name == 'account_signup' and not settings.ENABLE_SIGNUP:
|
||||
signup_token = False
|
||||
if 'signup_token' in request.session and InviteLink.objects.filter(valid_until__gte=datetime.datetime.today(), used_by=None, uuid=request.session['signup_token']).exists():
|
||||
signup_token = True
|
||||
|
||||
if (request.resolver_match.view_name == 'account_signup' or request.resolver_match.view_name == 'socialaccount_signup') and not settings.ENABLE_SIGNUP and not signup_token:
|
||||
return False
|
||||
else:
|
||||
return super(AllAuthCustomAdapter, self).is_open_for_signup(request)
|
||||
|
45
cookbook/helper/image_processing.py
Normal file
45
cookbook/helper/image_processing.py
Normal 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
|
@ -1,3 +1,4 @@
|
||||
import re
|
||||
import string
|
||||
import unicodedata
|
||||
|
||||
@ -22,20 +23,16 @@ def parse_fraction(x):
|
||||
def parse_amount(x):
|
||||
amount = 0
|
||||
unit = ''
|
||||
note = ''
|
||||
|
||||
did_check_frac = False
|
||||
end = 0
|
||||
while (
|
||||
end < len(x)
|
||||
and (
|
||||
x[end] in string.digits
|
||||
or (
|
||||
(x[end] == '.' or x[end] == ',' or x[end] == '/')
|
||||
and end + 1 < len(x)
|
||||
and x[end + 1] in string.digits
|
||||
)
|
||||
)
|
||||
):
|
||||
while (end < len(x) and (x[end] in string.digits
|
||||
or (
|
||||
(x[end] == '.' or x[end] == ',' or x[end] == '/')
|
||||
and end + 1 < len(x)
|
||||
and x[end + 1] in string.digits
|
||||
))):
|
||||
end += 1
|
||||
if end > 0:
|
||||
if "/" in x[:end]:
|
||||
@ -55,7 +52,11 @@ def parse_amount(x):
|
||||
unit = x[end + 1:]
|
||||
except ValueError:
|
||||
unit = x[end:]
|
||||
return amount, unit
|
||||
|
||||
if unit.startswith('(') or unit.startswith('-'): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
|
||||
unit = ''
|
||||
note = x
|
||||
return amount, unit, note
|
||||
|
||||
|
||||
def parse_ingredient_with_comma(tokens):
|
||||
@ -106,6 +107,13 @@ def parse(x):
|
||||
unit = ''
|
||||
ingredient = ''
|
||||
note = ''
|
||||
unit_note = ''
|
||||
|
||||
# if the string contains parenthesis early on remove it and place it at the end
|
||||
# because its likely some kind of note
|
||||
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', x):
|
||||
match = re.search('\((.[^\(])+\)', x)
|
||||
x = x[:match.start()] + x[match.end():] + ' ' + x[match.start():match.end()]
|
||||
|
||||
tokens = x.split()
|
||||
if len(tokens) == 1:
|
||||
@ -114,17 +122,17 @@ def parse(x):
|
||||
else:
|
||||
try:
|
||||
# try to parse first argument as amount
|
||||
amount, unit = parse_amount(tokens[0])
|
||||
amount, unit, unit_note = parse_amount(tokens[0])
|
||||
# only try to parse second argument as amount if there are at least
|
||||
# three arguments if it already has a unit there can't be
|
||||
# a fraction for the amount
|
||||
if len(tokens) > 2:
|
||||
try:
|
||||
if not unit == '':
|
||||
# a unit is already found, no need to try the second argument for a fraction # noqa: E501
|
||||
# a unit is already found, no need to try the second argument for a fraction
|
||||
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except # noqa: E501
|
||||
raise ValueError
|
||||
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½' # noqa: E501
|
||||
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
|
||||
amount += parse_fraction(tokens[1])
|
||||
# assume that units can't end with a comma
|
||||
if len(tokens) > 3 and not tokens[2].endswith(','):
|
||||
@ -142,7 +150,10 @@ def parse(x):
|
||||
# try to use second argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
|
||||
try:
|
||||
ingredient, note = parse_ingredient(tokens[2:])
|
||||
unit = tokens[1]
|
||||
if unit == '':
|
||||
unit = tokens[1]
|
||||
else:
|
||||
note = tokens[1]
|
||||
except ValueError:
|
||||
ingredient, note = parse_ingredient(tokens[1:])
|
||||
else:
|
||||
@ -158,11 +169,16 @@ def parse(x):
|
||||
ingredient, note = parse_ingredient(tokens)
|
||||
except ValueError:
|
||||
ingredient = ' '.join(tokens[1:])
|
||||
|
||||
if unit_note not in note:
|
||||
note += ' ' + unit_note
|
||||
return amount, unit.strip(), ingredient.strip(), note.strip()
|
||||
|
||||
|
||||
# small utility functions to prevent emtpy unit/food creation
|
||||
def get_unit(unit, space):
|
||||
if not unit:
|
||||
return None
|
||||
if len(unit) > 0:
|
||||
u, created = Unit.objects.get_or_create(name=unit, space=space)
|
||||
return u
|
||||
@ -170,6 +186,8 @@ def get_unit(unit, space):
|
||||
|
||||
|
||||
def get_food(food, space):
|
||||
if not food:
|
||||
return None
|
||||
if len(food) > 0:
|
||||
f, created = Food.objects.get_or_create(name=food, space=space)
|
||||
return f
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""
|
||||
Source: https://djangosnippets.org/snippets/1703/
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.core.cache import caches
|
||||
from django.views.generic.detail import SingleObjectTemplateResponseMixin
|
||||
from django.views.generic.edit import ModelFormMixin
|
||||
|
||||
@ -90,7 +92,18 @@ def share_link_valid(recipe, share):
|
||||
:return: true if a share link with the given recipe and uuid exists
|
||||
"""
|
||||
try:
|
||||
return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
|
||||
CACHE_KEY = f'recipe_share_{recipe.pk}_{share}'
|
||||
if c := caches['default'].get(CACHE_KEY, False):
|
||||
return c
|
||||
|
||||
if link := ShareLink.objects.filter(recipe=recipe, uuid=share, abuse_blocked=False).first():
|
||||
if 0 < settings.SHARING_LIMIT < link.request_count:
|
||||
return False
|
||||
link.request_count += 1
|
||||
link.save()
|
||||
caches['default'].set(CACHE_KEY, True, timeout=3)
|
||||
return True
|
||||
return False
|
||||
except ValidationError:
|
||||
return False
|
||||
|
||||
@ -121,15 +134,18 @@ class GroupRequiredMixin(object):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not has_group_permission(request.user, self.groups_required):
|
||||
if not request.user.is_authenticated:
|
||||
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You are not logged in and therefore cannot view this page!'))
|
||||
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You do not have the required permissions to view this page!'))
|
||||
return HttpResponseRedirect(reverse_lazy('index'))
|
||||
try:
|
||||
obj = self.get_object()
|
||||
if obj.get_space() != request.space:
|
||||
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You do not have the required permissions to view this page!'))
|
||||
return HttpResponseRedirect(reverse_lazy('index'))
|
||||
except AttributeError:
|
||||
pass
|
||||
@ -141,17 +157,20 @@ class OwnerRequiredMixin(object):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You are not logged in and therefore cannot view this page!'))
|
||||
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
|
||||
else:
|
||||
if not is_object_owner(request.user, self.get_object()):
|
||||
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You cannot interact with this object as it is not owned by you!'))
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
try:
|
||||
obj = self.get_object()
|
||||
if obj.get_space() != request.space:
|
||||
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('You do not have the required permissions to view this page!'))
|
||||
return HttpResponseRedirect(reverse_lazy('index'))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
@ -40,7 +40,7 @@ class Pepperplate(Integration):
|
||||
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n'
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
|
||||
)
|
||||
|
||||
for ingredient in ingredients:
|
||||
@ -49,7 +49,7 @@ class Pepperplate(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
@ -38,7 +38,7 @@ class ChefTap(Integration):
|
||||
|
||||
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space, )
|
||||
|
||||
step = Step.objects.create(instruction='\n'.join(directions))
|
||||
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space,)
|
||||
|
||||
if source_url != '':
|
||||
step.instruction += '\n' + source_url
|
||||
@ -50,7 +50,7 @@ class ChefTap(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
@ -3,6 +3,7 @@ import re
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
|
||||
@ -54,7 +55,7 @@ class Chowdown(Integration):
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions)
|
||||
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions), space=self.request.space,
|
||||
)
|
||||
|
||||
for ingredient in ingredients:
|
||||
@ -62,7 +63,7 @@ class Chowdown(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
@ -71,7 +72,7 @@ class Chowdown(Integration):
|
||||
import_zip = ZipFile(f['file'])
|
||||
for z in import_zip.filelist:
|
||||
if re.match(f'^images/{image}$', z.filename):
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename))
|
||||
|
||||
return recipe
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
import json
|
||||
from io import BytesIO
|
||||
from re import match
|
||||
from zipfile import ZipFile
|
||||
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.serializer import RecipeExportSerializer
|
||||
|
||||
@ -15,8 +17,9 @@ class Default(Integration):
|
||||
|
||||
recipe_string = recipe_zip.read('recipe.json').decode("utf-8")
|
||||
recipe = self.decode_recipe(recipe_string)
|
||||
if 'image.png' in recipe_zip.namelist():
|
||||
self.import_recipe_image(recipe, BytesIO(recipe_zip.read('image.png')))
|
||||
images = list(filter(lambda v: match('image.*', v), recipe_zip.namelist()))
|
||||
if images:
|
||||
self.import_recipe_image(recipe, BytesIO(recipe_zip.read(images[0])), filetype=get_filetype(images[0]))
|
||||
return recipe
|
||||
|
||||
def decode_recipe(self, string):
|
||||
|
@ -28,7 +28,7 @@ class Domestica(Integration):
|
||||
recipe.save()
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction=file['directions']
|
||||
instruction=file['directions'], space=self.request.space,
|
||||
)
|
||||
|
||||
if file['source'] != '':
|
||||
@ -40,12 +40,12 @@ class Domestica(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
if file['image'] != '':
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(file['image'].replace('data:image/jpeg;base64,', ''))))
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(file['image'].replace('data:image/jpeg;base64,', ''))), filetype='.jpeg')
|
||||
|
||||
return recipe
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from io import BytesIO, StringIO
|
||||
@ -12,6 +13,7 @@ from django.utils.translation import gettext as _
|
||||
from django_scopes import scope
|
||||
|
||||
from cookbook.forms import ImportExportBase
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.models import Keyword, Recipe
|
||||
|
||||
|
||||
@ -59,7 +61,7 @@ class Integration:
|
||||
recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
|
||||
recipe_stream.close()
|
||||
try:
|
||||
recipe_zip_obj.writestr('image.png', r.image.file.read())
|
||||
recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read())
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@ -107,35 +109,52 @@ class Integration:
|
||||
for f in files:
|
||||
if 'RecipeKeeper' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
file_list = []
|
||||
for z in import_zip.filelist:
|
||||
if self.import_file_name_filter(z):
|
||||
data_list = self.split_recipe_file(import_zip.read(z.filename).decode('utf-8'))
|
||||
for d in data_list:
|
||||
recipe = self.get_recipe_from_file(d)
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
file_list.append(z)
|
||||
il.total_recipes += len(file_list)
|
||||
|
||||
for z in file_list:
|
||||
data_list = self.split_recipe_file(import_zip.read(z.filename).decode('utf-8'))
|
||||
for d in data_list:
|
||||
recipe = self.get_recipe_from_file(d)
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
import_zip.close()
|
||||
elif '.zip' in f['name'] or '.paprikarecipes' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
file_list = []
|
||||
for z in import_zip.filelist:
|
||||
if self.import_file_name_filter(z):
|
||||
try:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
except Exception as e:
|
||||
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
|
||||
file_list.append(z)
|
||||
il.total_recipes += len(file_list)
|
||||
|
||||
for z in file_list:
|
||||
try:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
except Exception as e:
|
||||
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
|
||||
import_zip.close()
|
||||
elif '.json' in f['name'] or '.txt' in f['name']:
|
||||
data_list = self.split_recipe_file(f['file'])
|
||||
il.total_recipes += len(data_list)
|
||||
for d in data_list:
|
||||
try:
|
||||
recipe = self.get_recipe_from_file(d)
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
except Exception as e:
|
||||
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
|
||||
elif '.rtk' in f['name']:
|
||||
@ -143,12 +162,16 @@ class Integration:
|
||||
for z in import_zip.filelist:
|
||||
if self.import_file_name_filter(z):
|
||||
data_list = self.split_recipe_file(import_zip.read(z.filename).decode('utf-8'))
|
||||
il.total_recipes += len(data_list)
|
||||
|
||||
for d in data_list:
|
||||
try:
|
||||
recipe = self.get_recipe_from_file(d)
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
except Exception as e:
|
||||
il.msg += f'-------------------- \n ERROR \n{e}\n--------------------\n'
|
||||
import_zip.close()
|
||||
@ -160,6 +183,9 @@ class Integration:
|
||||
except BadZipFile:
|
||||
il.msg += 'ERROR ' + _(
|
||||
'Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n'
|
||||
except:
|
||||
il.msg += 'ERROR ' + _(
|
||||
'An unexpected error occurred during the import. Please make sure you have uploaded a valid file.') + '\n'
|
||||
|
||||
if len(self.ignored_recipes) > 0:
|
||||
il.msg += '\n' + _(
|
||||
@ -182,13 +208,14 @@ class Integration:
|
||||
self.ignored_recipes.append(recipe.name)
|
||||
|
||||
@staticmethod
|
||||
def import_recipe_image(recipe, image_file):
|
||||
def import_recipe_image(recipe, image_file, filetype='.jpeg'):
|
||||
"""
|
||||
Adds an image to a recipe naming it correctly
|
||||
:param recipe: Recipe object
|
||||
:param image_file: ByteIO stream containing the image
|
||||
:param filetype: type of file to write bytes to, default to .jpeg if unknown
|
||||
"""
|
||||
recipe.image = File(image_file, name=f'{uuid.uuid4()}_{recipe.pk}.png')
|
||||
recipe.image = File(image_file, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}')
|
||||
recipe.save()
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
@ -217,4 +244,3 @@ class Integration:
|
||||
- data - string content for file to get created in export zip
|
||||
"""
|
||||
raise NotImplementedError('Method not implemented in integration')
|
||||
|
||||
|
@ -3,6 +3,7 @@ import re
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
|
||||
@ -11,7 +12,7 @@ from cookbook.models import Recipe, Step, Food, Unit, Ingredient
|
||||
class Mealie(Integration):
|
||||
|
||||
def import_file_name_filter(self, zip_info_object):
|
||||
return re.match(r'^recipes/([A-Za-z\d-])+.json$', zip_info_object.filename)
|
||||
return re.match(r'^recipes/([A-Za-z\d-])+/([A-Za-z\d-])+.json$', zip_info_object.filename)
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
recipe_json = json.loads(file.getvalue().decode("utf-8"))
|
||||
@ -25,9 +26,9 @@ class Mealie(Integration):
|
||||
# TODO parse times (given in PT2H3M )
|
||||
|
||||
ingredients_added = False
|
||||
for s in recipe_json['recipeInstructions']:
|
||||
for s in recipe_json['recipe_instructions']:
|
||||
step = Step.objects.create(
|
||||
instruction=s['text']
|
||||
instruction=s['text'], space=self.request.space,
|
||||
)
|
||||
if not ingredients_added:
|
||||
ingredients_added = True
|
||||
@ -35,21 +36,31 @@ class Mealie(Integration):
|
||||
if len(recipe_json['description'].strip()) > 500:
|
||||
step.instruction = recipe_json['description'].strip() + '\n\n' + step.instruction
|
||||
|
||||
for ingredient in recipe_json['recipeIngredient']:
|
||||
amount, unit, ingredient, note = parse(ingredient)
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
))
|
||||
for ingredient in recipe_json['recipe_ingredient']:
|
||||
try:
|
||||
if ingredient['food']:
|
||||
f = get_food(ingredient['food'], self.request.space)
|
||||
u = get_unit(ingredient['unit'], self.request.space)
|
||||
amount = ingredient['quantity']
|
||||
note = ingredient['note']
|
||||
else:
|
||||
amount, unit, ingredient, note = parse(ingredient['note'])
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
except:
|
||||
pass
|
||||
recipe.steps.add(step)
|
||||
|
||||
for f in self.files:
|
||||
if '.zip' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
for z in import_zip.filelist:
|
||||
if re.match(f'^images/{recipe_json["slug"]}.jpg$', z.filename):
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
||||
try:
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(f'recipes/{recipe_json["slug"]}/images/min-original.webp')), filetype=get_filetype(f'recipes/{recipe_json["slug"]}/images/original'))
|
||||
except:
|
||||
pass
|
||||
|
||||
return recipe
|
||||
|
||||
|
@ -44,7 +44,7 @@ class MealMaster(Integration):
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n'
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
|
||||
)
|
||||
|
||||
for ingredient in ingredients:
|
||||
@ -53,7 +53,7 @@ class MealMaster(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
@ -3,6 +3,7 @@ import re
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Food, Unit, Ingredient
|
||||
@ -29,7 +30,7 @@ class NextcloudCookbook(Integration):
|
||||
ingredients_added = False
|
||||
for s in recipe_json['recipeInstructions']:
|
||||
step = Step.objects.create(
|
||||
instruction=s
|
||||
instruction=s, space=self.request.space,
|
||||
)
|
||||
if not ingredients_added:
|
||||
if len(recipe_json['description'].strip()) > 500:
|
||||
@ -42,7 +43,7 @@ class NextcloudCookbook(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
@ -51,7 +52,7 @@ class NextcloudCookbook(Integration):
|
||||
import_zip = ZipFile(f['file'])
|
||||
for z in import_zip.filelist:
|
||||
if re.match(f'^Recipes/{recipe.name}/full.jpg$', z.filename):
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)))
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename))
|
||||
|
||||
return recipe
|
||||
|
||||
|
@ -24,13 +24,13 @@ class OpenEats(Integration):
|
||||
if file["source"] != '':
|
||||
instructions += file["source"]
|
||||
|
||||
step = Step.objects.create(instruction=instructions)
|
||||
step = Step.objects.create(instruction=instructions, space=self.request.space,)
|
||||
|
||||
for ingredient in file['ingredients']:
|
||||
f = get_food(ingredient['food'], self.request.space)
|
||||
u = get_unit(ingredient['unit'], self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=ingredient['amount']
|
||||
food=f, unit=u, amount=ingredient['amount'], space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
@ -55,7 +55,7 @@ class Paprika(Integration):
|
||||
pass
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction=instructions
|
||||
instruction=instructions, space=self.request.space,
|
||||
)
|
||||
|
||||
if len(recipe_json['description'].strip()) > 500:
|
||||
@ -73,7 +73,7 @@ class Paprika(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
except AttributeError:
|
||||
pass
|
||||
@ -81,6 +81,6 @@ class Paprika(Integration):
|
||||
recipe.steps.add(step)
|
||||
|
||||
if recipe_json.get("photo_data", None):
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])))
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])), filetype='.jpeg')
|
||||
|
||||
return recipe
|
||||
|
@ -7,6 +7,7 @@ from zipfile import ZipFile
|
||||
import imghdr
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword
|
||||
@ -25,7 +26,7 @@ class RecetteTek(Integration):
|
||||
recipe_list = [r for r in recipe_json]
|
||||
|
||||
return recipe_list
|
||||
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
|
||||
# Create initial recipe with just a title and a decription
|
||||
@ -44,7 +45,7 @@ class RecetteTek(Integration):
|
||||
if not instructions:
|
||||
instructions = ''
|
||||
|
||||
step = Step.objects.create(instruction=instructions)
|
||||
step = Step.objects.create(instruction=instructions, space=self.request.space,)
|
||||
|
||||
# Append the original import url to the step (if it exists)
|
||||
try:
|
||||
@ -53,7 +54,7 @@ class RecetteTek(Integration):
|
||||
step.save()
|
||||
except Exception as e:
|
||||
print(recipe.name, ': failed to import source url ', str(e))
|
||||
|
||||
|
||||
try:
|
||||
# Process the ingredients. Assumes 1 ingredient per line.
|
||||
for ingredient in file['ingredients'].split('\n'):
|
||||
@ -62,7 +63,7 @@ class RecetteTek(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
except Exception as e:
|
||||
print(recipe.name, ': failed to parse recipe ingredients ', str(e))
|
||||
@ -96,7 +97,7 @@ class RecetteTek(Integration):
|
||||
recipe.waiting_time = int(file['cookingTime'])
|
||||
except Exception as e:
|
||||
print(recipe.name, ': failed to parse cooking time ', str(e))
|
||||
|
||||
|
||||
recipe.save()
|
||||
|
||||
# Import the recipe keywords
|
||||
@ -110,20 +111,20 @@ class RecetteTek(Integration):
|
||||
pass
|
||||
|
||||
# TODO: Parse Nutritional Information
|
||||
|
||||
|
||||
# Import the original image from the zip file, if we cannot do that, attempt to download it again.
|
||||
try:
|
||||
if file['pictures'][0] !='':
|
||||
if file['pictures'][0] != '':
|
||||
image_file_name = file['pictures'][0].split('/')[-1]
|
||||
for f in self.files:
|
||||
if '.rtk' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(image_file_name)))
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(image_file_name)), filetype=get_filetype(image_file_name))
|
||||
else:
|
||||
if file['originalPicture'] != '':
|
||||
response=requests.get(file['originalPicture'])
|
||||
response = requests.get(file['originalPicture'])
|
||||
if imghdr.what(BytesIO(response.content)) != None:
|
||||
self.import_recipe_image(recipe, BytesIO(response.content))
|
||||
self.import_recipe_image(recipe, BytesIO(response.content), filetype=get_filetype(file['originalPicture']))
|
||||
else:
|
||||
raise Exception("Original image failed to download.")
|
||||
except Exception as e:
|
||||
|
@ -41,7 +41,7 @@ class RecipeKeeper(Integration):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
step = Step.objects.create(instruction='')
|
||||
step = Step.objects.create(instruction='', space=self.request.space,)
|
||||
|
||||
for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"):
|
||||
if ingredient.text == "":
|
||||
@ -50,7 +50,7 @@ class RecipeKeeper(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
|
||||
for s in file.find("div", {"itemprop": "recipeDirections"}).find_all("p"):
|
||||
@ -70,7 +70,7 @@ class RecipeKeeper(Integration):
|
||||
for f in self.files:
|
||||
if '.zip' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipe-photo").get("src"))))
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipe-photo").get("src"))), filetype='.jpeg')
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
@ -36,7 +36,7 @@ class RecipeSage(Integration):
|
||||
ingredients_added = False
|
||||
for s in file['recipeInstructions']:
|
||||
step = Step.objects.create(
|
||||
instruction=s['text']
|
||||
instruction=s['text'], space=self.request.space,
|
||||
)
|
||||
if not ingredients_added:
|
||||
ingredients_added = True
|
||||
@ -46,7 +46,7 @@ class RecipeSage(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
@ -43,7 +43,7 @@ class RezKonv(Integration):
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction='\n'.join(directions) + '\n\n'
|
||||
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
|
||||
)
|
||||
|
||||
for ingredient in ingredients:
|
||||
@ -52,7 +52,7 @@ class RezKonv(Integration):
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
@ -43,14 +43,14 @@ class Safron(Integration):
|
||||
|
||||
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space, )
|
||||
|
||||
step = Step.objects.create(instruction='\n'.join(directions))
|
||||
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space,)
|
||||
|
||||
for ingredient in ingredients:
|
||||
amount, unit, ingredient, note = parse(ingredient)
|
||||
f = get_food(ingredient, self.request.space)
|
||||
u = get_unit(unit, self.request.space)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
18
cookbook/migrations/0132_sharelink_request_count.py
Normal file
18
cookbook/migrations/0132_sharelink_request_count.py
Normal 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),
|
||||
),
|
||||
]
|
18
cookbook/migrations/0133_sharelink_abuse_blocked.py
Normal file
18
cookbook/migrations/0133_sharelink_abuse_blocked.py
Normal 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),
|
||||
),
|
||||
]
|
18
cookbook/migrations/0134_space_allow_sharing.py
Normal file
18
cookbook/migrations/0134_space_allow_sharing.py
Normal 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),
|
||||
),
|
||||
]
|
29
cookbook/migrations/0135_auto_20210615_2210.py
Normal file
29
cookbook/migrations/0135_auto_20210615_2210.py
Normal 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'),
|
||||
),
|
||||
|
||||
]
|
23
cookbook/migrations/0136_auto_20210617_1343.py
Normal file
23
cookbook/migrations/0136_auto_20210617_1343.py
Normal 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),
|
||||
),
|
||||
]
|
34
cookbook/migrations/0137_auto_20210617_1501.py
Normal file
34
cookbook/migrations/0137_auto_20210617_1501.py
Normal 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),
|
||||
|
||||
]
|
31
cookbook/migrations/0138_auto_20210617_1602.py
Normal file
31
cookbook/migrations/0138_auto_20210617_1602.py
Normal 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'),
|
||||
),
|
||||
]
|
20
cookbook/migrations/0139_space_created_at.py
Normal file
20
cookbook/migrations/0139_space_created_at.py
Normal 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,
|
||||
),
|
||||
]
|
20
cookbook/migrations/0140_userpreference_created_at.py
Normal file
20
cookbook/migrations/0140_userpreference_created_at.py
Normal 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,
|
||||
),
|
||||
]
|
@ -70,10 +70,12 @@ class PermissionModelMixin:
|
||||
class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
name = models.CharField(max_length=128, default='Default')
|
||||
created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
message = models.CharField(max_length=512, default='', blank=True)
|
||||
max_recipes = models.IntegerField(default=0)
|
||||
max_file_storage_mb = models.IntegerField(default=0, help_text=_('Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.'))
|
||||
max_users = models.IntegerField(default=0)
|
||||
allow_sharing = models.BooleanField(default=True)
|
||||
demo = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
@ -157,6 +159,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
shopping_auto_sync = models.IntegerField(default=5)
|
||||
sticky_navbar = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
@ -388,14 +391,8 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
|
||||
no_amount = models.BooleanField(default=False)
|
||||
order = models.IntegerField(default=0)
|
||||
|
||||
objects = ScopedManager(space='step__recipe__space')
|
||||
|
||||
@staticmethod
|
||||
def get_space_key():
|
||||
return 'step', 'recipe', 'space'
|
||||
|
||||
def get_space(self):
|
||||
return self.step_set.first().recipe_set.first().space
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
def __str__(self):
|
||||
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food)
|
||||
@ -424,14 +421,8 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
|
||||
show_as_header = models.BooleanField(default=True)
|
||||
search_vector = SearchVectorField(null=True)
|
||||
|
||||
objects = ScopedManager(space='recipe__space')
|
||||
|
||||
@staticmethod
|
||||
def get_space_key():
|
||||
return 'recipe', 'space'
|
||||
|
||||
def get_space(self):
|
||||
return self.recipe_set.first().space
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
def get_instruction_render(self):
|
||||
from cookbook.helper.template_helper import render_instructions
|
||||
@ -453,17 +444,11 @@ class NutritionInformation(models.Model, PermissionModelMixin):
|
||||
max_length=512, default="", null=True, blank=True
|
||||
)
|
||||
|
||||
objects = ScopedManager(space='recipe__space')
|
||||
|
||||
@staticmethod
|
||||
def get_space_key():
|
||||
return 'recipe', 'space'
|
||||
|
||||
def get_space(self):
|
||||
return self.recipe_set.first().space
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
def __str__(self):
|
||||
return 'Nutrition'
|
||||
return f'Nutrition {self.pk}'
|
||||
|
||||
|
||||
class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModelMixin):
|
||||
@ -690,6 +675,8 @@ class ShoppingList(ExportModelOperationsMixin('shopping_list'), models.Model, Pe
|
||||
class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
|
||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
request_count = models.IntegerField(default=0)
|
||||
abuse_blocked = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@ -773,6 +760,10 @@ class ImportLog(models.Model, PermissionModelMixin):
|
||||
running = models.BooleanField(default=True)
|
||||
msg = models.TextField(default="")
|
||||
keyword = models.ForeignKey(Keyword, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
|
||||
total_recipes = models.IntegerField(default=0)
|
||||
imported_recipes = models.IntegerField(default=0)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
|
@ -3,7 +3,7 @@ from decimal import Decimal
|
||||
from gettext import gettext as _
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import QuerySet, Sum
|
||||
from django.db.models import QuerySet, Sum, Avg
|
||||
from drf_writable_nested import (UniqueFieldsMixin,
|
||||
WritableNestedModelSerializer)
|
||||
from rest_framework import serializers
|
||||
@ -301,6 +301,10 @@ class IngredientSerializer(WritableNestedModelSerializer):
|
||||
unit = UnitSerializer(allow_null=True)
|
||||
amount = CustomDecimalField()
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = Ingredient
|
||||
fields = (
|
||||
@ -313,7 +317,11 @@ class StepSerializer(WritableNestedModelSerializer):
|
||||
ingredients = IngredientSerializer(many=True)
|
||||
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
|
||||
ingredients_vue = serializers.SerializerMethodField('get_ingredients_vue')
|
||||
file = UserFileViewSerializer(allow_null=True)
|
||||
file = UserFileViewSerializer(allow_null=True, required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
def get_ingredients_vue(self, obj):
|
||||
return obj.get_instruction_render()
|
||||
@ -330,13 +338,34 @@ class StepSerializer(WritableNestedModelSerializer):
|
||||
|
||||
|
||||
class NutritionInformationSerializer(serializers.ModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = NutritionInformation
|
||||
fields = ('id', 'carbohydrates', 'fats', 'proteins', 'calories', 'source')
|
||||
|
||||
|
||||
class RecipeOverviewSerializer(WritableNestedModelSerializer):
|
||||
class RecipeBaseSerializer(WritableNestedModelSerializer):
|
||||
def get_recipe_rating(self, obj):
|
||||
rating = obj.cooklog_set.filter(created_by=self.context['request'].user, rating__gt=0).aggregate(Avg('rating'))
|
||||
if rating['rating__avg']:
|
||||
return rating['rating__avg']
|
||||
return 0
|
||||
|
||||
def get_recipe_last_cooked(self, obj):
|
||||
last = obj.cooklog_set.filter(created_by=self.context['request'].user).last()
|
||||
if last:
|
||||
return last.created_at
|
||||
return None
|
||||
|
||||
|
||||
class RecipeOverviewSerializer(RecipeBaseSerializer):
|
||||
keywords = KeywordLabelSerializer(many=True)
|
||||
rating = serializers.SerializerMethodField('get_recipe_rating')
|
||||
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
|
||||
|
||||
def create(self, validated_data):
|
||||
pass
|
||||
@ -349,22 +378,24 @@ class RecipeOverviewSerializer(WritableNestedModelSerializer):
|
||||
fields = (
|
||||
'id', 'name', 'description', 'image', 'keywords', 'working_time',
|
||||
'waiting_time', 'created_by', 'created_at', 'updated_at',
|
||||
'internal', 'servings', 'file_path'
|
||||
'internal', 'servings', 'servings_text', 'rating', 'last_cooked',
|
||||
)
|
||||
read_only_fields = ['image', 'created_by', 'created_at']
|
||||
|
||||
|
||||
class RecipeSerializer(WritableNestedModelSerializer):
|
||||
class RecipeSerializer(RecipeBaseSerializer):
|
||||
nutrition = NutritionInformationSerializer(allow_null=True, required=False)
|
||||
steps = StepSerializer(many=True)
|
||||
keywords = KeywordSerializer(many=True)
|
||||
rating = serializers.SerializerMethodField('get_recipe_rating')
|
||||
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = (
|
||||
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
|
||||
'waiting_time', 'created_by', 'created_at', 'updated_at',
|
||||
'internal', 'nutrition', 'servings', 'file_path', 'servings_text',
|
||||
'internal', 'nutrition', 'servings', 'file_path', 'servings_text', 'rating', 'last_cooked',
|
||||
)
|
||||
read_only_fields = ['image', 'created_by', 'created_at']
|
||||
|
||||
@ -538,7 +569,7 @@ class ImportLogSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ImportLog
|
||||
fields = ('id', 'type', 'msg', 'running', 'keyword', 'created_by', 'created_at')
|
||||
fields = ('id', 'type', 'msg', 'running', 'keyword', 'total_recipes', 'imported_recipes', 'created_by', 'created_at')
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
@ -596,6 +627,10 @@ class IngredientExportSerializer(WritableNestedModelSerializer):
|
||||
unit = UnitExportSerializer(allow_null=True)
|
||||
amount = CustomDecimalField()
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = Ingredient
|
||||
fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount')
|
||||
@ -604,6 +639,10 @@ class IngredientExportSerializer(WritableNestedModelSerializer):
|
||||
class StepExportSerializer(WritableNestedModelSerializer):
|
||||
ingredients = IngredientExportSerializer(many=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = Step
|
||||
fields = ('name', 'type', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')
|
||||
|
1112
cookbook/static/css/app.min.css
vendored
1112
cookbook/static/css/app.min.css
vendored
File diff suppressed because it is too large
Load Diff
10
cookbook/static/themes/tandoor.min.css
vendored
10
cookbook/static/themes/tandoor.min.css
vendored
@ -10402,16 +10402,6 @@ footer a:hover {
|
||||
box-shadow: none
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 500px
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.modal-content {
|
||||
width: 300px
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content .modal-header {
|
||||
justify-content: center;
|
||||
border: none
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -6,6 +6,14 @@
|
||||
{% block title %}{% trans "E-mail Addresses" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{% trans 'Email' %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h3>{% trans "E-mail Addresses" %}</h3>
|
||||
{% if user.emailaddress_set.all %}
|
||||
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>
|
||||
|
@ -44,13 +44,13 @@
|
||||
|
||||
{% if socialaccount_providers %}
|
||||
<div class="row" style="margin-top: 2vh">
|
||||
<div class="col-6 offset-3">
|
||||
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
|
||||
<h5>{% trans "Social Login" %}</h5>
|
||||
<span>{% trans 'You can use any of the following providers to sign in.' %}</span>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<ul class="socialaccount_providers">
|
||||
<ul class="socialaccount_providers list-unstyled">
|
||||
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
|
||||
</ul>
|
||||
|
||||
|
24
cookbook/templates/account/password_change.html
Normal file
24
cookbook/templates/account/password_change.html
Normal 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 %}
|
23
cookbook/templates/account/password_set.html
Normal file
23
cookbook/templates/account/password_set.html
Normal 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 %}
|
@ -67,9 +67,9 @@
|
||||
</button>
|
||||
|
||||
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="/" aria-label="Tandoor">
|
||||
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
|
||||
</a>
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="/" aria-label="Tandoor">
|
||||
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
@ -152,12 +152,18 @@
|
||||
<a class="dropdown-item" href="{% url 'view_history' %}"><i
|
||||
class="fas fa-history"></i> {% trans 'History' %}</a>
|
||||
{% if request.user == request.space.created_by %}
|
||||
<a class="dropdown-item" href="{% url 'view_space' %}"><i class="fas fa-server fa-fw"></i> {% trans 'Space Settings' %}</a>
|
||||
<a class="dropdown-item" href="{% url 'view_space' %}"><i
|
||||
class="fas fa-server fa-fw"></i> {% trans 'Space Settings' %}</a>
|
||||
{% endif %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{% url 'view_system' %}"><i
|
||||
class="fas fa-server fa-fw"></i> {% trans 'System' %}</a>
|
||||
<a class="dropdown-item" href="{% url 'admin:index' %}"><i
|
||||
class="fas fa-user-shield fa-fw"></i> {% trans 'Admin' %}</a>
|
||||
{% if user.is_superuser %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{% url 'view_system' %}"><i
|
||||
class="fas fa-server fa-fw"></i> {% trans 'System' %}</a>
|
||||
<a class="dropdown-item" href="{% url 'list_keyword' %}"><i
|
||||
class="fas fa-tags"></i> {% trans 'Keywords' %}</a>
|
||||
<a class="dropdown-item" href="{% url 'admin:index' %}"><i
|
||||
class="fas fa-user-shield fa-fw"></i> {% trans 'Admin' %}</a>
|
||||
{% endif %}
|
||||
@ -192,10 +198,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="container-fluid" id="id_base_container">
|
||||
<div class="container-fluid mt-2 mt-md-5 mt-xl-5 mt-lg-5" id="id_base_container">
|
||||
<div class="row">
|
||||
<div class="col-xl-2 d-none d-xl-block">
|
||||
{% block content_xl_left %}
|
||||
|
@ -67,7 +67,7 @@
|
||||
<img src="{% if recipe.image %}{{ recipe.image.url }}{% endif %}" id="id_image"
|
||||
class="img img-fluid img-responsive"
|
||||
style="max-height: 20vh">
|
||||
<input type="file" @change="imageChanged">
|
||||
<input class="mt-2" type="file" @change="imageChanged">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="id_name"> {% trans 'Preparation Time' %}</label>
|
||||
|
@ -701,10 +701,10 @@
|
||||
let first = true
|
||||
for (let se of this.shopping_list) {
|
||||
if (first) {
|
||||
url += `?r=[${se.recipe},${se.servings}]`
|
||||
url += `?r=[${se.recipe.id},${se.servings}]`
|
||||
first = false
|
||||
} else {
|
||||
url += `&r=[${se.recipe},${se.servings}]`
|
||||
url += `&r=[${se.recipe.id},${se.servings}]`
|
||||
}
|
||||
}
|
||||
return url
|
||||
|
@ -12,9 +12,11 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>
|
||||
{% trans 'Settings' %}
|
||||
</h3>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist" style="margin-bottom: 2vh">
|
||||
@ -56,23 +58,12 @@
|
||||
class="fas fa-save"></i> {% trans 'Save' %}</button>
|
||||
</form>
|
||||
|
||||
<h4>{% trans 'Password Settings' %}</h4>
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
{{ password_form|crispy }}
|
||||
<button class="btn btn-success" type="submit" name="password_form"><i
|
||||
class="fas fa-save"></i> {% trans 'Save' %}</button>
|
||||
</form>
|
||||
<h4>{% trans 'Account Settings' %}</h4>
|
||||
|
||||
<h4>{% trans 'Email Settings' %}</h4>
|
||||
<a href="{% url 'account_email'%}" class="btn btn-primary">{% trans 'Emails' %}</a>
|
||||
<a href="{% url 'account_change_password'%}" class="btn btn-primary">{% trans 'Password' %}</a>
|
||||
|
||||
<a href="{% url 'account_email'%}" class="btn btn-primary">{% trans 'Manage Email Settings' %}</a>
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<h4>{% trans 'Social' %}</h4>
|
||||
|
||||
<a href="{% url 'socialaccount_connections' %}" class="btn btn-primary">{% trans 'Manage Social Accounts' %}</a>
|
||||
<a href="{% url 'socialaccount_connections' %}" class="btn btn-primary">{% trans 'Social' %}</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
@ -4,6 +4,14 @@
|
||||
{% block head_title %}{% trans "Account Connections" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'view_settings' %}">{% trans 'Settings' %}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{% trans 'Social' %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h3>{% trans "Account Connections" %}</h3>
|
||||
|
||||
{% if form.accounts %}
|
||||
@ -46,7 +54,7 @@
|
||||
|
||||
<h4>{% trans 'Add a 3rd Party Account' %}</h4>
|
||||
|
||||
<ul class="socialaccount_providers">
|
||||
<ul class="socialaccount_providers list-unstyled">
|
||||
{% include "socialaccount/snippets/provider_list.html" with process="connect" %}
|
||||
</ul>
|
||||
|
||||
|
60
cookbook/templates/socialaccount/signup.html
Normal file
60
cookbook/templates/socialaccount/signup.html
Normal 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" %} »</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
133
cookbook/templates/socialaccount/snippets/provider_list.html
Normal file
133
cookbook/templates/socialaccount/snippets/provider_list.html
Normal 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 %}
|
@ -14,7 +14,13 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>{{ request.space.name }} <small>{% if HOSTED %}
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'view_space' %}">{% trans 'Space Settings' %}</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h3><span class="text-muted">{% trans 'Space:' %}</span> {{ request.space.name }} <small>{% if HOSTED %}
|
||||
<a href="https://tandoor.dev/manage">{% trans 'Manage Subscription' %}</a>{% endif %}</small></h3>
|
||||
|
||||
<br/>
|
||||
|
File diff suppressed because one or more lines are too long
@ -55,7 +55,7 @@ def recipe_rating(recipe, user):
|
||||
if not user.is_authenticated:
|
||||
return ''
|
||||
rating = recipe.cooklog_set \
|
||||
.filter(created_by=user, rating__gte=0) \
|
||||
.filter(created_by=user, rating__gt=0) \
|
||||
.aggregate(Avg('rating'))
|
||||
if rating['rating__avg']:
|
||||
|
||||
@ -64,7 +64,7 @@ def recipe_rating(recipe, user):
|
||||
rating_stars = rating_stars + '<i class="fas fa-star fa-xs"></i>'
|
||||
|
||||
if rating['rating__avg'] % 1 >= 0.5:
|
||||
rating_stars = rating_stars + '<i class="fas fa-star-half-alt fa-xs"></i>' # noqa: E501
|
||||
rating_stars = rating_stars + '<i class="fas fa-star-half-alt fa-xs"></i>'
|
||||
|
||||
rating_stars += '</span>'
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from django.db.models import Subquery, OuterRef
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import Ingredient
|
||||
from cookbook.models import Ingredient, Step
|
||||
|
||||
LIST_URL = 'api:ingredient-list'
|
||||
DETAIL_URL = 'api:ingredient-detail'
|
||||
@ -28,6 +29,8 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
|
||||
with scopes_disabled():
|
||||
recipe_1_s1.space = space_2
|
||||
recipe_1_s1.save()
|
||||
Step.objects.update(space=Subquery(Step.objects.filter(pk=OuterRef('pk')).values('recipe__space')[:1]))
|
||||
Ingredient.objects.update(space=Subquery(Ingredient.objects.filter(pk=OuterRef('pk')).values('step__recipe__space')[:1]))
|
||||
|
||||
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0
|
||||
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 10
|
||||
|
@ -1,10 +1,11 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from django.db.models import Subquery, OuterRef
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import Step
|
||||
from cookbook.models import Step, Ingredient
|
||||
|
||||
LIST_URL = 'api:step-list'
|
||||
DETAIL_URL = 'api:step-detail'
|
||||
@ -28,6 +29,8 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
|
||||
with scopes_disabled():
|
||||
recipe_1_s1.space = space_2
|
||||
recipe_1_s1.save()
|
||||
Step.objects.update(space=Subquery(Step.objects.filter(pk=OuterRef('pk')).values('recipe__space')[:1]))
|
||||
Ingredient.objects.update(space=Subquery(Ingredient.objects.filter(pk=OuterRef('pk')).values('step__recipe__space')[:1]))
|
||||
|
||||
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0
|
||||
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 2
|
||||
|
@ -51,8 +51,8 @@ def get_random_recipe(space_1, u1_s1):
|
||||
internal=True,
|
||||
)
|
||||
|
||||
s1 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), )
|
||||
s2 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), )
|
||||
s1 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), space=space_1, )
|
||||
s2 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), space=space_1, )
|
||||
|
||||
r.steps.add(s1)
|
||||
r.steps.add(s2)
|
||||
@ -64,6 +64,7 @@ def get_random_recipe(space_1, u1_s1):
|
||||
food=Food.objects.create(name=uuid.uuid4(), space=space_1, ),
|
||||
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
|
||||
note=uuid.uuid4(),
|
||||
space=space_1,
|
||||
)
|
||||
)
|
||||
|
||||
@ -73,6 +74,7 @@ def get_random_recipe(space_1, u1_s1):
|
||||
food=Food.objects.create(name=uuid.uuid4(), space=space_1, ),
|
||||
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
|
||||
note=uuid.uuid4(),
|
||||
space=space_1,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -773,7 +773,7 @@ COOKPAD = {
|
||||
"text": "Water",
|
||||
"id": 49092
|
||||
},
|
||||
"note": "",
|
||||
"note": "2-3",
|
||||
"original": "2-3 c Water"
|
||||
},
|
||||
{
|
||||
@ -1498,10 +1498,10 @@ GIALLOZAFFERANO = {
|
||||
"id": 64900
|
||||
},
|
||||
"ingredient": {
|
||||
"text": "Pane (raffermo o secco) 80 g",
|
||||
"text": "Pane 80 g",
|
||||
"id": 24720
|
||||
},
|
||||
"note": "",
|
||||
"note": "raffermo o secco",
|
||||
"original": "Pane (raffermo o secco) 80 g"
|
||||
},
|
||||
{
|
||||
|
@ -53,7 +53,12 @@ def test_ingredient_parser():
|
||||
"50 g smör eller margarin": (50, "g", "smör eller margarin", ""),
|
||||
"3,5 l Wasser": (3.5, "l", "Wasser", ""),
|
||||
"3.5 l Wasser": (3.5, "l", "Wasser", ""),
|
||||
"400 g Karotte(n)": (400, "g", "Karotte(n)", "")
|
||||
"400 g Karotte(n)": (400, "g", "Karotte(n)", ""),
|
||||
"400g unsalted butter": (400, "g", "butter", "unsalted"),
|
||||
"2L Wasser": (2, "L", "Wasser", ""),
|
||||
"1 (16 ounce) package dry lentils, rinsed": (1, "package", "dry lentils, rinsed", "16 ounce"),
|
||||
"2-3 c Water": (2, "c", "Water", "2-3"),
|
||||
"Pane (raffermo o secco) 80 g": (0, "", "Pane 80 g", "raffermo o secco"), #TODO this is actually not a good result but currently expected
|
||||
}
|
||||
# for German you could say that if an ingredient does not have
|
||||
# an amount # and it starts with a lowercase letter, then that
|
||||
|
@ -64,6 +64,7 @@ urlpatterns = [
|
||||
path('history/', views.history, name='view_history'),
|
||||
path('supermarket/', views.supermarket, name='view_supermarket'),
|
||||
path('files/', views.files, name='view_files'),
|
||||
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
|
||||
path('test/', views.test, name='view_test'),
|
||||
path('test2/', views.test2, name='view_test2'),
|
||||
|
||||
@ -105,6 +106,7 @@ urlpatterns = [
|
||||
path('api/recipe-from-source/', api.recipe_from_source, name='api_recipe_from_source'),
|
||||
path('api/backup/', api.get_backup, name='api_backup'),
|
||||
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
|
||||
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
|
||||
|
||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
||||
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'),
|
||||
|
@ -16,6 +16,7 @@ from django.db.models import Q
|
||||
from django.http import FileResponse, HttpResponse, JsonResponse
|
||||
from django_scopes import scopes_disabled
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from icalendar import Calendar, Event
|
||||
from recipe_scrapers import scrape_me, WebsiteNotImplementedError, NoSchemaFoundInWildMode
|
||||
@ -28,6 +29,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ViewSetMixin
|
||||
from treebeard.exceptions import PathOverflow, InvalidMoveToDescendant, InvalidPosition
|
||||
|
||||
from cookbook.helper.image_processing import handle_image
|
||||
from cookbook.helper.ingredient_parser import parse
|
||||
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
|
||||
CustomIsOwner, CustomIsShare,
|
||||
@ -41,7 +43,7 @@ from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
|
||||
MealType, Recipe, RecipeBook, ShoppingList,
|
||||
ShoppingListEntry, ShoppingListRecipe, Step,
|
||||
Storage, Sync, SyncLog, Unit, UserPreference,
|
||||
ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile)
|
||||
ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile, ShareLink)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.local import Local
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
@ -482,16 +484,9 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
img = Image.open(obj.image)
|
||||
|
||||
basewidth = 720
|
||||
wpercent = (basewidth / float(img.size[0]))
|
||||
hsize = int((float(img.size[1]) * float(wpercent)))
|
||||
img = img.resize((basewidth, hsize), Image.ANTIALIAS)
|
||||
|
||||
im_io = io.BytesIO()
|
||||
img.save(im_io, 'PNG', quality=70)
|
||||
obj.image = File(im_io, name=f'{uuid.uuid4()}_{obj.pk}.png')
|
||||
img, filetype = handle_image(request, obj.image)
|
||||
obj.image = File(img, name=f'{uuid.uuid4()}_{obj.pk}{filetype}')
|
||||
obj.save()
|
||||
|
||||
return Response(serializer.data)
|
||||
@ -669,6 +664,16 @@ def sync_all(request):
|
||||
return redirect('list_recipe_import')
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def share_link(request, pk):
|
||||
if request.space.allow_sharing:
|
||||
recipe = get_object_or_404(Recipe, pk=pk, space=request.space)
|
||||
link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space)
|
||||
return JsonResponse({'pk': pk, 'share': link.uuid, 'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
|
||||
else:
|
||||
return JsonResponse({'error': 'sharing_disabled'}, status=403)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
@ajax_request
|
||||
def log_cooking(request, recipe_id):
|
||||
|
@ -17,6 +17,7 @@ from PIL import Image, UnidentifiedImageError
|
||||
from requests.exceptions import MissingSchema
|
||||
|
||||
from cookbook.forms import BatchEditForm, SyncForm
|
||||
from cookbook.helper.image_processing import handle_image
|
||||
from cookbook.helper.permission_helper import group_required, has_group_permission
|
||||
from cookbook.helper.recipe_url_import import parse_cooktime
|
||||
from cookbook.models import (Comment, Food, Ingredient, Keyword, Recipe,
|
||||
@ -142,7 +143,7 @@ def import_url(request):
|
||||
)
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction=data['recipeInstructions'],
|
||||
instruction=data['recipeInstructions'], space=request.space,
|
||||
)
|
||||
|
||||
recipe.steps.add(step)
|
||||
@ -157,7 +158,7 @@ def import_url(request):
|
||||
recipe.keywords.add(k)
|
||||
|
||||
for ing in data['recipeIngredient']:
|
||||
ingredient = Ingredient()
|
||||
ingredient = Ingredient(space=request.space,)
|
||||
|
||||
if ing['ingredient']['text'] != '':
|
||||
ingredient.food, f_created = Food.objects.get_or_create(
|
||||
@ -188,23 +189,20 @@ def import_url(request):
|
||||
if 'image' in data and data['image'] != '' and data['image'] is not None:
|
||||
try:
|
||||
response = requests.get(data['image'])
|
||||
img = Image.open(BytesIO(response.content))
|
||||
|
||||
# todo move image processing to dedicated function
|
||||
basewidth = 720
|
||||
wpercent = (basewidth / float(img.size[0]))
|
||||
hsize = int((float(img.size[1]) * float(wpercent)))
|
||||
img = img.resize((basewidth, hsize), Image.ANTIALIAS)
|
||||
|
||||
im_io = BytesIO()
|
||||
img.save(im_io, 'PNG', quality=70)
|
||||
img, filetype = handle_image(request, BytesIO(response.content))
|
||||
recipe.image = File(
|
||||
im_io, name=f'{uuid.uuid4()}_{recipe.pk}.png'
|
||||
img, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}'
|
||||
)
|
||||
recipe.save()
|
||||
except UnidentifiedImageError:
|
||||
except UnidentifiedImageError as e:
|
||||
print(e)
|
||||
pass
|
||||
except MissingSchema:
|
||||
except MissingSchema as e:
|
||||
print(e)
|
||||
pass
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
return HttpResponse(reverse('view_recipe', args=[recipe.pk]))
|
||||
|
@ -41,7 +41,7 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
|
||||
obj.space = self.request.space
|
||||
obj.internal = True
|
||||
obj.save()
|
||||
obj.steps.add(Step.objects.create())
|
||||
obj.steps.add(Step.objects.create(space=self.request.space))
|
||||
return HttpResponseRedirect(reverse('edit_recipe', kwargs={'pk': obj.pk}))
|
||||
|
||||
def get_success_url(self):
|
||||
|
@ -13,7 +13,7 @@ from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Avg, Q, Sum
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
@ -26,11 +26,13 @@ from cookbook.filters import RecipeFilter
|
||||
from cookbook.forms import (CommentForm, Recipe, User,
|
||||
UserCreateForm, UserNameForm, UserPreference,
|
||||
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm,
|
||||
SearchPreferenceForm, AllAuthSignupForm)
|
||||
SearchPreferenceForm, AllAuthSignupForm,
|
||||
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm, AllAuthSignupForm)
|
||||
from cookbook.helper.ingredient_parser import parse
|
||||
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
|
||||
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
|
||||
RecipeBook, RecipeBookEntry, ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit,
|
||||
Food, UserFile)
|
||||
Food, UserFile, ShareLink)
|
||||
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
|
||||
ViewLogTable, InviteLinkTable)
|
||||
from cookbook.views.data import Object
|
||||
@ -116,15 +118,17 @@ def no_space(request):
|
||||
created_space = Space.objects.create(
|
||||
name=create_form.cleaned_data['name'],
|
||||
created_by=request.user,
|
||||
allow_files=settings.SPACE_DEFAULT_FILES,
|
||||
max_file_storage_mb=settings.SPACE_DEFAULT_MAX_FILES,
|
||||
max_recipes=settings.SPACE_DEFAULT_MAX_RECIPES,
|
||||
max_users=settings.SPACE_DEFAULT_MAX_USERS,
|
||||
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
|
||||
)
|
||||
request.user.userpreference.space = created_space
|
||||
request.user.userpreference.save()
|
||||
request.user.groups.add(Group.objects.filter(name='admin').get())
|
||||
|
||||
messages.add_message(request, messages.SUCCESS, _('You have successfully created your own recipe space. Start by adding some recipes or invite other people to join you.'))
|
||||
messages.add_message(request, messages.SUCCESS,
|
||||
_('You have successfully created your own recipe space. Start by adding some recipes or invite other people to join you.'))
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
if join_form.is_valid():
|
||||
@ -241,10 +245,12 @@ def supermarket(request):
|
||||
@group_required('user')
|
||||
def files(request):
|
||||
try:
|
||||
current_file_size_mb = UserFile.objects.filter(space=request.space).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
|
||||
current_file_size_mb = UserFile.objects.filter(space=request.space).aggregate(Sum('file_size_kb'))[
|
||||
'file_size_kb__sum'] / 1000
|
||||
except TypeError:
|
||||
current_file_size_mb = 0
|
||||
return render(request, 'files.html', {'current_file_size_mb': current_file_size_mb, 'max_file_size_mb': request.space.max_file_storage_mb})
|
||||
return render(request, 'files.html',
|
||||
{'current_file_size_mb': current_file_size_mb, 'max_file_size_mb': request.space.max_file_storage_mb})
|
||||
|
||||
|
||||
@group_required('user')
|
||||
@ -266,7 +272,9 @@ def meal_plan_entry(request, pk):
|
||||
|
||||
@group_required('user')
|
||||
def latest_shopping_list(request):
|
||||
sl = ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).filter(finished=False, pace=request.space).order_by('-created_at').first()
|
||||
sl = ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).filter(finished=False,
|
||||
space=request.space).order_by(
|
||||
'-created_at').first()
|
||||
|
||||
if sl:
|
||||
return HttpResponseRedirect(reverse('view_shopping', kwargs={'pk': sl.pk}) + '?edit=true')
|
||||
@ -303,8 +311,6 @@ def user_settings(request):
|
||||
active_tab = 'account'
|
||||
|
||||
user_name_form = UserNameForm(instance=request.user)
|
||||
password_form = PasswordChangeForm(request.user)
|
||||
password_form.fields['old_password'].widget.attrs.pop("autofocus", None)
|
||||
|
||||
if request.method == "POST":
|
||||
if 'preference_form' in request.POST:
|
||||
@ -400,7 +406,6 @@ def user_settings(request):
|
||||
return render(request, 'settings.html', {
|
||||
'preference_form': preference_form,
|
||||
'user_name_form': user_name_form,
|
||||
'password_form': password_form,
|
||||
'api_token': api_token,
|
||||
'search_form': search_form,
|
||||
'active_tab': active_tab
|
||||
@ -490,7 +495,8 @@ def invite_link(request, token):
|
||||
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
|
||||
if request.user.is_authenticated:
|
||||
if request.user.userpreference.space:
|
||||
messages.add_message(request, messages.WARNING, _('You are already member of a space and therefore cannot join this one.'))
|
||||
messages.add_message(request, messages.WARNING,
|
||||
_('You are already member of a space and therefore cannot join this one.'))
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
link.used_by = request.user
|
||||
@ -533,7 +539,8 @@ def space(request):
|
||||
|
||||
counts.recipes_no_keyword = Recipe.objects.filter(keywords=None, space=request.space).count()
|
||||
|
||||
invite_links = InviteLinkTable(InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=request.space).all())
|
||||
invite_links = InviteLinkTable(
|
||||
InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=request.space).all())
|
||||
RequestConfig(request, paginate={'per_page': 25}).configure(invite_links)
|
||||
|
||||
return render(request, 'space.html', {'space_users': space_users, 'counts': counts, 'invite_links': invite_links})
|
||||
@ -567,6 +574,19 @@ def space_change_member(request, user_id, space_id, group):
|
||||
return HttpResponseRedirect(reverse('view_space'))
|
||||
|
||||
|
||||
def report_share_abuse(request, token):
|
||||
if not settings.SHARING_ABUSE:
|
||||
messages.add_message(request, messages.WARNING,
|
||||
_('Reporting share links is not enabled for this instance. Please notify the page administrator to report problems.'))
|
||||
else:
|
||||
if link := ShareLink.objects.filter(uuid=token).first():
|
||||
link.abuse_blocked = True
|
||||
link.save()
|
||||
messages.add_message(request, messages.WARNING,
|
||||
_('Recipe sharing link has been disabled! For additional information please contact the page administrator.'))
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
|
||||
def markdown_info(request):
|
||||
return render(request, 'markdown_info.html', {})
|
||||
|
||||
@ -587,6 +607,7 @@ def offline(request):
|
||||
def test(request):
|
||||
if not settings.DEBUG:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
return JsonResponse(parse('Pane (raffermo o secco) 80 g'), safe=False)
|
||||
|
||||
|
||||
def test2(request):
|
||||
|
@ -172,6 +172,7 @@ This zip file can simply be imported into Tandoor.
|
||||
OpenEats does not provide any way to export the data using the interface. Luckily it is relatively easy to export it from the command line.
|
||||
You need to run the command `python manage.py dumpdata recipe ingredient` inside of the application api container.
|
||||
If you followed the default installation method you can use the following command `docker-compose -f docker-prod.yml run --rm --entrypoint 'sh' api ./manage.py dumpdata recipe ingredient`.
|
||||
This command might also work `docker exec -it openeats_api_1 ./manage.py dumpdata recipe ingredient > recipe_ingredients.json`
|
||||
|
||||
Store the outputted json string in a `.json` file and simply import it using the importer. The file should look something like this
|
||||
```json
|
||||
|
@ -36,7 +36,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
|
||||
<a href="https://docs.tandoor.dev/install/docker.html" rel="noopener noreferrer">Installation</a> •
|
||||
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Documentation</a> •
|
||||
<a href="https://app.tandoor.dev/" target="_blank" rel="noopener noreferrer">Demo</a>
|
||||
</p>
|
||||
@ -88,4 +88,4 @@ Over the time tons of features have been added making this the most comprehensiv
|
||||
I am just a single developer with many other interests and obligations so development and support might be slow at times,
|
||||
but I try my best to constantly improve this application.
|
||||
|
||||
If you have any wishes, feature requests, problems or ideas feel free to open an issue on GitHub.
|
||||
If you have any wishes, feature requests, problems or ideas feel free to open an issue on GitHub.
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,42 +18,42 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,42 +18,42 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,42 +18,42 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,42 +18,42 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,42 +18,42 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -17,42 +17,42 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,42 +18,42 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -19,42 +19,42 @@ msgstr ""
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : "
|
||||
"2);\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,42 +18,42 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,42 +18,42 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -17,42 +17,42 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -18,42 +18,42 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -17,42 +17,42 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: .\recipes\settings.py:220
|
||||
#: .\recipes\settings.py:303
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:221
|
||||
#: .\recipes\settings.py:304
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:222
|
||||
#: .\recipes\settings.py:305
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:223
|
||||
#: .\recipes\settings.py:306
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:224
|
||||
#: .\recipes\settings.py:307
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:225
|
||||
#: .\recipes\settings.py:308
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:226
|
||||
#: .\recipes\settings.py:309
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:227
|
||||
#: .\recipes\settings.py:310
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:228
|
||||
#: .\recipes\settings.py:311
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:229
|
||||
#: .\recipes\settings.py:312
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
@ -10,6 +10,7 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.0/ref/settings/
|
||||
"""
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
@ -32,7 +33,8 @@ SOCIAL_DEFAULT_GROUP = os.getenv('SOCIAL_DEFAULT_GROUP', 'guest')
|
||||
|
||||
SPACE_DEFAULT_MAX_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0))
|
||||
SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0))
|
||||
SPACE_DEFAULT_FILES = bool(int(os.getenv('SPACE_DEFAULT_FILES', True)))
|
||||
SPACE_DEFAULT_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 0))
|
||||
SPACE_DEFAULT_ALLOW_SHARING = bool(int(os.getenv('SPACE_DEFAULT_ALLOW_SHARING', True)))
|
||||
|
||||
INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
|
||||
|
||||
@ -66,6 +68,9 @@ DJANGO_TABLES2_PAGE_RANGE = 8
|
||||
HCAPTCHA_SITEKEY = os.getenv('HCAPTCHA_SITEKEY', '')
|
||||
HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
|
||||
|
||||
SHARING_ABUSE = bool(int(os.getenv('SHARING_ABUSE', False)))
|
||||
SHARING_LIMIT = int(os.getenv('SHARING_LIMIT', 0))
|
||||
|
||||
ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm'
|
||||
|
||||
TERMS_URL = os.getenv('TERMS_URL', '')
|
||||
@ -111,15 +116,24 @@ INSTALLED_APPS = [
|
||||
]
|
||||
|
||||
SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split(',') if os.getenv('SOCIAL_PROVIDERS') else []
|
||||
SOCIALACCOUNT_EMAIL_VERIFICATION = 'none'
|
||||
INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS
|
||||
|
||||
ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE = True
|
||||
ACCOUNT_MAX_EMAIL_ADDRESSES = 3
|
||||
ACCOUNT_EMAIL_REQUIRED = True
|
||||
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 90
|
||||
ACCOUNT_LOGOUT_ON_GET = True
|
||||
|
||||
SOCIALACCOUNT_PROVIDERS = ast.literal_eval(
|
||||
os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
|
||||
try:
|
||||
SOCIALACCOUNT_PROVIDERS = ast.literal_eval(
|
||||
os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
|
||||
except ValueError:
|
||||
SOCIALACCOUNT_PROVIDERS = json.loads(
|
||||
os.getenv('SOCIALACCOUNT_PROVIDERS').replace("'", '"') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
|
||||
|
||||
SESSION_COOKIE_DOMAIN = os.getenv('SESSION_COOKIE_DOMAIN', None)
|
||||
SESSION_COOKIE_NAME = os.getenv('SESSION_COOKIE_NAME', 'sessionid')
|
||||
|
||||
ENABLE_SIGNUP = bool(int(os.getenv('ENABLE_SIGNUP', False)))
|
||||
|
||||
|
@ -3,7 +3,7 @@ cryptography==3.4.7
|
||||
django-annoying==0.10.6
|
||||
django-autocomplete-light==3.8.2
|
||||
django-cleanup==5.2.0
|
||||
django-crispy-forms==1.11.2
|
||||
django-crispy-forms==1.12.0
|
||||
django-emoji-picker==0.0.6
|
||||
django-filter==2.4.0
|
||||
django-tables2==2.4.0
|
||||
@ -15,8 +15,8 @@ gunicorn==20.1.0
|
||||
lxml==4.6.3
|
||||
Markdown==3.3.4
|
||||
Pillow==8.2.0
|
||||
psycopg2-binary==2.8.6
|
||||
python-dotenv==0.17.1
|
||||
psycopg2-binary==2.9.1
|
||||
python-dotenv==0.18.0
|
||||
requests==2.25.1
|
||||
simplejson==3.17.2
|
||||
six==1.16.0
|
||||
@ -28,16 +28,16 @@ uritemplate==3.0.1
|
||||
beautifulsoup4==4.9.3
|
||||
microdata==0.7.1
|
||||
Jinja2==3.0.1
|
||||
django-webpack-loader==1.0.0
|
||||
django-webpack-loader==1.1.0
|
||||
django-js-reverse==0.9.1
|
||||
django-allauth==0.44.0
|
||||
recipe-scrapers==13.2.7
|
||||
recipe-scrapers==13.3.0
|
||||
django-scopes==1.2.0
|
||||
pytest==6.2.4
|
||||
pytest-django==4.4.0
|
||||
django-cors-headers==3.7.0
|
||||
django-treebeard==4.5.1
|
||||
django-storages==1.11.1
|
||||
boto3==1.17.90
|
||||
boto3==1.17.102
|
||||
django-prometheus==2.1.0
|
||||
django-hCaptcha==0.1.0
|
||||
|
@ -44,7 +44,7 @@
|
||||
"eslint-plugin-vue": "^7.10.0",
|
||||
"typescript": "~4.3.2",
|
||||
"vue-cli-plugin-i18n": "^2.1.1",
|
||||
"webpack-bundle-tracker": "1.0.0",
|
||||
"webpack-bundle-tracker": "1.1.0",
|
||||
"workbox-expiration": "^6.0.2",
|
||||
"workbox-navigation-preload": "^6.0.2",
|
||||
"workbox-precaching": "^6.0.2",
|
||||
|
@ -1,54 +1,42 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<h2>{{ $t('Import') }}</h2>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<template v-if="import_info !== undefined">
|
||||
|
||||
<template v-if="import_info.running" style="text-align: center;">
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<loading-spinner></loading-spinner>
|
||||
<br/>
|
||||
<br/>
|
||||
<template v-if="import_info.running">
|
||||
<h5 style="text-align: center">{{ $t('Importing') }}...</h5>
|
||||
|
||||
<b-progress :max="import_info.total_recipes">
|
||||
<b-progress-bar :value="import_info.imported_recipes" :label="`${import_info.imported_recipes}/${import_info.total_recipes}`"></b-progress-bar>
|
||||
</b-progress>
|
||||
|
||||
<loading-spinner :size="25"></loading-spinner>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<span>{{ $t('Import_finished') }}! </span>
|
||||
<a :href="`${resolveDjangoUrl('view_search') }?keyword=${import_info.keyword.id}`"
|
||||
v-if="import_info.keyword !== null">{{ $t('View_Recipes') }}</a>
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12" v-if="!import_info.running">
|
||||
<span>{{ $t('Import_finished') }}! </span>
|
||||
<a :href="`${resolveDjangoUrl('view_search') }?keyword=${import_info.keyword.id}`"
|
||||
v-if="import_info.keyword !== null">{{ $t('View_Recipes') }}</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<label for="id_textarea">{{ $t('Information') }}</label>
|
||||
<textarea id="id_textarea" class="form-control" style="height: 50vh" v-html="import_info.msg"
|
||||
disabled></textarea>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<label for="id_textarea">{{ $t('Information') }}</label>
|
||||
<textarea id="id_textarea" ref="output_text" class="form-control" style="height: 50vh"
|
||||
v-html="import_info.msg"
|
||||
disabled></textarea>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@ -90,6 +78,8 @@ export default {
|
||||
setInterval(() => {
|
||||
if ((this.import_id !== null) && window.navigator.onLine && this.import_info.running) {
|
||||
this.refreshData()
|
||||
let el = this.$refs.output_text
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
@ -100,6 +90,7 @@ export default {
|
||||
|
||||
apiClient.retrieveImportLog(this.import_id).then(result => {
|
||||
this.import_info = result.data
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,9 @@
|
||||
<b-input-group class="mt-3">
|
||||
<b-input class="form-control" v-model="settings.search_input" v-bind:placeholder="$t('Search')"></b-input>
|
||||
<b-input-group-append>
|
||||
<b-button v-b-toggle.collapse_advanced_search variant="primary" class="shadow-none"><i
|
||||
<b-button v-b-toggle.collapse_advanced_search
|
||||
v-bind:class="{'btn-primary': !isAdvancedSettingsSet(), 'btn-danger': isAdvancedSettingsSet()}"
|
||||
class="shadow-none btn"><i
|
||||
class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i><i class="fas fa-caret-up"
|
||||
v-if="settings.advanced_search_visible"></i>
|
||||
</b-button>
|
||||
@ -37,21 +39,16 @@
|
||||
:href="resolveDjangoUrl('data_import_url')">{{ $t('Import') }}</a>
|
||||
</div>
|
||||
<div class="col-md-3" style="margin-top: 1vh">
|
||||
<button class="btn btn-primary btn-block text-uppercase" @click="resetSearch">
|
||||
{{ $t('Reset_Search') }}
|
||||
<button class="btn btn-block text-uppercase" v-b-tooltip.hover :title="$t('show_only_internal')"
|
||||
v-bind:class="{'btn-success':settings.search_internal, 'btn-primary':!settings.search_internal}"
|
||||
@click="settings.search_internal = !settings.search_internal;refreshData()">
|
||||
{{ $t('Internal') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2" style="position: relative; margin-top: 1vh">
|
||||
<b-form-checkbox v-model="settings.search_internal" name="check-button"
|
||||
@change="refreshData(false)"
|
||||
class="shadow-none"
|
||||
style="position:relative;top: 50%; transform: translateY(-50%);" switch>
|
||||
{{ $t('show_only_internal') }}
|
||||
</b-form-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="col-md-1" style="position: relative; margin-top: 1vh">
|
||||
<button id="id_settings_button" class="btn btn-primary btn-block"><i class="fas fa-cog"></i>
|
||||
<div class="col-md-3" style="position: relative; margin-top: 1vh">
|
||||
<button id="id_settings_button" class="btn btn-primary btn-block text-uppercase"><i
|
||||
class="fas fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -181,7 +178,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 2vh">
|
||||
<div class="row">
|
||||
<div class="col col-md-12 text-right" style="margin-top: 2vh">
|
||||
<span class="text-muted">
|
||||
{{ $t('Page') }} {{ settings.pagination_page }}/{{ pagination_count }} <a href="#" @click="resetSearch"><i
|
||||
class="fas fa-times-circle"></i> {{ $t('Reset') }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
|
||||
|
||||
@ -200,10 +206,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 2vh; text-align: center">
|
||||
<div class="row" style="margin-top: 2vh">
|
||||
<div class="col col-md-12">
|
||||
<b-button @click="loadMore()" class="btn-block btn-success" v-if="pagination_more">{{ $t('Load_More') }}
|
||||
</b-button>
|
||||
<b-pagination pills
|
||||
v-model="settings.pagination_page"
|
||||
:total-rows="pagination_count"
|
||||
per-page="25"
|
||||
@change="pageChange"
|
||||
align="center">
|
||||
|
||||
</b-pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -239,6 +251,8 @@ import GenericMultiselect from "@/components/GenericMultiselect";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
let SETTINGS_COOKIE_NAME = 'search_settings'
|
||||
|
||||
export default {
|
||||
name: 'RecipeSearchView',
|
||||
mixins: [ResolveUrlMixin],
|
||||
@ -249,6 +263,7 @@ export default {
|
||||
meal_plans: [],
|
||||
last_viewed_recipes: [],
|
||||
|
||||
settings_loaded: false,
|
||||
settings: {
|
||||
search_input: '',
|
||||
search_internal: false,
|
||||
@ -262,19 +277,33 @@ export default {
|
||||
advanced_search_visible: false,
|
||||
show_meal_plan: true,
|
||||
recently_viewed: 5,
|
||||
|
||||
pagination_page: 1,
|
||||
},
|
||||
|
||||
pagination_more: true,
|
||||
pagination_page: 1,
|
||||
pagination_count: 0,
|
||||
}
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(function () {
|
||||
if (this.$cookies.isKey('search_settings_v2')) {
|
||||
this.settings = this.$cookies.get("search_settings_v2")
|
||||
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
|
||||
let cookie_val = this.$cookies.get(SETTINGS_COOKIE_NAME)
|
||||
for (let i of Object.keys(cookie_val)) {
|
||||
this.$set(this.settings, i, cookie_val[i])
|
||||
}
|
||||
//TODO i have no idea why the above code does not suffice to update the
|
||||
//TODO pagination UI element as $set should update all values reactively but it does not
|
||||
setTimeout(function () {
|
||||
this.$set(this.settings, 'pagination_page', 0)
|
||||
}.bind(this), 50)
|
||||
setTimeout(function () {
|
||||
this.$set(this.settings, 'pagination_page', cookie_val['pagination_page'])
|
||||
}.bind(this), 51)
|
||||
|
||||
}
|
||||
|
||||
|
||||
let urlParams = new URLSearchParams(window.location.search);
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
@ -284,7 +313,7 @@ export default {
|
||||
let keyword = {id: x, name: 'loading'}
|
||||
this.settings.search_keywords.push(keyword)
|
||||
apiClient.retrieveKeyword(x).then(result => {
|
||||
this.$set(this.settings.search_keywords,this.settings.search_keywords.indexOf(keyword), result.data)
|
||||
this.$set(this.settings.search_keywords, this.settings.search_keywords.indexOf(keyword), result.data)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -299,7 +328,7 @@ export default {
|
||||
watch: {
|
||||
settings: {
|
||||
handler() {
|
||||
this.$cookies.set("search_settings_v2", this.settings, -1)
|
||||
this.$cookies.set(SETTINGS_COOKIE_NAME, this.settings, -1)
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
@ -333,16 +362,11 @@ export default {
|
||||
|
||||
this.settings.search_internal,
|
||||
undefined,
|
||||
this.pagination_page,
|
||||
this.settings.pagination_page,
|
||||
).then(result => {
|
||||
this.pagination_more = (result.data.next !== null)
|
||||
if (page_load) {
|
||||
for (let x of result.data.results) {
|
||||
this.recipes.push(x)
|
||||
}
|
||||
} else {
|
||||
this.recipes = result.data.results
|
||||
}
|
||||
window.scrollTo(0, 0);
|
||||
this.pagination_count = result.data.count
|
||||
this.recipes = result.data.results
|
||||
})
|
||||
},
|
||||
loadMealPlan: function () {
|
||||
@ -384,11 +408,15 @@ export default {
|
||||
this.settings.search_keywords = []
|
||||
this.settings.search_foods = []
|
||||
this.settings.search_books = []
|
||||
this.settings.pagination_page = 1
|
||||
this.refreshData(false)
|
||||
},
|
||||
loadMore: function (page) {
|
||||
this.pagination_page++
|
||||
this.refreshData(true)
|
||||
pageChange: function (page) {
|
||||
this.settings.pagination_page = page
|
||||
this.refreshData()
|
||||
},
|
||||
isAdvancedSettingsSet() {
|
||||
return ((this.settings.search_keywords.length + this.settings.search_foods.length + this.settings.search_books.length) > 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user