Merge branch 'develop' into feature/unit-conversion

# Conflicts:
#	requirements.txt
This commit is contained in:
vabene1111 2023-04-30 21:51:56 +02:00
commit 89e3e85d1e
72 changed files with 5733 additions and 3812 deletions

View File

@ -158,6 +158,7 @@ REVERSE_PROXY_AUTH=0
#AUTH_LDAP_BIND_PASSWORD=
#AUTH_LDAP_USER_SEARCH_BASE_DN=
#AUTH_LDAP_TLS_CACERTFILE=
#AUTH_LDAP_START_TLS=
# Enables exporting PDF (see export docs)
# Disabled by default, uncomment to enable

View File

@ -115,13 +115,17 @@ jobs:
needs: build-container
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Set tag name
run: |
# Strip "refs/tags/" prefix
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
# Send stable discord notification
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2
with:
args: '🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}'
args: '🚀 Version {{ VERSION }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ VERSION }}'
notify-beta:
name: Notify Beta

View File

@ -3,9 +3,9 @@ from collections import Counter
from datetime import date, timedelta
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
from django.core.cache import cache
from django.core.cache import caches
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value, When, FilteredRelation)
from django.core.cache import cache, caches
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value,
When)
from django.db.models.functions import Coalesce, Lower, Substr
from django.utils import timezone, translation
from django.utils.translation import gettext as _
@ -20,7 +20,8 @@ from recipes import settings
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
class RecipeSearch():
_postgres = settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
_postgres = settings.DATABASES['default']['ENGINE'] in [
'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
def __init__(self, request, **params):
self._request = request
@ -45,7 +46,8 @@ class RecipeSearch():
cache.set(CACHE_KEY, self._search_prefs, timeout=10)
else:
self._search_prefs = SearchPreference()
self._string = self._params.get('query').strip() if self._params.get('query', None) else None
self._string = self._params.get('query').strip(
) if self._params.get('query', None) else None
self._rating = self._params.get('rating', None)
self._keywords = {
'or': self._params.get('keywords_or', None) or self._params.get('keywords', None),
@ -74,7 +76,8 @@ class RecipeSearch():
self._random = str2bool(self._params.get('random', False))
self._new = str2bool(self._params.get('new', False))
self._num_recent = int(self._params.get('num_recent', 0))
self._include_children = str2bool(self._params.get('include_children', None))
self._include_children = str2bool(
self._params.get('include_children', None))
self._timescooked = self._params.get('timescooked', None)
self._cookedon = self._params.get('cookedon', None)
self._createdon = self._params.get('createdon', None)
@ -95,18 +98,24 @@ class RecipeSearch():
self._search_type = self._search_prefs.search or 'plain'
if self._string:
if self._postgres:
self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
self._unaccent_include = self._search_prefs.unaccent.values_list(
'field', flat=True)
else:
self._unaccent_include = []
self._icontains_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
self._istartswith_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
self._icontains_include = [
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
self._istartswith_include = [
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
self._trigram_include = None
self._fulltext_include = None
self._trigram = False
if self._postgres and self._string:
self._language = DICTIONARY.get(translation.get_language(), 'simple')
self._trigram_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
self._fulltext_include = self._search_prefs.fulltext.values_list('field', flat=True) or None
self._language = DICTIONARY.get(
translation.get_language(), 'simple')
self._trigram_include = [
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
self._fulltext_include = self._search_prefs.fulltext.values_list(
'field', flat=True) or None
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
self._trigram = True
@ -182,8 +191,10 @@ class RecipeSearch():
# otherwise sort by the remaining order_by attributes or favorite by default
else:
order += default_order
order[:] = [Lower('name').asc() if x == 'name' else x for x in order]
order[:] = [Lower('name').desc() if x == '-name' else x for x in order]
order[:] = [Lower('name').asc() if x ==
'name' else x for x in order]
order[:] = [Lower('name').desc() if x ==
'-name' else x for x in order]
self.orderby = order
def string_filters(self, string=None):
@ -200,21 +211,28 @@ class RecipeSearch():
for f in self._filters:
query_filter |= f
self._queryset = self._queryset.filter(query_filter).distinct() # this creates duplicate records which can screw up other aggregates, see makenow for workaround
# this creates duplicate records which can screw up other aggregates, see makenow for workaround
self._queryset = self._queryset.filter(query_filter).distinct()
if self._fulltext_include:
if self._fuzzy_match is None:
self._queryset = self._queryset.annotate(score=Coalesce(Max(self.search_rank), 0.0))
self._queryset = self._queryset.annotate(
score=Coalesce(Max(self.search_rank), 0.0))
else:
self._queryset = self._queryset.annotate(rank=Coalesce(Max(self.search_rank), 0.0))
self._queryset = self._queryset.annotate(
rank=Coalesce(Max(self.search_rank), 0.0))
if self._fuzzy_match is not None:
simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity')
simularity = self._fuzzy_match.filter(
pk=OuterRef('pk')).values('simularity')
if not self._fulltext_include:
self._queryset = self._queryset.annotate(score=Coalesce(Subquery(simularity), 0.0))
self._queryset = self._queryset.annotate(
score=Coalesce(Subquery(simularity), 0.0))
else:
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
self._queryset = self._queryset.annotate(
simularity=Coalesce(Subquery(simularity), 0.0))
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
self._queryset = self._queryset.annotate(score=F('rank') + F('simularity'))
self._queryset = self._queryset.annotate(
score=F('rank') + F('simularity'))
else:
query_filter = Q()
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
@ -223,7 +241,8 @@ class RecipeSearch():
def _cooked_on_filter(self, cooked_date=None):
if self._sort_includes('lastcooked') or cooked_date:
lessthan = self._sort_includes('-lastcooked') or '-' in (cooked_date or [])[:1]
lessthan = self._sort_includes(
'-lastcooked') or '-' in (cooked_date or [])[:1]
if lessthan:
default = timezone.now() - timedelta(days=100000)
else:
@ -233,32 +252,41 @@ class RecipeSearch():
if cooked_date is None:
return
cooked_date = date(*[int(x) for x in cooked_date.split('-') if x != ''])
cooked_date = date(*[int(x)
for x in cooked_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
self._queryset = self._queryset.filter(
lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
else:
self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
self._queryset = self._queryset.filter(
lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
def _created_on_filter(self, created_date=None):
if created_date is None:
return
lessthan = '-' in created_date[:1]
created_date = date(*[int(x) for x in created_date.split('-') if x != ''])
created_date = date(*[int(x)
for x in created_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(created_at__date__lte=created_date)
self._queryset = self._queryset.filter(
created_at__date__lte=created_date)
else:
self._queryset = self._queryset.filter(created_at__date__gte=created_date)
self._queryset = self._queryset.filter(
created_at__date__gte=created_date)
def _updated_on_filter(self, updated_date=None):
if updated_date is None:
return
lessthan = '-' in updated_date[:1]
updated_date = date(*[int(x) for x in updated_date.split('-') if x != ''])
updated_date = date(*[int(x)
for x in updated_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(updated_at__date__lte=updated_date)
self._queryset = self._queryset.filter(
updated_at__date__lte=updated_date)
else:
self._queryset = self._queryset.filter(updated_at__date__gte=updated_date)
self._queryset = self._queryset.filter(
updated_at__date__gte=updated_date)
def _viewed_on_filter(self, viewed_date=None):
if self._sort_includes('lastviewed') or viewed_date:
@ -268,12 +296,15 @@ class RecipeSearch():
if viewed_date is None:
return
lessthan = '-' in viewed_date[:1]
viewed_date = date(*[int(x) for x in viewed_date.split('-') if x != ''])
viewed_date = date(*[int(x)
for x in viewed_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
self._queryset = self._queryset.filter(
lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
else:
self._queryset = self._queryset.filter(lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
self._queryset = self._queryset.filter(
lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
def _new_recipes(self, new_days=7):
# TODO make new days a user-setting
@ -293,27 +324,32 @@ class RecipeSearch():
num_recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space).values(
'recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(
pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
def _favorite_recipes(self, times_cooked=None):
if self._sort_includes('favorite') or times_cooked:
less_than = '-' in (times_cooked or []) or not self._sort_includes('-favorite')
less_than = '-' in (times_cooked or []
) and not self._sort_includes('-favorite')
if less_than:
default = 1000
else:
default = 0
favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk')
).values('recipe').annotate(count=Count('pk', distinct=True)).values('count')
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
self._queryset = self._queryset.annotate(
favorite=Coalesce(Subquery(favorite_recipes), default))
if times_cooked is None:
return
if times_cooked == '0':
self._queryset = self._queryset.filter(favorite=0)
elif less_than:
self._queryset = self._queryset.filter(favorite__lte=int(times_cooked[1:])).exclude(favorite=0)
self._queryset = self._queryset.filter(favorite__lte=int(
times_cooked.replace('-', ''))).exclude(favorite=0)
else:
self._queryset = self._queryset.filter(favorite__gte=int(times_cooked))
self._queryset = self._queryset.filter(
favorite__gte=int(times_cooked))
def keyword_filters(self, **kwargs):
if all([kwargs[x] is None for x in kwargs]):
@ -346,7 +382,8 @@ class RecipeSearch():
else:
self._queryset = self._queryset.filter(f_and)
if 'not' in kw_filter:
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
self._queryset = self._queryset.exclude(
id__in=recipes.values('id'))
def food_filters(self, **kwargs):
if all([kwargs[x] is None for x in kwargs]):
@ -360,7 +397,8 @@ class RecipeSearch():
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
if 'or' in fd_filter:
if self._include_children:
f_or = Q(steps__ingredients__food__in=Food.include_descendants(foods))
f_or = Q(
steps__ingredients__food__in=Food.include_descendants(foods))
else:
f_or = Q(steps__ingredients__food__in=foods)
@ -372,7 +410,8 @@ class RecipeSearch():
recipes = Recipe.objects.all()
for food in foods:
if self._include_children:
f_and = Q(steps__ingredients__food__in=food.get_descendants_and_self())
f_and = Q(
steps__ingredients__food__in=food.get_descendants_and_self())
else:
f_and = Q(steps__ingredients__food=food)
if 'not' in fd_filter:
@ -380,7 +419,8 @@ class RecipeSearch():
else:
self._queryset = self._queryset.filter(f_and)
if 'not' in fd_filter:
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
self._queryset = self._queryset.exclude(
id__in=recipes.values('id'))
def unit_filters(self, units=None, operator=True):
if operator != True:
@ -389,7 +429,8 @@ class RecipeSearch():
return
if not isinstance(units, list):
units = [units]
self._queryset = self._queryset.filter(steps__ingredients__unit__in=units)
self._queryset = self._queryset.filter(
steps__ingredients__unit__in=units)
def rating_filter(self, rating=None):
if rating or self._sort_includes('rating'):
@ -399,14 +440,16 @@ class RecipeSearch():
else:
default = 0
# TODO make ratings a settings user-only vs all-users
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(
cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
if rating is None:
return
if rating == '0':
self._queryset = self._queryset.filter(rating=0)
elif lessthan:
self._queryset = self._queryset.filter(rating__lte=int(rating[1:])).exclude(rating=0)
self._queryset = self._queryset.filter(
rating__lte=int(rating[1:])).exclude(rating=0)
else:
self._queryset = self._queryset.filter(rating__gte=int(rating))
@ -434,11 +477,14 @@ class RecipeSearch():
recipes = Recipe.objects.all()
for book in kwargs[bk_filter]:
if 'not' in bk_filter:
recipes = recipes.filter(recipebookentry__book__id=book)
recipes = recipes.filter(
recipebookentry__book__id=book)
else:
self._queryset = self._queryset.filter(recipebookentry__book__id=book)
self._queryset = self._queryset.filter(
recipebookentry__book__id=book)
if 'not' in bk_filter:
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
self._queryset = self._queryset.exclude(
id__in=recipes.values('id'))
def step_filters(self, steps=None, operator=True):
if operator != True:
@ -446,7 +492,7 @@ class RecipeSearch():
if not steps:
return
if not isinstance(steps, list):
steps = [unistepsts]
steps = [steps]
self._queryset = self._queryset.filter(steps__id__in=steps)
def build_fulltext_filters(self, string=None):
@ -457,20 +503,25 @@ class RecipeSearch():
rank = []
if 'name' in self._fulltext_include:
vectors.append('name_search_vector')
rank.append(SearchRank('name_search_vector', self.search_query, cover_density=True))
rank.append(SearchRank('name_search_vector',
self.search_query, cover_density=True))
if 'description' in self._fulltext_include:
vectors.append('desc_search_vector')
rank.append(SearchRank('desc_search_vector', self.search_query, cover_density=True))
rank.append(SearchRank('desc_search_vector',
self.search_query, cover_density=True))
if 'steps__instruction' in self._fulltext_include:
vectors.append('steps__search_vector')
rank.append(SearchRank('steps__search_vector', self.search_query, cover_density=True))
rank.append(SearchRank('steps__search_vector',
self.search_query, cover_density=True))
if 'keywords__name' in self._fulltext_include:
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
vectors.append('keywords__name__unaccent')
rank.append(SearchRank('keywords__name__unaccent', self.search_query, cover_density=True))
rank.append(SearchRank('keywords__name__unaccent',
self.search_query, cover_density=True))
if 'steps__ingredients__food__name' in self._fulltext_include:
vectors.append('steps__ingredients__food__name__unaccent')
rank.append(SearchRank('steps__ingredients__food__name', self.search_query, cover_density=True))
rank.append(SearchRank('steps__ingredients__food__name',
self.search_query, cover_density=True))
for r in rank:
if self.search_rank is None:
@ -478,7 +529,8 @@ class RecipeSearch():
else:
self.search_rank += r
# modifying queryset will annotation creates duplicate results
self._filters.append(Q(id__in=Recipe.objects.annotate(vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
self._filters.append(Q(id__in=Recipe.objects.annotate(
vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
def build_text_filters(self, string=None):
if not string:
@ -510,23 +562,30 @@ class RecipeSearch():
def _makenow_filter(self, missing=None):
if missing is None or (type(missing) == bool and missing == False):
return
shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
shopping_users = [
*self._request.user.get_shopping_share(), self._request.user]
onhand_filter = (
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users) # or substitute food onhand
# or substitute food onhand
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users)
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
)
makenow_recipes = Recipe.objects.annotate(
count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
count_food=Count('steps__ingredients__food__pk', filter=Q(
steps__ingredients__food__isnull=False), distinct=True),
count_onhand=Count('steps__ingredients__food__pk',
filter=onhand_filter, distinct=True),
count_ignore_shopping=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True,
steps__ingredients__food__recipe__isnull=True), distinct=True),
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users), then=Value(1)), default=Value(0)),
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)), default=Value(0))
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(
shopping_users), then=Value(1)), default=Value(0)),
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(
shopping_users), then=Value(1)), default=Value(0))
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood=missing)
self._queryset = self._queryset.distinct().filter(id__in=makenow_recipes.values('id'))
self._queryset = self._queryset.distinct().filter(
id__in=makenow_recipes.values('id'))
@staticmethod
def __children_substitute_filter(shopping_users=None):
@ -547,7 +606,8 @@ class RecipeSearch():
@staticmethod
def __sibling_substitute_filter(shopping_users=None):
sibling_onhand_subquery = Food.objects.filter(
path__startswith=Substr(OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
path__startswith=Substr(
OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
depth=OuterRef('depth'),
onhand_users__in=shopping_users
)
@ -586,7 +646,8 @@ class RecipeFacet():
self.Recent = self._cache.get('Recent', None)
if self._queryset is not None:
self._recipe_list = list(self._queryset.values_list('id', flat=True))
self._recipe_list = list(
self._queryset.values_list('id', flat=True))
self._search_params = {
'keyword_list': self._request.query_params.getlist('keywords', []),
'food_list': self._request.query_params.getlist('foods', []),
@ -618,7 +679,8 @@ class RecipeFacet():
'Books': self.Books
}
caches['default'].set(self._SEARCH_CACHE_KEY, self._cache, self._cache_timeout)
caches['default'].set(self._SEARCH_CACHE_KEY,
self._cache, self._cache_timeout)
def get_facets(self, from_cache=False):
if from_cache:
@ -655,13 +717,16 @@ class RecipeFacet():
def get_keywords(self):
if self.Keywords is None:
if self._search_params['search_keywords_or']:
keywords = Keyword.objects.filter(space=self._request.space).distinct()
keywords = Keyword.objects.filter(
space=self._request.space).distinct()
else:
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(
depth=1)).filter(space=self._request.space).distinct()
# set keywords to root objects only
keywords = self._keyword_queryset(keywords)
self.Keywords = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
self.Keywords = [{**x, 'children': None}
if x['numchild'] > 0 else x for x in list(keywords)]
self.set_cache('Keywords', self.Keywords)
return self.Keywords
@ -669,28 +734,28 @@ class RecipeFacet():
if self.Foods is None:
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
if self._search_params['search_foods_or']:
foods = Food.objects.filter(space=self._request.space).distinct()
foods = Food.objects.filter(
space=self._request.space).distinct()
else:
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(
depth=1)).filter(space=self._request.space).distinct()
# set keywords to root objects only
foods = self._food_queryset(foods)
self.Foods = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
self.Foods = [{**x, 'children': None}
if x['numchild'] > 0 else x for x in list(foods)]
self.set_cache('Foods', self.Foods)
return self.Foods
def get_books(self):
if self.Books is None:
self.Books = []
return self.Books
def get_ratings(self):
if self.Ratings is None:
if not self._request.space.demo and self._request.space.show_facet_count:
if self._queryset is None:
self._queryset = Recipe.objects.filter(id__in=self._recipe_list)
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
self._queryset = Recipe.objects.filter(
id__in=self._recipe_list)
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(
cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
self.Ratings = dict(Counter(r.rating for r in rating_qs))
else:
self.Rating = {}
@ -715,10 +780,13 @@ class RecipeFacet():
foods = self._food_queryset(food.get_children(), food)
deep_search = self.Foods
for node in nodes:
index = next((i for i, x in enumerate(deep_search) if x["id"] == node.id), None)
index = next((i for i, x in enumerate(
deep_search) if x["id"] == node.id), None)
deep_search = deep_search[index]['children']
index = next((i for i, x in enumerate(deep_search) if x["id"] == food.id), None)
deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
index = next((i for i, x in enumerate(
deep_search) if x["id"] == food.id), None)
deep_search[index]['children'] = [
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
self.set_cache('Foods', self.Foods)
return self.get_facets()
@ -731,10 +799,13 @@ class RecipeFacet():
keywords = self._keyword_queryset(keyword.get_children(), keyword)
deep_search = self.Keywords
for node in nodes:
index = next((i for i, x in enumerate(deep_search) if x["id"] == node.id), None)
index = next((i for i, x in enumerate(
deep_search) if x["id"] == node.id), None)
deep_search = deep_search[index]['children']
index = next((i for i, x in enumerate(deep_search) if x["id"] == keyword.id), None)
deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
index = next((i for i, x in enumerate(deep_search)
if x["id"] == keyword.id), None)
deep_search[index]['children'] = [
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
self.set_cache('Keywords', self.Keywords)
return self.get_facets()

View File

@ -1,8 +1,8 @@
import random
# import random
import re
from html import unescape
from unicodedata import decomposition
from django.core.cache import caches
from django.utils.dateparse import parse_duration
from django.utils.translation import gettext as _
from isodate import parse_duration as iso_parse_duration
@ -10,9 +10,11 @@ from isodate.isoerror import ISO8601Error
from pytube import YouTube
from recipe_scrapers._utils import get_host_name, get_minutes
from cookbook.helper import recipe_url_import as helper
# from cookbook.helper import recipe_url_import as helper
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.models import Keyword, Automation
from cookbook.models import Automation, Keyword
# from unicodedata import decomposition
# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
@ -127,7 +129,7 @@ def get_from_scraper(scrape, request):
try:
if scrape.author():
keywords.append(scrape.author())
except:
except Exception:
pass
try:
@ -367,10 +369,28 @@ def parse_time(recipe_time):
def parse_keywords(keyword_json, space):
keywords = []
keyword_aliases = {}
# retrieve keyword automation cache if it exists, otherwise build from database
KEYWORD_CACHE_KEY = f'automation_keyword_alias_{space.pk}'
if c := caches['default'].get(KEYWORD_CACHE_KEY, None):
self.food_aliases = c
caches['default'].touch(KEYWORD_CACHE_KEY, 30)
else:
for a in Automation.objects.filter(space=space, disabled=False, type=Automation.KEYWORD_ALIAS).only('param_1', 'param_2').order_by('order').all():
keyword_aliases[a.param_1] = a.param_2
caches['default'].set(KEYWORD_CACHE_KEY, keyword_aliases, 30)
# keywords as list
for kw in keyword_json:
kw = normalize_string(kw)
# if alias exists use that instead
if len(kw) != 0:
if keyword_aliases:
try:
kw = keyword_aliases[kw]
except KeyError:
pass
if k := Keyword.objects.filter(name=kw, space=space).first():
keywords.append({'label': str(k), 'name': k.name, 'id': k.id})
else:

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-29 18:42+0200\n"
"PO-Revision-Date: 2022-05-10 15:32+0000\n"
"Last-Translator: zeon <zeonbg@gmail.com>\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Bulgarian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/bg/>\n"
"Language: bg\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.10.1\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
#: .\cookbook\templates\space.html:49 .\cookbook\templates\stats.html:28
@ -1433,7 +1433,7 @@ msgstr ""
#: .\cookbook\templates\index.html:29
msgid "Search recipe ..."
msgstr "Търсете рецепта..."
msgstr "Търсете рецепта ..."
#: .\cookbook\templates\index.html:44
msgid "New Recipe"

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-29 18:42+0200\n"
"PO-Revision-Date: 2023-03-06 10:55+0000\n"
"Last-Translator: Anders Obro <oebro@duck.com>\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Danish <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/da/>\n"
"Language: da\n"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"PO-Revision-Date: 2021-04-11 15:23+0000\n"
"Last-Translator: Allan Nordhøy <epost@anotheragency.no>\n"
"PO-Revision-Date: 2023-04-17 20:55+0000\n"
"Last-Translator: Espen Sellevåg <buskmenn.drammer03@icloud.com>\n"
"Language-Team: Norwegian Bokmål <http://translate.tandoor.dev/projects/"
"tandoor/recipes-backend/nb_NO/>\n"
"Language: nb_NO\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5.3\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:91
#: .\cookbook\templates\forms\edit_internal_recipe.html:219
@ -34,19 +34,23 @@ msgstr ""
#: .\cookbook\forms.py:46
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
msgstr ""
msgstr "Standard enhet når ny ingrediens legges til en oppskrift."
#: .\cookbook\forms.py:47
msgid ""
"Enables support for fractions in ingredient amounts (e.g. convert decimals "
"to fractions automatically)"
msgstr ""
"Aktiverer støtte for deler av ingrediensmengde (konverterer feks. desimaler "
"til deler automatisk)"
#: .\cookbook\forms.py:48
msgid ""
"Users with whom newly created meal plan/shopping list entries should be "
"shared by default."
msgstr ""
"Brukere som oppretter nye måltidsplaner/handlelister, deler disse "
"oppføringene som standard."
#: .\cookbook\forms.py:49
msgid "Show recently viewed recipes on search page."
@ -58,7 +62,7 @@ msgstr "Antall desimaler ingredienser skal avrundes til."
#: .\cookbook\forms.py:51
msgid "If you want to be able to create and see comments underneath recipes."
msgstr ""
msgstr "Hvis du ønsker å opprette og se kommentarer under oppskrifter."
#: .\cookbook\forms.py:53
msgid ""
@ -67,6 +71,11 @@ msgid ""
"Useful when shopping with multiple people but might use a little bit of "
"mobile data. If lower than instance limit it is reset when saving."
msgstr ""
"0 vil deaktivere automatisk synkronisering. Når en handleliste vises, "
"oppdateres listen med oppgitt antall sekunders mellomrom for å synkronisere "
"endringer fra andre brukere. Nyttig dersom flere brukere handler samtidig. "
"Datatrafikk oppstår når aktiv. Hvis verdien er lavere enn grensen, "
"tilbakestilles den ved lagring."
#: .\cookbook\forms.py:56
msgid "Makes the navbar stick to the top of the page."
@ -100,11 +109,11 @@ msgstr ""
#: .\cookbook\forms.py:97 .\cookbook\forms.py:317
msgid "Path"
msgstr ""
msgstr "Sti"
#: .\cookbook\forms.py:98
msgid "Storage UID"
msgstr ""
msgstr "Lagring UID"
#: .\cookbook\forms.py:121
msgid "Default"
@ -129,7 +138,6 @@ msgid "Old Unit"
msgstr "Gammel enhet"
#: .\cookbook\forms.py:156
#, fuzzy
msgid "Unit that should be replaced."
msgstr "Enhet som skal erstattes."
@ -204,12 +212,11 @@ msgstr ""
#: .\cookbook\views\views.py:112 .\cookbook\views\views.py:116
#: .\cookbook\views\views.py:184
msgid "You do not have the required permissions to view this page!"
msgstr "Du har ikke påkrevd tilgang for å vise denne siden."
msgstr "Du har ikke påkrevd tilgang for å vise denne siden!"
#: .\cookbook\helper\permission_helper.py:141
#, fuzzy
msgid "You are not logged in and therefore cannot view this page!"
msgstr "Du er ikke innlogget og kan derfor ikke vise siden."
msgstr "Du er ikke innlogget og kan derfor ikke vise siden!"
#: .\cookbook\helper\permission_helper.py:145
#: .\cookbook\helper\permission_helper.py:167
@ -379,7 +386,7 @@ msgstr "Finner ikke siden du leter etter."
#: .\cookbook\templates\404.html:33
msgid "Take me Home"
msgstr ""
msgstr "Tilbake til Startsiden"
#: .\cookbook\templates\404.html:35
msgid "Report a Bug"
@ -388,12 +395,12 @@ msgstr "Rapporter en feil"
#: .\cookbook\templates\account\login.html:7
#: .\cookbook\templates\base.html:170
msgid "Login"
msgstr ""
msgstr "Logg inn"
#: .\cookbook\templates\account\login.html:13
#: .\cookbook\templates\account\login.html:28
msgid "Sign In"
msgstr ""
msgstr "Opprett bruker"
#: .\cookbook\templates\account\login.html:38
msgid "Social Login"
@ -401,7 +408,7 @@ msgstr "Sosial innlogging"
#: .\cookbook\templates\account\login.html:39
msgid "You can use any of the following providers to sign in."
msgstr ""
msgstr "Velg en av følgende leverandører for å logge på."
#: .\cookbook\templates\account\logout.html:5
#: .\cookbook\templates\account\logout.html:9
@ -416,20 +423,20 @@ msgstr "Er du sikker på at du vil logge ut?"
#: .\cookbook\templates\account\password_reset.html:5
#: .\cookbook\templates\account\password_reset_done.html:5
msgid "Password Reset"
msgstr ""
msgstr "Nullstill passord"
#: .\cookbook\templates\account\password_reset.html:9
#: .\cookbook\templates\account\password_reset_done.html:9
msgid "Password reset is not implemented for the time being!"
msgstr ""
msgstr "Det er foreløpig ikke implementert funksjon for å nullstille passord!"
#: .\cookbook\templates\account\signup.html:5
msgid "Register"
msgstr ""
msgstr "Registrer"
#: .\cookbook\templates\account\signup.html:9
msgid "Create your Account"
msgstr "Opprett din konto"
msgstr "Opprett konto"
#: .\cookbook\templates\account\signup.html:14
msgid "Create User"
@ -442,11 +449,11 @@ msgstr "API-dokumentasjon"
#: .\cookbook\templates\base.html:78
msgid "Utensils"
msgstr ""
msgstr "Redskaper"
#: .\cookbook\templates\base.html:88
msgid "Shopping"
msgstr ""
msgstr "Handle"
#: .\cookbook\templates\base.html:102 .\cookbook\views\delete.py:84
#: .\cookbook\views\edit.py:93 .\cookbook\views\lists.py:26
@ -456,27 +463,27 @@ msgstr "Nøkkelord"
#: .\cookbook\templates\base.html:104
msgid "Batch Edit"
msgstr ""
msgstr "Oppdatere flere"
#: .\cookbook\templates\base.html:109
msgid "Storage Data"
msgstr ""
msgstr "Datalagring"
#: .\cookbook\templates\base.html:113
msgid "Storage Backends"
msgstr ""
msgstr "Lagringsplasser"
#: .\cookbook\templates\base.html:115
msgid "Configure Sync"
msgstr ""
msgstr "Konfigurer synkronisering"
#: .\cookbook\templates\base.html:117
msgid "Discovered Recipes"
msgstr ""
msgstr "Oppdagede oppskrifter"
#: .\cookbook\templates\base.html:119
msgid "Discovery Log"
msgstr ""
msgstr "Logg Oppdagelser"
#: .\cookbook\templates\base.html:121 .\cookbook\templates\stats.html:10
msgid "Statistics"
@ -484,7 +491,7 @@ msgstr "Statistikk"
#: .\cookbook\templates\base.html:123
msgid "Units & Ingredients"
msgstr ""
msgstr "Enheter & Ingredienser"
#: .\cookbook\templates\base.html:125
msgid "Import Recipe"
@ -521,58 +528,61 @@ msgid "API Browser"
msgstr "API-utforsker"
#: .\cookbook\templates\base.html:165
#, fuzzy
msgid "Logout"
msgstr "Logg ut"
#: .\cookbook\templates\batch\edit.html:6
msgid "Batch edit Category"
msgstr ""
msgstr "Oppdater flere kategorier"
#: .\cookbook\templates\batch\edit.html:15
msgid "Batch edit Recipes"
msgstr ""
msgstr "Oppdater flere oppskrifter"
#: .\cookbook\templates\batch\edit.html:20
msgid "Add the specified keywords to all recipes containing a word"
msgstr ""
msgstr "Legg til spesifikt nøkkelord til alle oppskrifter som inneholder et ord"
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:76
msgid "Sync"
msgstr ""
msgstr "Synkronisering"
#: .\cookbook\templates\batch\monitor.html:10
msgid "Manage watched Folders"
msgstr ""
msgstr "Behandle overvåkede mapper"
#: .\cookbook\templates\batch\monitor.html:14
msgid ""
"On this Page you can manage all storage folder locations that should be "
"monitored and synced."
msgstr ""
"Her kan du behandle alle lagringsmapper og plasseringer for monitorering og "
"synkronisering."
#: .\cookbook\templates\batch\monitor.html:16
msgid "The path must be in the following format"
msgstr ""
msgstr "Stien må være i følgende format"
#: .\cookbook\templates\batch\monitor.html:27
msgid "Sync Now!"
msgstr ""
msgstr "Synkroniser nå!"
#: .\cookbook\templates\batch\waiting.html:4
#: .\cookbook\templates\batch\waiting.html:10
msgid "Importing Recipes"
msgstr ""
msgstr "Importerer oppskrifter"
#: .\cookbook\templates\batch\waiting.html:23
msgid ""
"This can take a few minutes, depending on the number of recipes in sync, "
"please wait."
msgstr ""
"Dette kan ta noen minutter, avhenging av antall oppskrifter som skal "
"synkroniseres. Vennligst vent."
#: .\cookbook\templates\books.html:5 .\cookbook\templates\books.html:11
msgid "Recipe Books"
msgstr ""
msgstr "Oppskriftsbøker"
#: .\cookbook\templates\books.html:15
msgid "New Book"
@ -584,32 +594,32 @@ msgstr "av"
#: .\cookbook\templates\books.html:34
msgid "Toggle Recipes"
msgstr ""
msgstr "Veksle oppskrifter"
#: .\cookbook\templates\books.html:54
#: .\cookbook\templates\meal_plan_entry.html:48
#: .\cookbook\templates\recipes_table.html:64
msgid "Last cooked"
msgstr ""
msgstr "Forrige tilbereding"
#: .\cookbook\templates\books.html:71
msgid "There are no recipes in this book yet."
msgstr ""
msgstr "Det er foreløpig ingen oppskrifter i denne boken."
#: .\cookbook\templates\export.html:6 .\cookbook\templates\test2.html:6
msgid "Export Recipes"
msgstr ""
msgstr "Eksporter oppskrifter"
#: .\cookbook\templates\export.html:14 .\cookbook\templates\export.html:20
#: .\cookbook\templates\shopping_list.html:347
#: .\cookbook\templates\test2.html:14 .\cookbook\templates\test2.html:20
msgid "Export"
msgstr ""
msgstr "Eksporter"
#: .\cookbook\templates\forms\edit_import_recipe.html:5
#: .\cookbook\templates\forms\edit_import_recipe.html:9
msgid "Import new Recipe"
msgstr ""
msgstr "Importer ny oppskrift"
#: .\cookbook\templates\forms\edit_import_recipe.html:14
#: .\cookbook\templates\forms\edit_internal_recipe.html:389
@ -635,29 +645,29 @@ msgstr "Beskrivelse"
#: .\cookbook\templates\forms\edit_internal_recipe.html:72
msgid "Waiting Time"
msgstr ""
msgstr "Ventetid"
#: .\cookbook\templates\forms\edit_internal_recipe.html:78
msgid "Servings Text"
msgstr ""
msgstr "Porsjon beskrivelse"
#: .\cookbook\templates\forms\edit_internal_recipe.html:89
msgid "Select Keywords"
msgstr ""
msgstr "Velg nøkkelord"
#: .\cookbook\templates\forms\edit_internal_recipe.html:90
#: .\cookbook\templates\url_import.html:212
msgid "Add Keyword"
msgstr ""
msgstr "Legg til nøkkelord"
#: .\cookbook\templates\forms\edit_internal_recipe.html:108
msgid "Nutrition"
msgstr ""
msgstr "Næringsinnhold"
#: .\cookbook\templates\forms\edit_internal_recipe.html:112
#: .\cookbook\templates\forms\edit_internal_recipe.html:162
msgid "Delete Step"
msgstr ""
msgstr "Fjern trinn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:116
msgid "Calories"
@ -678,15 +688,15 @@ msgstr "Proteiner"
#: .\cookbook\templates\forms\edit_internal_recipe.html:146
#: .\cookbook\templates\forms\edit_internal_recipe.html:454
msgid "Step"
msgstr ""
msgstr "Trinn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:167
msgid "Show as header"
msgstr ""
msgstr "Vis som overskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:173
msgid "Hide as header"
msgstr ""
msgstr "Skjul overskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:178
msgid "Move Up"
@ -698,15 +708,15 @@ msgstr "Flytt nedover"
#: .\cookbook\templates\forms\edit_internal_recipe.html:192
msgid "Step Name"
msgstr ""
msgstr "Trinn navn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:196
msgid "Step Type"
msgstr ""
msgstr "Trinn type"
#: .\cookbook\templates\forms\edit_internal_recipe.html:207
msgid "Step time in Minutes"
msgstr ""
msgstr "Trinn tid i minutter"
#: .\cookbook\templates\forms\edit_internal_recipe.html:261
#: .\cookbook\templates\shopping_list.html:183
@ -740,7 +750,7 @@ msgstr "Velg mat"
#: .\cookbook\templates\meal_plan.html:256
#: .\cookbook\templates\url_import.html:171
msgid "Note"
msgstr ""
msgstr "Notis"
#: .\cookbook\templates\forms\edit_internal_recipe.html:319
msgid "Delete Ingredient"
@ -748,7 +758,7 @@ msgstr "Slett ingrediens"
#: .\cookbook\templates\forms\edit_internal_recipe.html:325
msgid "Make Header"
msgstr ""
msgstr "Bruk som overskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:331
msgid "Make Ingredient"
@ -756,15 +766,15 @@ msgstr "Opprett ingrediens"
#: .\cookbook\templates\forms\edit_internal_recipe.html:337
msgid "Disable Amount"
msgstr ""
msgstr "Deaktiver mengde"
#: .\cookbook\templates\forms\edit_internal_recipe.html:343
msgid "Enable Amount"
msgstr ""
msgstr "Aktiver mengde"
#: .\cookbook\templates\forms\edit_internal_recipe.html:348
msgid "Copy Template Reference"
msgstr ""
msgstr "Kopier mal-referanse"
#: .\cookbook\templates\forms\edit_internal_recipe.html:374
#: .\cookbook\templates\url_import.html:196
@ -773,29 +783,28 @@ msgstr "Instruksjoner"
#: .\cookbook\templates\forms\edit_internal_recipe.html:387
#: .\cookbook\templates\forms\edit_internal_recipe.html:418
#, fuzzy
msgid "Save & View"
msgstr "Lagre og vis"
#: .\cookbook\templates\forms\edit_internal_recipe.html:391
#: .\cookbook\templates\forms\edit_internal_recipe.html:424
msgid "Add Step"
msgstr ""
msgstr "Legg til trinn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:394
#: .\cookbook\templates\forms\edit_internal_recipe.html:428
msgid "Add Nutrition"
msgstr ""
msgstr "Legg til næringsinnhold"
#: .\cookbook\templates\forms\edit_internal_recipe.html:396
#: .\cookbook\templates\forms\edit_internal_recipe.html:430
msgid "Remove Nutrition"
msgstr ""
msgstr "Fjern næringsinnhold"
#: .\cookbook\templates\forms\edit_internal_recipe.html:398
#: .\cookbook\templates\forms\edit_internal_recipe.html:433
msgid "View Recipe"
msgstr ""
msgstr "Vis oppskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:400
#: .\cookbook\templates\forms\edit_internal_recipe.html:435
@ -804,11 +813,11 @@ msgstr "Slett oppskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:441
msgid "Steps"
msgstr ""
msgstr "Trinn"
#: .\cookbook\templates\forms\ingredients.html:15
msgid "Edit Ingredients"
msgstr ""
msgstr "Rediger ingrediens"
#: .\cookbook\templates\forms\ingredients.html:16
msgid ""
@ -820,54 +829,61 @@ msgid ""
"them.\n"
" "
msgstr ""
"\n"
" Følgende skjema kan brukes dersom, tilfeldigvis, to eller flere "
"enheter eller ingredienser er opprettet,\n"
" og burde være identiske.\n"
" Det slår sammen to enheter eller ingredienser og oppdaterer alle "
"oppskrifter som inneholder disse.\n"
" "
#: .\cookbook\templates\forms\ingredients.html:24
#: .\cookbook\templates\stats.html:26
msgid "Units"
msgstr ""
msgstr "Enheter"
#: .\cookbook\templates\forms\ingredients.html:26
msgid "Are you sure that you want to merge these two units?"
msgstr ""
msgstr "Er du sikker på at du vil slå sammen disse enhetene?"
#: .\cookbook\templates\forms\ingredients.html:31
#: .\cookbook\templates\forms\ingredients.html:40
msgid "Merge"
msgstr "Flett"
msgstr "Slå sammen"
#: .\cookbook\templates\forms\ingredients.html:36
msgid "Are you sure that you want to merge these two ingredients?"
msgstr ""
msgstr "Er du sikker på at du vil slå sammen disse ingrediensene?"
#: .\cookbook\templates\generic\delete_template.html:18
#, python-format
msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
msgstr ""
msgstr "Er du sikker på at du vil slette %(title)s: <b>%(object)s</b> "
#: .\cookbook\templates\generic\delete_template.html:21
msgid "Confirm"
msgstr ""
msgstr "Bekreft"
#: .\cookbook\templates\generic\edit_template.html:30
msgid "View"
msgstr ""
msgstr "Vis"
#: .\cookbook\templates\generic\edit_template.html:34
msgid "Delete original file"
msgstr ""
msgstr "Slett opprinnelig fil"
#: .\cookbook\templates\generic\list_template.html:6
#: .\cookbook\templates\generic\list_template.html:12
msgid "List"
msgstr ""
msgstr "Liste"
#: .\cookbook\templates\generic\list_template.html:25
msgid "Filter"
msgstr ""
msgstr "Filtrer"
#: .\cookbook\templates\generic\list_template.html:30
msgid "Import all"
msgstr ""
msgstr "Importer alle"
#: .\cookbook\templates\generic\new_template.html:6
#: .\cookbook\templates\generic\new_template.html:14
@ -891,19 +907,19 @@ msgstr "Vis logg"
#: .\cookbook\templates\history.html:24
msgid "Cook Log"
msgstr ""
msgstr "Tilberedingslogg"
#: .\cookbook\templates\import.html:6 .\cookbook\templates\test.html:6
msgid "Import Recipes"
msgstr ""
msgstr "Importer oppskrifter"
#: .\cookbook\templates\include\log_cooking.html:7
msgid "Log Recipe Cooking"
msgstr ""
msgstr "Loggfør tilberedt oppskrift"
#: .\cookbook\templates\include\log_cooking.html:13
msgid "All fields are optional and can be left empty."
msgstr ""
msgstr "Alle felt er valgfri og kan stå tomme."
#: .\cookbook\templates\include\log_cooking.html:19
msgid "Rating"
@ -943,44 +959,53 @@ msgid ""
"can be used.\n"
" "
msgstr ""
"\n"
" <b>Passord og nøkkelfeltene</b> er lagret som <b>ren tekst</b> i "
"databasen.\n"
" Dette er nødvendig for å kunne utføre API-forespørsler, men det øker "
"samtidig risiko for\n"
" uønsket tilgang til dem.<br/>\n"
" For å begrense kosekvensene av uønsket tilgang, kan nøkler eller "
"kontoer med begrenset tilgang benyttes.\n"
" "
#: .\cookbook\templates\index.html:29
msgid "Search recipe ..."
msgstr ""
msgstr "Søk etter oppskrift..."
#: .\cookbook\templates\index.html:44
msgid "New Recipe"
msgstr ""
msgstr "Ny oppskrift"
#: .\cookbook\templates\index.html:47
msgid "Website Import"
msgstr ""
msgstr "Importer fra nettside"
#: .\cookbook\templates\index.html:53
msgid "Advanced Search"
msgstr ""
msgstr "Avansert søk"
#: .\cookbook\templates\index.html:57
msgid "Reset Search"
msgstr ""
msgstr "Nullstill søk"
#: .\cookbook\templates\index.html:85
msgid "Last viewed"
msgstr ""
msgstr "Sist sett"
#: .\cookbook\templates\index.html:87 .\cookbook\templates\meal_plan.html:178
#: .\cookbook\templates\stats.html:22
msgid "Recipes"
msgstr ""
msgstr "Oppskrifter"
#: .\cookbook\templates\index.html:94
msgid "Log in to view recipes"
msgstr ""
msgstr "Logg inn for å se oppskrifter"
#: .\cookbook\templates\markdown_info.html:5
#: .\cookbook\templates\markdown_info.html:13
msgid "Markdown Info"
msgstr ""
msgstr "Markdown informasjon"
#: .\cookbook\templates\markdown_info.html:14
msgid ""
@ -997,43 +1022,56 @@ msgid ""
"below.\n"
" "
msgstr ""
"\n"
" Markdown er et lettvekts markup språk som benyttes for å formatere "
"ren tekst.\n"
" Denne siden bruker biblioteket <a href=\"https://python-markdown."
"github.io/\" target=\"_blank\">Python Markdown</a> for\n"
" å konvertere teksten din til velformatert HTML. Fullstendig "
"dokumentasjon for markdown finner du\n"
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" "
"target=\"_blank\">her</a>.\n"
" En ufullstendig, men sannsynligvis tilstrekkelig dokumentasjon "
"finner du under her.\n"
" "
#: .\cookbook\templates\markdown_info.html:25
msgid "Headers"
msgstr ""
msgstr "Overskrifter"
#: .\cookbook\templates\markdown_info.html:54
msgid "Formatting"
msgstr ""
msgstr "Formatering"
#: .\cookbook\templates\markdown_info.html:56
#: .\cookbook\templates\markdown_info.html:72
msgid "Line breaks are inserted by adding two spaces after the end of a line"
msgstr ""
"Linjeskift er satt inn ved å sette inn to mellomrom på slutten av en linje"
#: .\cookbook\templates\markdown_info.html:57
#: .\cookbook\templates\markdown_info.html:73
msgid "or by leaving a blank line inbetween."
msgstr ""
msgstr "eller ved å sette inn en tom linje mellom."
#: .\cookbook\templates\markdown_info.html:59
#: .\cookbook\templates\markdown_info.html:74
msgid "This text is bold"
msgstr ""
msgstr "Denne teksten er Fet"
#: .\cookbook\templates\markdown_info.html:60
#: .\cookbook\templates\markdown_info.html:75
msgid "This text is italic"
msgstr ""
msgstr "Denne teksten er Kursiv"
#: .\cookbook\templates\markdown_info.html:61
#: .\cookbook\templates\markdown_info.html:77
msgid "Blockquotes are also possible"
msgstr ""
msgstr "Det er også mulig å sitere avsnitt"
#: .\cookbook\templates\markdown_info.html:84
msgid "Lists"
msgstr ""
msgstr "Lister"
#: .\cookbook\templates\markdown_info.html:85
msgid ""
@ -1264,7 +1302,7 @@ msgstr ""
#: .\cookbook\templates\no_groups_info.html:5
#: .\cookbook\templates\no_groups_info.html:12
msgid "No Permissions"
msgstr "Ingen tilganger."
msgstr "Ingen tilgang"
#: .\cookbook\templates\no_groups_info.html:17
msgid "You do not have any groups and therefor cannot use this application."
@ -1298,12 +1336,11 @@ msgstr ""
#: .\cookbook\templates\offline.html:6
msgid "Offline"
msgstr "Frakoblet."
msgstr "Frakoblet"
#: .\cookbook\templates\offline.html:19
#, fuzzy
msgid "You are currently offline!"
msgstr "Du er ikke tilkoblet Internett."
msgstr "Du er ikke tilkoblet!"
#: .\cookbook\templates\offline.html:20
msgid ""
@ -1366,7 +1403,7 @@ msgstr "Stil"
#: .\cookbook\templates\settings.html:79
msgid "API Token"
msgstr "API-symbol"
msgstr "API nøkkel"
#: .\cookbook\templates\settings.html:80
msgid ""
@ -1389,9 +1426,8 @@ msgid "Cookbook Setup"
msgstr "Kokeboksoppsett"
#: .\cookbook\templates\setup.html:14
#, fuzzy
msgid "Setup"
msgstr "Sett opp"
msgstr "Installering"
#: .\cookbook\templates\setup.html:15
msgid ""
@ -1424,11 +1460,11 @@ msgstr "Mengde"
#: .\cookbook\templates\shopping_list.html:226
msgid "Supermarket"
msgstr "Matbutikk"
msgstr "Butikk"
#: .\cookbook\templates\shopping_list.html:236
msgid "Select Supermarket"
msgstr "Velg matbutikk"
msgstr "Velg butikk"
#: .\cookbook\templates\shopping_list.html:260
msgid "Select User"
@ -1540,7 +1576,6 @@ msgstr ""
#: .\cookbook\templates\system.html:49 .\cookbook\templates\system.html:64
#: .\cookbook\templates\system.html:80 .\cookbook\templates\system.html:95
#, fuzzy
msgid "Ok"
msgstr "OK"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-02-11 08:52+0100\n"
"PO-Revision-Date: 2023-02-18 10:55+0000\n"
"Last-Translator: Joachim Weber <joachim.weber@gmx.de>\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Portuguese (Brazil) <http://translate.tandoor.dev/projects/"
"tandoor/recipes-backend/pt_BR/>\n"
"Language: pt_BR\n"
@ -2208,7 +2208,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:38
msgid "URL"
msgstr ""
msgstr "URL"
#: .\cookbook\templates\url_import.html:40
msgid "App"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,17 +8,17 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-13 22:40+0200\n"
"PO-Revision-Date: 2022-11-30 19:09+0000\n"
"Last-Translator: Alex <kovsharoff@gmail.com>\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Russian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/ru/>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.14.1\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
#: .\cookbook\templates\forms\ingredients.html:34
@ -861,7 +861,7 @@ msgstr ""
#: .\cookbook\templates\base.html:220
msgid "GitHub"
msgstr ""
msgstr "GitHub"
#: .\cookbook\templates\base.html:224
msgid "API Browser"
@ -1937,7 +1937,7 @@ msgstr ""
#: .\cookbook\templates\space.html:106
msgid "user"
msgstr ""
msgstr "пользователь"
#: .\cookbook\templates\space.html:107
msgid "guest"

View File

@ -8,17 +8,17 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
"PO-Revision-Date: 2022-02-02 15:31+0000\n"
"Last-Translator: Mario Dvorsek <mario.dvorsek@gmail.com>\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Slovenian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/sl/>\n"
"Language: sl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
"%100==4 ? 2 : 3;\n"
"X-Generator: Weblate 4.10.1\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || "
"n%100==4 ? 2 : 3;\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
#: .\cookbook\templates\forms\ingredients.html:34
@ -2107,7 +2107,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:36
msgid "URL"
msgstr ""
msgstr "URL"
#: .\cookbook\templates\url_import.html:38
msgid "App"

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-29 18:42+0200\n"
"PO-Revision-Date: 2023-02-09 13:55+0000\n"
"Last-Translator: vertilo <vertilo.dev@gmail.com>\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Ukrainian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/uk/>\n"
"Language: uk\n"
@ -1091,7 +1091,7 @@ msgstr ""
#: .\cookbook\templates\base.html:311
msgid "GitHub"
msgstr ""
msgstr "GitHub"
#: .\cookbook\templates\base.html:313
msgid "Translate Tandoor"

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ from django.urls import reverse
from django_scopes import scope, scopes_disabled
from pytest_factoryboy import LazyFixture, register
from cookbook.models import Food, FoodInheritField, Ingredient, ShoppingList, ShoppingListEntry
from cookbook.models import Food, Ingredient, ShoppingListEntry
from cookbook.tests.factories import (FoodFactory, IngredientFactory, ShoppingListEntryFactory,
SupermarketCategoryFactory)
@ -56,23 +56,32 @@ def obj_tree_1(request, space_1):
params = request.param # request.param is a magic variable
except AttributeError:
params = {}
objs = []
inherit = params.pop('inherit', False)
objs.extend(FoodFactory.create_batch(3, space=space_1, **params))
FoodFactory.create_batch(3, space=space_1, **params)
objs = Food.objects.values_list('id', flat=True)
obj_id = objs[1]
child_id = objs[0]
parent_id = objs[2]
# set all foods to inherit everything
if inherit:
inherit = Food.inheritable_fields
Through = Food.objects.filter(space=space_1).first().inherit_fields.through
Through = Food.objects.filter(
space=space_1).first().inherit_fields.through
for i in inherit:
Through.objects.bulk_create([
Through(food_id=x, foodinheritfield_id=i.id)
for x in Food.objects.filter(space=space_1).values_list('id', flat=True)
])
objs[0].move(objs[1], node_location)
objs[1].move(objs[2], node_location)
return Food.objects.get(id=objs[1].id) # whenever you move/merge a tree it's safest to re-get the object
Food.objects.get(id=child_id).move(
Food.objects.get(id=obj_id), node_location)
Food.objects.get(id=obj_id).move(
Food.objects.get(id=parent_id), node_location)
# whenever you move/merge a tree it's safest to re-get the object
return Food.objects.get(id=obj_id)
@pytest.mark.parametrize("arg", [
@ -107,19 +116,23 @@ def test_list_filter(obj_1, obj_2, u1_s1):
assert obj_2.name in [x['name'] for x in response['results']]
assert response['results'][0]['name'] < response['results'][1]['name']
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?page_size=1').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?page_size=1').content)
assert len(response['results']) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
assert len(response['results']) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=''&limit=1').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?query=''&limit=1').content)
assert len(response['results']) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
assert response['count'] == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[:-4]}').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[:-4]}').content)
assert response['count'] == 1
@ -262,8 +275,9 @@ def test_integrity(u1_s1, recipe_1_s1):
def test_move(u1_s1, obj_tree_1, obj_2, obj_3, space_1):
with scope(space=space_1):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
assert parent.get_num_children() == 1
assert parent.get_descendant_count() == 2
assert Food.get_root_nodes().filter(space=space_1).count() == 2
@ -295,8 +309,9 @@ def test_move(u1_s1, obj_tree_1, obj_2, obj_3, space_1):
def test_move_errors(u1_s1, obj_tree_1, obj_3, space_1):
with scope(space=space_1):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
# move child to root
r = u1_s1.put(reverse(MOVE_URL, args=[obj_tree_1.id, 0]))
assert r.status_code == 200
@ -373,6 +388,8 @@ def test_merge_shopping_entries(obj_tree_1, u1_s1, space_1):
def test_merge(u1_s1, obj_tree_1, obj_1, obj_3, space_1):
with scope(space=space_1):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
assert parent.get_num_children() == 1
@ -416,8 +433,9 @@ def test_merge(u1_s1, obj_tree_1, obj_1, obj_3, space_1):
def test_merge_errors(u1_s1, obj_tree_1, obj_3, space_1):
with scope(space=space_1):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
# attempt to merge with non-existent parent
r = u1_s1.put(
@ -451,44 +469,63 @@ def test_merge_errors(u1_s1, obj_tree_1, obj_3, space_1):
def test_root_filter(obj_tree_1, obj_2, obj_3, u1_s1):
with scope(space=obj_tree_1.space):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
# should return root objects in the space (obj_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root=0').content)
assert len(response['results']) == 2
# django_tree bypasses ORM - best to retrieve all changed objects
with scopes_disabled():
obj_2.move(parent, node_location)
obj_2 = Food.objects.get(id=obj_2.id)
parent = Food.objects.get(id=parent.id)
# should return direct children of parent (obj_tree_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={parent.id}').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?root={parent.id}').content)
assert response['count'] == 2
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={parent.id}&query={obj_2.name[4:]}').content)
response = json.loads(u1_s1.get(
f'{reverse(LIST_URL)}?root={parent.id}&query={obj_2.name[4:]}').content)
assert response['count'] == 2
def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1):
with scope(space=obj_tree_1.space):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
obj_2.move(parent, node_location)
obj_2 = Food.objects.get(id=obj_2.id)
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = Food.objects.get(id=parent.id)
# should return full tree starting at parent (obj_tree_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}').content)
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}').content)
assert response['count'] == 4
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}&query={obj_2.name[4:]}').content)
response = json.loads(u1_s1.get(
f'{reverse(LIST_URL)}?tree={parent.id}&query={obj_2.name[4:]}').content)
assert response['count'] == 4
# This is more about the model than the API - should this be moved to a different test?
@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [
({'has_category': True, 'inherit': True}, 'supermarket_category', True, 'cat_1'),
({'has_category': True, 'inherit': False}, 'supermarket_category', False, 'cat_1'),
({'has_category': True, 'inherit': True},
'supermarket_category', True, 'cat_1'),
({'has_category': True, 'inherit': False},
'supermarket_category', False, 'cat_1'),
({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'),
({'ignore_shopping': True, 'inherit': False}, 'ignore_shopping', False, 'false'),
({'substitute_children': True, 'inherit': True}, 'substitute_children', True, 'false'),
({'substitute_children': True, 'inherit': False}, 'substitute_children', False, 'false'),
({'substitute_siblings': True, 'inherit': True}, 'substitute_siblings', True, 'false'),
({'substitute_siblings': True, 'inherit': False}, 'substitute_siblings', False, 'false'),
({'ignore_shopping': True, 'inherit': False},
'ignore_shopping', False, 'false'),
({'substitute_children': True, 'inherit': True},
'substitute_children', True, 'false'),
({'substitute_children': True, 'inherit': False},
'substitute_children', False, 'false'),
({'substitute_siblings': True, 'inherit': True},
'substitute_siblings', True, 'false'),
({'substitute_siblings': True, 'inherit': False},
'substitute_siblings', False, 'false'),
], indirect=['obj_tree_1']) # indirect=True populates magic variable request.param of obj_tree_1 with the parameter
def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
with scope(space=obj_tree_1.space):
@ -498,8 +535,10 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
new_val = request.getfixturevalue(new_val)
# if this test passes it demonstrates that inheritance works
# when moving to a parent as each food is created with a different category
assert (getattr(parent, field) == getattr(obj_tree_1, field)) in [inherit, True]
assert (getattr(obj_tree_1, field) == getattr(child, field)) in [inherit, True]
assert (getattr(parent, field) == getattr(
obj_tree_1, field)) in [inherit, True]
assert (getattr(obj_tree_1, field) == getattr(
child, field)) in [inherit, True]
# change parent to a new value
setattr(parent, field, new_val)
with scope(space=parent.space):
@ -515,7 +554,8 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
@pytest.mark.parametrize("obj_tree_1", [
({'has_category': True, 'inherit': False, 'ignore_shopping': True, 'substitute_children': True, 'substitute_siblings': True}),
({'has_category': True, 'inherit': False, 'ignore_shopping': True,
'substitute_children': True, 'substitute_siblings': True}),
], indirect=['obj_tree_1'])
@pytest.mark.parametrize("global_reset", [True, False])
@pytest.mark.parametrize("field", ['ignore_shopping', 'substitute_children', 'substitute_siblings', 'supermarket_category'])
@ -534,10 +574,13 @@ def test_reset_inherit_space_fields(obj_tree_1, space_1, global_reset, field):
assert getattr(parent, field) != getattr(obj_tree_1, field)
if global_reset:
space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields
# set default inherit fields
space_1.food_inherit.add(
*Food.inheritable_fields.values_list('id', flat=True))
parent.reset_inheritance(space=space_1)
else:
obj_tree_1.child_inherit_fields.set(Food.inheritable_fields.values_list('id', flat=True))
obj_tree_1.child_inherit_fields.set(
Food.inheritable_fields.values_list('id', flat=True))
obj_tree_1.save()
parent.reset_inheritance(space=space_1, food=obj_tree_1)
# djangotree bypasses ORM and need to be retrieved again
@ -545,12 +588,14 @@ def test_reset_inherit_space_fields(obj_tree_1, space_1, global_reset, field):
parent = Food.objects.get(id=parent.id)
child = Food.objects.get(id=child.id)
assert (getattr(parent, field) == getattr(obj_tree_1, field)) == global_reset
assert (getattr(parent, field) == getattr(
obj_tree_1, field)) == global_reset
assert getattr(obj_tree_1, field) == getattr(child, field)
@pytest.mark.parametrize("obj_tree_1", [
({'has_category': True, 'inherit': False, 'ignore_shopping': True, 'substitute_children': True, 'substitute_siblings': True}),
({'has_category': True, 'inherit': False, 'ignore_shopping': True,
'substitute_children': True, 'substitute_siblings': True}),
], indirect=['obj_tree_1'])
@pytest.mark.parametrize("field", ['ignore_shopping', 'substitute_children', 'substitute_siblings', 'supermarket_category'])
def test_reset_inherit_no_food_instances(obj_tree_1, space_1, field):
@ -558,13 +603,17 @@ def test_reset_inherit_no_food_instances(obj_tree_1, space_1, field):
parent = obj_tree_1.get_parent()
Food.objects.all().delete()
space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields
# set default inherit fields
space_1.food_inherit.add(
*Food.inheritable_fields.values_list('id', flat=True))
parent.reset_inheritance(space=space_1)
def test_onhand(obj_1, u1_s1, u2_s1):
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
'food_onhand'] == False
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
'food_onhand'] == False
u1_s1.patch(
reverse(
@ -574,10 +623,13 @@ def test_onhand(obj_1, u1_s1, u2_s1):
{'food_onhand': True},
content_type='application/json'
)
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
'food_onhand'] == True
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
'food_onhand'] == False
user1 = auth.get_user(u1_s1)
user2 = auth.get_user(u2_s1)
user1.userpreference.shopping_share.add(user2)
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
'food_onhand'] == True

View File

@ -1,20 +1,14 @@
import json
from datetime import timedelta
import factory
import pytest
# work around for bug described here https://stackoverflow.com/a/70312265/15762829
from django.conf import settings
from django.contrib import auth
from django.forms import model_to_dict
from django.urls import reverse
from django.utils import timezone
from django_scopes import scope, scopes_disabled
from pytest_factoryboy import LazyFixture, register
from django_scopes import scopes_disabled
from cookbook.models import Food, Ingredient, ShoppingListEntry, Step
from cookbook.tests.factories import (IngredientFactory, MealPlanFactory, RecipeFactory,
StepFactory, UserFactory)
from cookbook.models import Food, Ingredient
from cookbook.tests.factories import MealPlanFactory, RecipeFactory, StepFactory, UserFactory
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
@ -32,9 +26,12 @@ def user2(request, u1_s1):
except AttributeError:
params = {}
user = auth.get_user(u1_s1)
user.userpreference.mealplan_autoadd_shopping = params.get('mealplan_autoadd_shopping', True)
user.userpreference.mealplan_autoinclude_related = params.get('mealplan_autoinclude_related', True)
user.userpreference.mealplan_autoexclude_onhand = params.get('mealplan_autoexclude_onhand', True)
user.userpreference.mealplan_autoadd_shopping = params.get(
'mealplan_autoadd_shopping', True)
user.userpreference.mealplan_autoinclude_related = params.get(
'mealplan_autoinclude_related', True)
user.userpreference.mealplan_autoexclude_onhand = params.get(
'mealplan_autoexclude_onhand', True)
user.userpreference.save()
return u1_s1
@ -50,7 +47,6 @@ def recipe(request, space_1, u1_s1):
return RecipeFactory(**params)
@pytest.mark.parametrize("arg", [
['g1_s1', 204],
['u1_s1', 204],
@ -59,9 +55,12 @@ def recipe(request, space_1, u1_s1):
])
@pytest.mark.parametrize("recipe, sle_count", [
({}, 10),
({'steps__recipe_count': 1}, 20), # shopping list from recipe with StepRecipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19), # shopping list from recipe with food recipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29), # shopping list from recipe with StepRecipe and food recipe
# shopping list from recipe with StepRecipe
({'steps__recipe_count': 1}, 20),
# shopping list from recipe with food recipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19),
# shopping list from recipe with StepRecipe and food recipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29),
], indirect=['recipe'])
def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1):
c = request.getfixturevalue(arg[0])
@ -78,16 +77,20 @@ def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1):
if r.status_code == 204: # skip anonymous user
r = json.loads(c.get(reverse(SHOPPING_LIST_URL)).content)
assert len(r) == sle_count # recipe factory creates 10 ingredients by default
# recipe factory creates 10 ingredients by default
assert len(r) == sle_count
assert [x['created_by']['id'] for x in r].count(user.id) == sle_count
# user in space can't see shopping list
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
assert len(json.loads(
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
user.userpreference.shopping_share.add(auth.get_user(u2_s1))
# after share, user in space can see shopping list
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
assert len(json.loads(
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
# confirm that the author of the recipe doesn't have access to shopping list
if c != u1_s1:
assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
assert len(json.loads(
u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
r = c.get(url)
assert r.status_code == 405
@ -99,9 +102,12 @@ def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1):
@pytest.mark.parametrize("recipe, sle_count", [
({}, 10),
({'steps__recipe_count': 1}, 20), # shopping list from recipe with StepRecipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19), # shopping list from recipe with food recipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29), # shopping list from recipe with StepRecipe and food recipe
# shopping list from recipe with StepRecipe
({'steps__recipe_count': 1}, 20),
# shopping list from recipe with food recipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19),
# shopping list from recipe with StepRecipe and food recipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29),
], indirect=['recipe'])
@pytest.mark.parametrize("use_mealplan", [(False), (True), ])
def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u2_s1):
@ -115,31 +121,33 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
user.userpreference.save()
if use_mealplan:
mealplan = MealPlanFactory(space=recipe.space, created_by=user, servings=recipe.servings, recipe=recipe)
mealplan = MealPlanFactory(
space=recipe.space, created_by=user, servings=recipe.servings, recipe=recipe)
else:
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}))
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert [x['created_by']['id'] for x in r].count(user.id) == sle_count
all_ing = [x['ingredient'] for x in r]
keep_ing = all_ing[1:-1] # remove first and last element
del keep_ing[int(len(keep_ing)/2)] # remove a middle element
del keep_ing[int(len(keep_ing) / 2)] # remove a middle element
list_recipe = r[0]['list_recipe']
amount_sum = sum([x['amount'] for x in r])
# test modifying shopping list as different user
# test increasing servings size of recipe shopping list
if use_mealplan:
mealplan.servings = 2*recipe.servings
mealplan.servings = 2 * recipe.servings
mealplan.save()
else:
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
{'list_recipe': list_recipe, 'servings': 2*recipe.servings},
{'list_recipe': list_recipe, 'servings': 2 * recipe.servings},
content_type='application/json'
)
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert sum([x['amount'] for x in r]) == amount_sum * 2
assert len(r) == sle_count
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
assert len(json.loads(
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
# testing decreasing servings size of recipe shopping list
if use_mealplan:
@ -153,7 +161,8 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert sum([x['amount'] for x in r]) == amount_sum * .5
assert len(r) == sle_count
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
assert len(json.loads(
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
# test removing 3 items from shopping list
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
@ -162,7 +171,8 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
)
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert len(r) == sle_count - 3
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count - 3
assert len(json.loads(
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count - 3
# add all ingredients to existing shopping list - don't change serving size
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
@ -172,14 +182,16 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert sum([x['amount'] for x in r]) == amount_sum * .5
assert len(r) == sle_count
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
assert len(json.loads(
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
@pytest.mark.parametrize("user2, sle_count", [
({'mealplan_autoadd_shopping': False}, (0, 18)),
({'mealplan_autoinclude_related': False}, (9, 9)),
({'mealplan_autoexclude_onhand': False}, (20, 20)),
({'mealplan_autoexclude_onhand': False, 'mealplan_autoinclude_related': False}, (10, 10)),
({'mealplan_autoexclude_onhand': False,
'mealplan_autoinclude_related': False}, (10, 10)),
], indirect=['user2'])
@pytest.mark.parametrize("use_mealplan", [(False), (True), ])
@pytest.mark.parametrize("recipe", [({'steps__recipe_count': 1})], indirect=['recipe'])
@ -191,20 +203,24 @@ def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2):
food = Food.objects.get(id=ingredients[2].food.id)
food.onhand_users.add(user)
food.save()
food = recipe.steps.exclude(step_recipe=None).first().step_recipe.steps.first().ingredients.first().food
food = recipe.steps.exclude(step_recipe=None).first(
).step_recipe.steps.first().ingredients.first().food
food = Food.objects.get(id=food.id)
food.onhand_users.add(user)
food.save()
if use_mealplan:
mealplan = MealPlanFactory(space=recipe.space, created_by=user, servings=recipe.servings, recipe=recipe)
assert len(json.loads(user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[0]
MealPlanFactory(
space=recipe.space, created_by=user, servings=recipe.servings, recipe=recipe)
assert len(json.loads(
user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[0]
else:
user2.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}))
assert len(json.loads(user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[1]
assert len(json.loads(
user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[1]
def test_shopping_recipe_mixed_authors(u1_s1, u2_s1,space_1):
def test_shopping_recipe_mixed_authors(u1_s1, u2_s1, space_1):
with scopes_disabled():
user1 = auth.get_user(u1_s1)
user2 = auth.get_user(u2_s1)
@ -213,15 +229,19 @@ def test_shopping_recipe_mixed_authors(u1_s1, u2_s1,space_1):
recipe1 = RecipeFactory(created_by=user1, space=space)
recipe2 = RecipeFactory(created_by=user2, space=space)
recipe3 = RecipeFactory(created_by=user3, space=space)
food = Food.objects.get(id=recipe1.steps.first().ingredients.first().food.id)
food = Food.objects.get(
id=recipe1.steps.first().ingredients.first().food.id)
food.recipe = recipe2
food.save()
recipe1.steps.add(StepFactory(step_recipe=recipe3, ingredients__count=0, space=space))
recipe1.steps.add(StepFactory(step_recipe=recipe3,
ingredients__count=0, space=space))
recipe1.save()
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe1.id}))
assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 29
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
assert len(json.loads(
u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 29
assert len(json.loads(
u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
@pytest.mark.parametrize("recipe", [{'steps__ingredients__header': 1}], indirect=['recipe'])
@ -230,4 +250,5 @@ def test_shopping_with_header_ingredient(u1_s1, recipe):
# recipe.step_set.first().ingredient_set.add(IngredientFactory(ingredients__header=1))
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}))
assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 10
assert len(json.loads(u1_s1.get(reverse('api:ingredient-list')).content)['results']) == 11
assert len(json.loads(
u1_s1.get(reverse('api:ingredient-list')).content)['results']) == 11

View File

@ -5,12 +5,11 @@ import uuid
import pytest
from django.contrib import auth
from django.contrib.auth.models import Group, User
from django_scopes import scopes_disabled
from pytest_factoryboy import LazyFixture, register
from pytest_factoryboy import register
from cookbook.models import Food, Ingredient, Recipe, Space, Step, Unit
from cookbook.tests.factories import FoodFactory, SpaceFactory, UserFactory
from cookbook.models import Food, Ingredient, Recipe, Step, Unit
from cookbook.tests.factories import SpaceFactory, UserFactory
register(SpaceFactory, 'space_1')
register(SpaceFactory, 'space_2')
@ -60,8 +59,10 @@ def get_random_recipe(space_1, u1_s1):
internal=True,
)
s1 = Step.objects.create(name=str(uuid.uuid4()), instruction=str(uuid.uuid4()), space=space_1, )
s2 = Step.objects.create(name=str(uuid.uuid4()), instruction=str(uuid.uuid4()), space=space_1, )
s1 = Step.objects.create(name=str(uuid.uuid4()),
instruction=str(uuid.uuid4()), space=space_1, )
s2 = Step.objects.create(name=str(uuid.uuid4()),
instruction=str(uuid.uuid4()), space=space_1, )
r.steps.add(s1)
r.steps.add(s2)
@ -70,8 +71,10 @@ def get_random_recipe(space_1, u1_s1):
s1.ingredients.add(
Ingredient.objects.create(
amount=1,
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0],
unit=Unit.objects.create(name=str(uuid.uuid4()), space=space_1, ),
food=Food.objects.get_or_create(
name=str(uuid.uuid4()), space=space_1)[0],
unit=Unit.objects.create(
name=str(uuid.uuid4()), space=space_1, ),
note=str(uuid.uuid4()),
space=space_1,
)
@ -80,8 +83,10 @@ def get_random_recipe(space_1, u1_s1):
s2.ingredients.add(
Ingredient.objects.create(
amount=1,
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0],
unit=Unit.objects.create(name=str(uuid.uuid4()), space=space_1, ),
food=Food.objects.get_or_create(
name=str(uuid.uuid4()), space=space_1)[0],
unit=Unit.objects.create(
name=str(uuid.uuid4()), space=space_1, ),
note=str(uuid.uuid4()),
space=space_1,
)
@ -99,8 +104,10 @@ def get_random_json_recipe():
{
"instruction": str(uuid.uuid4()),
"ingredients": [
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(uuid.uuid4())}, "amount": random.randint(0, 10)},
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(uuid.uuid4())}, "amount": random.randint(0, 10)},
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(
uuid.uuid4())}, "amount": random.randint(0, 10)},
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(
uuid.uuid4())}, "amount": random.randint(0, 10)},
],
}
],
@ -133,7 +140,8 @@ def validate_recipe(expected, recipe):
for key in expected_lists:
for k in expected_lists[key]:
try:
print('comparing ', any([dict_compare(k, i) for i in target_lists[key]]))
print('comparing ', any([dict_compare(k, i)
for i in target_lists[key]]))
assert any([dict_compare(k, i) for i in target_lists[key]])
except AssertionError:
for result in [dict_compare(k, i, details=True) for i in target_lists[key]]:
@ -152,7 +160,8 @@ def dict_compare(d1, d2, details=False):
added = d1_keys - d2_keys
removed = d2_keys - d1_keys
modified = {o: (d1[o], d2[o]) for o in not_dicts if d1[o] != d2[o]}
modified_dicts = {o: (d1[o], d2[o]) for o in sub_dicts if not d1[o].items() <= d2[o].items()}
modified_dicts = {o: (d1[o], d2[o])
for o in sub_dicts if not d1[o].items() <= d2[o].items()}
if details:
return added, removed, modified, modified_dicts
else:
@ -173,12 +182,12 @@ def transpose(text, number=2):
positions = random.sample(range(len(tokens[token_pos])), number)
# swap the positions
l = list(tokens[token_pos])
lt = list(tokens[token_pos])
for first, second in zip(positions[::2], positions[1::2]):
l[first], l[second] = l[second], l[first]
lt[first], lt[second] = lt[second], lt[first]
# replace original tokens with swapped
tokens[token_pos] = ''.join(l)
tokens[token_pos] = ''.join(lt)
# return text with the swapped token
return ' '.join(tokens)

View File

@ -4,13 +4,12 @@ from decimal import Decimal
import factory
import pytest
from django.contrib import auth
from django.contrib.auth.models import Group, User
from django_scopes import scopes_disabled
from faker import Factory as FakerFactory
from pytest_factoryboy import register
from cookbook.models import Recipe, Step, UserSpace
from cookbook.models import UserSpace
# this code will run immediately prior to creating the model object useful when you want a reverse relationship
# log = factory.RelatedFactory(
@ -53,7 +52,8 @@ class SpaceFactory(factory.django.DjangoModelFactory):
class UserFactory(factory.django.DjangoModelFactory):
"""User factory."""
username = factory.LazyAttribute(lambda x: faker.simple_profile()['username'])
username = factory.LazyAttribute(
lambda x: faker.simple_profile()['username'])
first_name = factory.LazyAttribute(lambda x: faker.first_name())
last_name = factory.LazyAttribute(lambda x: faker.last_name())
email = factory.LazyAttribute(lambda x: faker.email())
@ -65,7 +65,8 @@ class UserFactory(factory.django.DjangoModelFactory):
return
if extracted:
us = UserSpace.objects.create(space=self.space, user=self, active=True)
us = UserSpace.objects.create(
space=self.space, user=self, active=True)
us.groups.add(Group.objects.get(name=extracted))
@factory.post_generation
@ -75,10 +76,12 @@ class UserFactory(factory.django.DjangoModelFactory):
if extracted:
for prefs in extracted:
self.userpreference[prefs] = extracted[prefs]/0 # intentionally break so it can be debugged later
# intentionally break so it can be debugged later
self.userpreference[prefs] = extracted[prefs] / 0
class Meta:
model = User
django_get_or_create = ('username', 'space',)
@register
@ -98,18 +101,22 @@ class SupermarketCategoryFactory(factory.django.DjangoModelFactory):
class FoodFactory(factory.django.DjangoModelFactory):
"""Food factory."""
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10)[:128])
plural_name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=3, variable_nb_words=False))
plural_name = factory.LazyAttribute(
lambda x: faker.sentence(nb_words=3, variable_nb_words=False))
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
supermarket_category = factory.Maybe(
factory.LazyAttribute(lambda x: x.has_category),
yes_declaration=factory.SubFactory(SupermarketCategoryFactory, space=factory.SelfAttribute('..space')),
yes_declaration=factory.SubFactory(
SupermarketCategoryFactory, space=factory.SelfAttribute('..space')),
no_declaration=None
)
recipe = factory.Maybe(
factory.LazyAttribute(lambda x: x.has_recipe),
yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
yes_declaration=factory.SubFactory(
'cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
no_declaration=None
)
path = None
space = factory.SubFactory(SpaceFactory)
@factory.post_generation
@ -127,17 +134,19 @@ class FoodFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'cookbook.Food'
django_get_or_create = ('name', 'plural_name', 'space',)
django_get_or_create = ('name', 'plural_name', 'path', 'space',)
@register
class RecipeBookFactory(factory.django.DjangoModelFactory):
"""RecipeBook factory."""
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=3, variable_nb_words=False))
name = factory.LazyAttribute(lambda x: faker.sentence(
nb_words=3, variable_nb_words=False))
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
icon = None
# shared = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
created_by = factory.SubFactory(
UserFactory, space=factory.SelfAttribute('..space'))
filter = None
space = factory.SubFactory(SpaceFactory)
@ -149,7 +158,8 @@ class RecipeBookFactory(factory.django.DjangoModelFactory):
@register
class RecipeBookEntryFactory(factory.django.DjangoModelFactory):
"""RecipeBookEntry factory."""
book = factory.SubFactory(RecipeBookFactory, space=factory.SelfAttribute('..recipe.space'))
book = factory.SubFactory(
RecipeBookFactory, space=factory.SelfAttribute('..recipe.space'))
recipe = None
class Meta:
@ -173,7 +183,8 @@ class UnitFactory(factory.django.DjangoModelFactory):
@register
class KeywordFactory(factory.django.DjangoModelFactory):
"""Keyword factory."""
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=2, variable_nb_words=False))
name = factory.LazyAttribute(lambda x: faker.sentence(
nb_words=2, variable_nb_words=False))
# icon = models.CharField(max_length=16, blank=True, null=True)
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
space = factory.SubFactory(SpaceFactory)
@ -184,15 +195,17 @@ class KeywordFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'cookbook.Keyword'
django_get_or_create = ('name', 'space',)
django_get_or_create = ('name', 'space')
exclude = ('num')
@register
class IngredientFactory(factory.django.DjangoModelFactory):
"""Ingredient factory."""
food = factory.SubFactory(FoodFactory, space=factory.SelfAttribute('..space'))
unit = factory.SubFactory(UnitFactory, space=factory.SelfAttribute('..space'))
food = factory.SubFactory(
FoodFactory, space=factory.SelfAttribute('..space'))
unit = factory.SubFactory(
UnitFactory, space=factory.SelfAttribute('..space'))
amount = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=10))
note = factory.LazyAttribute(lambda x: faker.sentence(nb_words=8))
is_header = False
@ -210,7 +223,8 @@ class MealTypeFactory(factory.django.DjangoModelFactory):
# icon =
color = factory.LazyAttribute(lambda x: faker.safe_hex_color())
default = False
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
created_by = factory.SubFactory(
UserFactory, space=factory.SelfAttribute('..space'))
space = factory.SubFactory(SpaceFactory)
class Meta:
@ -221,13 +235,17 @@ class MealTypeFactory(factory.django.DjangoModelFactory):
class MealPlanFactory(factory.django.DjangoModelFactory):
recipe = factory.Maybe(
factory.LazyAttribute(lambda x: x.has_recipe),
yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
yes_declaration=factory.SubFactory(
'cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
no_declaration=None
)
servings = factory.LazyAttribute(lambda x: Decimal(faker.random_int(min=1, max=1000)/100))
servings = factory.LazyAttribute(
lambda x: Decimal(faker.random_int(min=1, max=1000) / 100))
title = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5))
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
meal_type = factory.SubFactory(MealTypeFactory, space=factory.SelfAttribute('..space'))
created_by = factory.SubFactory(
UserFactory, space=factory.SelfAttribute('..space'))
meal_type = factory.SubFactory(
MealTypeFactory, space=factory.SelfAttribute('..space'))
note = factory.LazyAttribute(lambda x: faker.paragraph())
date = factory.LazyAttribute(lambda x: faker.future_date())
space = factory.SubFactory(SpaceFactory)
@ -244,11 +262,13 @@ class ShoppingListRecipeFactory(factory.django.DjangoModelFactory):
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5))
recipe = factory.Maybe(
factory.LazyAttribute(lambda x: x.has_recipe),
yes_declaration=factory.SubFactory('cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
yes_declaration=factory.SubFactory(
'cookbook.tests.factories.RecipeFactory', space=factory.SelfAttribute('..space')),
no_declaration=None
)
servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=10))
mealplan = factory.SubFactory(MealPlanFactory, space=factory.SelfAttribute('..space'))
mealplan = factory.SubFactory(
MealPlanFactory, space=factory.SelfAttribute('..space'))
space = factory.SubFactory(SpaceFactory)
class Params:
@ -264,25 +284,32 @@ class ShoppingListEntryFactory(factory.django.DjangoModelFactory):
list_recipe = factory.Maybe(
factory.LazyAttribute(lambda x: x.has_mealplan),
yes_declaration=factory.SubFactory(ShoppingListRecipeFactory, space=factory.SelfAttribute('..space')),
yes_declaration=factory.SubFactory(
ShoppingListRecipeFactory, space=factory.SelfAttribute('..space')),
no_declaration=None
)
food = factory.SubFactory(FoodFactory, space=factory.SelfAttribute('..space'))
unit = factory.SubFactory(UnitFactory, space=factory.SelfAttribute('..space'))
food = factory.SubFactory(
FoodFactory, space=factory.SelfAttribute('..space'))
unit = factory.SubFactory(
UnitFactory, space=factory.SelfAttribute('..space'))
# # ingredient = factory.SubFactory(IngredientFactory)
amount = factory.LazyAttribute(lambda x: Decimal(faker.random_int(min=1, max=100))/10)
amount = factory.LazyAttribute(
lambda x: Decimal(faker.random_int(min=1, max=100)) / 10)
order = factory.Sequence(int)
checked = False
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
created_by = factory.SubFactory(
UserFactory, space=factory.SelfAttribute('..space'))
created_at = factory.LazyAttribute(lambda x: faker.past_date())
completed_at = None
delay_until = None
space = factory.SubFactory(SpaceFactory)
@classmethod
def _create(cls, target_class, *args, **kwargs): # override create to prevent auto_add_now from changing the created_at date
# override create to prevent auto_add_now from changing the created_at date
def _create(cls, target_class, *args, **kwargs):
created_at = kwargs.pop('created_at', None)
obj = super(ShoppingListEntryFactory, cls)._create(target_class, *args, **kwargs)
obj = super(ShoppingListEntryFactory, cls)._create(
target_class, *args, **kwargs)
if created_at is not None:
obj.created_at = created_at
obj.save()
@ -298,7 +325,8 @@ class ShoppingListEntryFactory(factory.django.DjangoModelFactory):
@register
class StepFactory(factory.django.DjangoModelFactory):
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5))
instruction = factory.LazyAttribute(lambda x: ''.join(faker.paragraphs(nb=5)))
instruction = factory.LazyAttribute(
lambda x: ''.join(faker.paragraphs(nb=5)))
# TODO add optional recipe food, make dependent on recipe, make number of recipes a Params
ingredients__count = 10 # default number of ingredients to add
ingredients__header = 0
@ -330,14 +358,16 @@ class StepFactory(factory.django.DjangoModelFactory):
for i in range(num_ing):
if num_food_recipe > 0:
has_recipe = True
num_food_recipe = num_food_recipe-1
num_food_recipe = num_food_recipe - 1
else:
has_recipe = False
self.ingredients.add(IngredientFactory(space=self.space, food__has_recipe=has_recipe))
self.ingredients.add(IngredientFactory(
space=self.space, food__has_recipe=has_recipe))
num_header = kwargs.get('header', 0)
if num_header > 0:
for i in range(num_header):
self.ingredients.add(IngredientFactory(food=None, unit=None, amount=0, is_header=True, space=self.space))
self.ingredients.add(IngredientFactory(
food=None, unit=None, amount=0, is_header=True, space=self.space))
elif extracted:
for ing in extracted:
self.ingredients.add(ing)
@ -351,20 +381,27 @@ class RecipeFactory(factory.django.DjangoModelFactory):
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=7))
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=20))
servings_text = factory.LazyAttribute(lambda x: faker.sentence(nb_words=1)) # TODO generate list of expected servings text that can be iterated through
# TODO generate list of expected servings text that can be iterated through
servings_text = factory.LazyAttribute(lambda x: faker.sentence(nb_words=1))
keywords__count = 5 # default number of keywords to generate
steps__count = 1 # default number of steps to create
steps__recipe_count = 0 # default number of step recipes to create
steps__food_recipe_count = {} # by default, don't create food recipes, to override {'steps__food_recipe_count': {'step': 0, 'count': 1}}
working_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360))
waiting_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360))
# by default, don't create food recipes, to override {'steps__food_recipe_count': {'step': 0, 'count': 1}}
steps__food_recipe_count = {}
working_time = factory.LazyAttribute(
lambda x: faker.random_int(min=0, max=360))
waiting_time = factory.LazyAttribute(
lambda x: faker.random_int(min=0, max=360))
internal = False
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
created_at = factory.LazyAttribute(lambda x: faker.date_between_dates(date_start=date(2000, 1, 1), date_end=date(2020, 12, 31)))
created_by = factory.SubFactory(
UserFactory, space=factory.SelfAttribute('..space'))
created_at = factory.LazyAttribute(lambda x: faker.date_between_dates(
date_start=date(2000, 1, 1), date_end=date(2020, 12, 31)))
space = factory.SubFactory(SpaceFactory)
@classmethod
def _create(cls, target_class, *args, **kwargs): # override create to prevent auto_add_now from changing the created_at date
# override create to prevent auto_add_now from changing the created_at date
def _create(cls, target_class, *args, **kwargs):
created_at = kwargs.pop('created_at', None)
# updated_at = kwargs.pop('updated_at', None)
obj = super(RecipeFactory, cls)._create(target_class, *args, **kwargs)
@ -401,11 +438,13 @@ class RecipeFactory(factory.django.DjangoModelFactory):
ing_recipe_count = 0
if food_recipe_count.get('step', None) == i:
ing_recipe_count = food_recipe_count.get('count', 0)
self.steps.add(StepFactory(space=self.space, ingredients__food_recipe_count=ing_recipe_count, ingredients__header=num_ing_headers))
num_ing_headers+-1
self.steps.add(StepFactory(
space=self.space, ingredients__food_recipe_count=ing_recipe_count, ingredients__header=num_ing_headers))
num_ing_headers + - 1
if num_recipe_steps > 0:
for j in range(num_recipe_steps):
self.steps.add(StepFactory(space=self.space, step_recipe__has_recipe=True, ingredients__count=0))
self.steps.add(StepFactory(
space=self.space, step_recipe__has_recipe=True, ingredients__count=0))
if extracted and (num_steps + num_recipe_steps == 0):
for step in extracted:
self.steps.add(step)
@ -428,15 +467,18 @@ class RecipeFactory(factory.django.DjangoModelFactory):
@register
class CookLogFactory(factory.django.DjangoModelFactory):
"""CookLog factory."""
recipe = factory.SubFactory(RecipeFactory, space=factory.SelfAttribute('..space'))
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
recipe = factory.SubFactory(
RecipeFactory, space=factory.SelfAttribute('..space'))
created_by = factory.SubFactory(
UserFactory, space=factory.SelfAttribute('..space'))
created_at = factory.LazyAttribute(lambda x: faker.date_this_decade())
rating = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=5))
servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=32))
space = factory.SubFactory(SpaceFactory)
@classmethod
def _create(cls, target_class, *args, **kwargs): # override create to prevent auto_add_now from changing the created_at date
# override create to prevent auto_add_now from changing the created_at date
def _create(cls, target_class, *args, **kwargs):
created_at = kwargs.pop('created_at', None)
obj = super(CookLogFactory, cls)._create(target_class, *args, **kwargs)
if created_at is not None:
@ -451,13 +493,17 @@ class CookLogFactory(factory.django.DjangoModelFactory):
@register
class ViewLogFactory(factory.django.DjangoModelFactory):
"""ViewLog factory."""
recipe = factory.SubFactory(RecipeFactory, space=factory.SelfAttribute('..space'))
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space'))
created_at = factory.LazyAttribute(lambda x: faker.past_datetime(start_date='-365d'))
recipe = factory.SubFactory(
RecipeFactory, space=factory.SelfAttribute('..space'))
created_by = factory.SubFactory(
UserFactory, space=factory.SelfAttribute('..space'))
created_at = factory.LazyAttribute(
lambda x: faker.past_datetime(start_date='-365d'))
space = factory.SubFactory(SpaceFactory)
@classmethod
def _create(cls, target_class, *args, **kwargs): # override create to prevent auto_add_now from changing the created_at date
# override create to prevent auto_add_now from changing the created_at date
def _create(cls, target_class, *args, **kwargs):
created_at = kwargs.pop('created_at', None)
obj = super(ViewLogFactory, cls)._create(target_class, *args, **kwargs)
if created_at is not None:

View File

@ -11,6 +11,11 @@ from cookbook.tests.factories import FoodFactory, RecipeFactory
# TODO returns recipes with all ingredients via child substitute
# TODO returns recipes with all ingredients via sibling substitute
if (Food.node_order_by):
node_location = 'sorted-child'
else:
node_location = 'last-child'
@pytest.fixture
def recipes(space_1):
@ -19,7 +24,8 @@ def recipes(space_1):
@pytest.fixture
def makenow_recipe(request, space_1):
onhand_user = auth.get_user(request.getfixturevalue(request.param.get('onhand_users', 'u1_s1')))
onhand_user = auth.get_user(request.getfixturevalue(
request.param.get('onhand_users', 'u1_s1')))
recipe = RecipeFactory.create(space=space_1)
for food in Food.objects.filter(ingredient__step__recipe=recipe.id):
@ -55,13 +61,16 @@ def test_makenow_ignoreshopping(recipes, makenow_recipe, user1, space_1):
request = type('', (object,), {'space': space_1, 'user': user1})()
search = RecipeSearch(request, makenow='true')
with scope(space=space_1):
food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first()
food = Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id).first()
food.onhand_users.clear()
assert search.get_queryset(Recipe.objects.all()).count() == 0
food.ignore_shopping = True
food.save()
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, ignore_shopping=True).count() == 1
assert Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
assert Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id, ignore_shopping=True).count() == 1
search = search.get_queryset(Recipe.objects.all())
assert search.count() == 1
assert search.first().id == makenow_recipe.id
@ -74,13 +83,17 @@ def test_makenow_substitute(recipes, makenow_recipe, user1, space_1):
request = type('', (object,), {'space': space_1, 'user': user1})()
search = RecipeSearch(request, makenow='true')
with scope(space=space_1):
food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first()
food = Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id).first()
onhand_user = food.onhand_users.first()
food.onhand_users.clear()
assert search.get_queryset(Recipe.objects.all()).count() == 0
food.substitute.add(FoodFactory.create(space=space_1, onhand_users=[onhand_user]))
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, substitute__isnull=False).count() == 1
food.substitute.add(FoodFactory.create(
space=space_1, onhand_users=[onhand_user]))
assert Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
assert Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id, substitute__isnull=False).count() == 1
search = search.get_queryset(Recipe.objects.all())
assert search.count() == 1
@ -94,16 +107,20 @@ def test_makenow_child_substitute(recipes, makenow_recipe, user1, space_1):
request = type('', (object,), {'space': space_1, 'user': user1})()
search = RecipeSearch(request, makenow='true')
with scope(space=space_1):
food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first()
food = Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id).first()
onhand_user = food.onhand_users.first()
food.onhand_users.clear()
food.substitute_children = True
food.save()
assert search.get_queryset(Recipe.objects.all()).count() == 0
new_food = FoodFactory.create(space=space_1, onhand_users=[onhand_user])
new_food.move(food, 'first-child')
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, numchild__gt=0).count() == 1
new_food = FoodFactory.create(
space=space_1, onhand_users=[onhand_user])
new_food.move(food, node_location)
assert Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
assert Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id, numchild__gt=0).count() == 1
search = search.get_queryset(Recipe.objects.all())
assert search.count() == 1
assert search.first().id == makenow_recipe.id
@ -116,18 +133,22 @@ def test_makenow_sibling_substitute(recipes, makenow_recipe, user1, space_1):
request = type('', (object,), {'space': space_1, 'user': user1})()
search = RecipeSearch(request, makenow='true')
with scope(space=space_1):
food = Food.objects.filter(ingredient__step__recipe=makenow_recipe.id).first()
food = Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id).first()
onhand_user = food.onhand_users.first()
food.onhand_users.clear()
food.substitute_siblings = True
food.save()
assert search.get_queryset(Recipe.objects.all()).count() == 0
new_parent = FoodFactory.create(space=space_1)
new_sibling = FoodFactory.create(space=space_1, onhand_users=[onhand_user])
new_sibling.move(new_parent, 'first-child')
food.move(new_parent, 'first-child')
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, depth=2).count() == 1
new_sibling = FoodFactory.create(
space=space_1, onhand_users=[onhand_user])
new_sibling.move(new_parent, node_location)
food.move(new_parent, node_location)
assert Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9
assert Food.objects.filter(
ingredient__step__recipe=makenow_recipe.id, depth=2).count() == 1
search = search.get_queryset(Recipe.objects.all())
assert search.count() == 1
assert search.first().id == makenow_recipe.id

View File

@ -7,9 +7,9 @@ from django.conf import settings
from django.contrib import auth
from django.urls import reverse
from django.utils import timezone
from django_scopes import scope, scopes_disabled
from django_scopes import scope
from cookbook.models import Food, Recipe, SearchFields
from cookbook.models import Recipe, SearchFields
from cookbook.tests.conftest import transpose
from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFactory,
KeywordFactory, RecipeBookEntryFactory, RecipeFactory,
@ -23,7 +23,8 @@ from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFac
# TODO makenow with above filters
# TODO test search food/keywords including/excluding children
LIST_URL = 'api:recipe-list'
sqlite = settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
sqlite = settings.DATABASES['default']['ENGINE'] not in [
'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
@pytest.fixture
@ -50,26 +51,43 @@ def user1(request, space_1, u1_s1, unaccent):
if params.get('fuzzy_lookups', False):
user.searchpreference.lookup = True
misspelled_result = 1
else:
user.searchpreference.lookup = False
if params.get('fuzzy_search', False):
user.searchpreference.trigram.set(SearchFields.objects.all())
misspelled_result = 1
else:
user.searchpreference.trigram.set([])
if params.get('icontains', False):
user.searchpreference.icontains.set(SearchFields.objects.all())
search_term = 'ghijklmn'
else:
user.searchpreference.icontains.set([])
if params.get('istartswith', False):
user.searchpreference.istartswith.set(SearchFields.objects.all())
search_term = 'abcdef'
else:
user.searchpreference.istartswith.set([])
if params.get('unaccent', False):
user.searchpreference.unaccent.set(SearchFields.objects.all())
misspelled_result *= 2
result *= 2
else:
user.searchpreference.unaccent.set([])
# full text vectors are hard coded to use unaccent - put this after unaccent to override result
if params.get('fulltext', False):
user.searchpreference.fulltext.set(SearchFields.objects.all())
# user.searchpreference.search = 'websearch'
search_term = 'ghijklmn uvwxyz'
result = 2
else:
user.searchpreference.fulltext.set([])
user.searchpreference.save()
misspelled_term = transpose(search_term, number=3)
return (u1_s1, result, misspelled_result, search_term, misspelled_term, params)
@ -104,7 +122,8 @@ def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1):
obj2 = FoodFactory.create(name=accent, space=space_1)
recipe1.steps.first().ingredients.add(IngredientFactory.create(food=obj1))
recipe2.steps.first().ingredients.add(IngredientFactory.create(food=obj2))
recipe3.steps.first().ingredients.add(IngredientFactory.create(food=obj1), IngredientFactory.create(food=obj2))
recipe3.steps.first().ingredients.add(IngredientFactory.create(
food=obj1), IngredientFactory.create(food=obj2))
if request.param.get('keyword', None):
obj1 = KeywordFactory.create(name=unaccent, space=space_1)
obj2 = KeywordFactory.create(name=accent, space=space_1)
@ -125,7 +144,8 @@ def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1):
obj2 = UnitFactory.create(name=accent, space=space_1)
recipe1.steps.first().ingredients.add(IngredientFactory.create(unit=obj1))
recipe2.steps.first().ingredients.add(IngredientFactory.create(unit=obj2))
recipe3.steps.first().ingredients.add(IngredientFactory.create(unit=obj1), IngredientFactory.create(unit=obj2))
recipe3.steps.first().ingredients.add(IngredientFactory.create(
unit=obj1), IngredientFactory.create(unit=obj2))
if request.param.get('name', None):
recipe1.name = unaccent
recipe2.name = accent
@ -145,21 +165,32 @@ def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1):
i2.save()
if request.param.get('viewedon', None):
ViewLogFactory.create(recipe=recipe1, created_by=user1, created_at=days_3, space=space_1)
ViewLogFactory.create(recipe=recipe2, created_by=user1, created_at=days_30, space=space_1)
ViewLogFactory.create(recipe=recipe3, created_by=user2, created_at=days_15, space=space_1)
ViewLogFactory.create(recipe=recipe1, created_by=user1,
created_at=days_3, space=space_1)
ViewLogFactory.create(recipe=recipe2, created_by=user1,
created_at=days_30, space=space_1)
ViewLogFactory.create(recipe=recipe3, created_by=user2,
created_at=days_15, space=space_1)
if request.param.get('cookedon', None):
CookLogFactory.create(recipe=recipe1, created_by=user1, created_at=days_3, space=space_1)
CookLogFactory.create(recipe=recipe2, created_by=user1, created_at=days_30, space=space_1)
CookLogFactory.create(recipe=recipe3, created_by=user2, created_at=days_15, space=space_1)
CookLogFactory.create(recipe=recipe1, created_by=user1,
created_at=days_3, space=space_1)
CookLogFactory.create(recipe=recipe2, created_by=user1,
created_at=days_30, space=space_1)
CookLogFactory.create(recipe=recipe3, created_by=user2,
created_at=days_15, space=space_1)
if request.param.get('timescooked', None):
CookLogFactory.create_batch(5, recipe=recipe1, created_by=user1, space=space_1)
CookLogFactory.create_batch(
5, recipe=recipe1, created_by=user1, space=space_1)
CookLogFactory.create(recipe=recipe2, created_by=user1, space=space_1)
CookLogFactory.create_batch(3, recipe=recipe3, created_by=user2, space=space_1)
CookLogFactory.create_batch(
3, recipe=recipe3, created_by=user2, space=space_1)
if request.param.get('rating', None):
CookLogFactory.create(recipe=recipe1, created_by=user1, rating=5.0, space=space_1)
CookLogFactory.create(recipe=recipe2, created_by=user1, rating=1.0, space=space_1)
CookLogFactory.create(recipe=recipe3, created_by=user2, rating=3.0, space=space_1)
CookLogFactory.create(
recipe=recipe1, created_by=user1, rating=5.0, space=space_1)
CookLogFactory.create(
recipe=recipe2, created_by=user1, rating=1.0, space=space_1)
CookLogFactory.create(
recipe=recipe3, created_by=user2, rating=3.0, space=space_1)
return (recipe1, recipe2, recipe3, obj1, obj2, request.param)
@ -188,7 +219,8 @@ def test_search_or_and_not(found_recipe, param_type, operator, recipes, u1_s1, s
assert found_recipe[1].id in [x['id'] for x in r['results']]
assert found_recipe[2].id in [x['id'] for x in r['results']]
r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1}&{param2}').content)
r = json.loads(u1_s1.get(reverse(LIST_URL) +
f'?{param1}&{param2}').content)
assert r['count'] == operator[1]
assert found_recipe[2].id in [x['id'] for x in r['results']]
@ -203,7 +235,8 @@ def test_search_or_and_not(found_recipe, param_type, operator, recipes, u1_s1, s
assert found_recipe[1].id not in [x['id'] for x in r['results']]
assert found_recipe[2].id not in [x['id'] for x in r['results']]
r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1_not}&{param2_not}').content)
r = json.loads(u1_s1.get(reverse(LIST_URL) +
f'?{param1_not}&{param2_not}').content)
assert r['count'] == 10 + operator[2]
assert found_recipe[2].id not in [x['id'] for x in r['results']]
@ -227,7 +260,8 @@ def test_search_units(found_recipe, recipes, u1_s1, space_1):
assert found_recipe[1].id in [x['id'] for x in r['results']]
assert found_recipe[2].id in [x['id'] for x in r['results']]
r = json.loads(u1_s1.get(reverse(LIST_URL) + f'?{param1}&{param2}').content)
r = json.loads(u1_s1.get(reverse(LIST_URL) +
f'?{param1}&{param2}').content)
assert r['count'] == 3
assert found_recipe[2].id in [x['id'] for x in r['results']]
@ -251,11 +285,15 @@ def test_fuzzy_lookup(found_recipe, recipes, param_type, user1, space_1):
param1 = f"query={user1[3]}"
param2 = f"query={user1[4]}"
r = json.loads(user1[0].get(reverse(list_url) + f'?{param1}&limit=2').content)
assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[3].id, found_recipe[4].id]]) == user1[1]
r = json.loads(user1[0].get(reverse(list_url) +
f'?{param1}&limit=2').content)
assert len([x['id'] for x in r['results'] if x['id'] in [
found_recipe[3].id, found_recipe[4].id]]) == user1[1]
r = json.loads(user1[0].get(reverse(list_url) + f'?{param2}&limit=10').content)
assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[3].id, found_recipe[4].id]]) == user1[2]
r = json.loads(user1[0].get(reverse(list_url) +
f'?{param2}&limit=10').content)
assert len([x['id'] for x in r['results'] if x['id'] in [
found_recipe[3].id, found_recipe[4].id]]) == user1[2]
# commenting this out for general use - it is really slow
# it should be run on occasion to ensure everything still works
@ -276,29 +314,35 @@ def test_fuzzy_lookup(found_recipe, recipes, param_type, user1, space_1):
# ({'keyword': True}),
# ({'food': True}),
# ], indirect=['found_recipe'])
# # user array contains: user client, expected count of search, expected count of mispelled search, search string, mispelled search string, user search preferences
# def test_search_string(found_recipe, recipes, user1, space_1):
# with scope(space=space_1):
# param1 = f"query={user1[3]}"
# param2 = f"query={user1[4]}"
# r = json.loads(user1[0].get(reverse(LIST_URL) + f'?{param1}').content)
# assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[0].id, found_recipe[1].id]]) == user1[1]
# assert len([x['id'] for x in r['results'] if x['id'] in [
# found_recipe[0].id, found_recipe[1].id]]) == user1[1]
# r = json.loads(user1[0].get(reverse(LIST_URL) + f'?{param2}').content)
# assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[0].id, found_recipe[1].id]]) == user1[2]
# assert len([x['id'] for x in r['results'] if x['id'] in [
# found_recipe[0].id, found_recipe[1].id]]) == user1[2]
@pytest.mark.parametrize("found_recipe, param_type, result", [
({'viewedon': True}, 'viewedon', (1, 1)),
({'cookedon': True}, 'cookedon', (1, 1)),
({'createdon': True}, 'createdon', (2, 12)), # created dates are not filtered by user
({'createdon': True}, 'updatedon', (2, 12)), # updated dates are not filtered by user
# created dates are not filtered by user
({'createdon': True}, 'createdon', (2, 12)),
# updated dates are not filtered by user
({'createdon': True}, 'updatedon', (2, 12)),
], indirect=['found_recipe'])
def test_search_date(found_recipe, recipes, param_type, result, u1_s1, u2_s1, space_1):
# force updated_at to equal created_at datetime
with scope(space=space_1):
for recipe in Recipe.objects.all():
Recipe.objects.filter(id=recipe.id).update(updated_at=recipe.created_at)
Recipe.objects.filter(id=recipe.id).update(
updated_at=recipe.created_at)
date = (timezone.now() - timedelta(days=15)).strftime("%Y-%m-%d")
param1 = f"?{param_type}={date}"
@ -321,34 +365,34 @@ def test_search_date(found_recipe, recipes, param_type, result, u1_s1, u2_s1, sp
assert found_recipe[2].id in [x['id'] for x in r['results']]
# TODO this is somehow screwed, probably the search itself, dont want to fix it for now
# @pytest.mark.parametrize("found_recipe, param_type", [
# ({'rating': True}, 'rating'),
# ({'timescooked': True}, 'timescooked'),
# ], indirect=['found_recipe'])
# def test_search_count(found_recipe, recipes, param_type, u1_s1, u2_s1, space_1):
# param1 = f'?{param_type}=3'
# param2 = f'?{param_type}=-3'
# param3 = f'?{param_type}=0'
#
# r = json.loads(u1_s1.get(reverse(LIST_URL) + param1).content)
# assert r['count'] == 1
# assert found_recipe[0].id in [x['id'] for x in r['results']]
#
# r = json.loads(u1_s1.get(reverse(LIST_URL) + param2).content)
# assert r['count'] == 1
# assert found_recipe[1].id in [x['id'] for x in r['results']]
#
# # test search for not rated/cooked
# r = json.loads(u1_s1.get(reverse(LIST_URL) + param3).content)
# assert r['count'] == 11
# assert (found_recipe[0].id or found_recipe[1].id) not in [x['id'] for x in r['results']]
#
# # test matched returns for lte and gte searches
# r = json.loads(u2_s1.get(reverse(LIST_URL) + param1).content)
# assert r['count'] == 1
# assert found_recipe[2].id in [x['id'] for x in r['results']]
#
# r = json.loads(u2_s1.get(reverse(LIST_URL) + param2).content)
# assert r['count'] == 1
# assert found_recipe[2].id in [x['id'] for x in r['results']]
@pytest.mark.parametrize("found_recipe, param_type", [
({'rating': True}, 'rating'),
({'timescooked': True}, 'timescooked'),
], indirect=['found_recipe'])
def test_search_count(found_recipe, recipes, param_type, u1_s1, u2_s1, space_1):
param1 = f'?{param_type}=3'
param2 = f'?{param_type}=-3'
param3 = f'?{param_type}=0'
r = json.loads(u1_s1.get(reverse(LIST_URL) + param1).content)
assert r['count'] == 1
assert found_recipe[0].id in [x['id'] for x in r['results']]
r = json.loads(u1_s1.get(reverse(LIST_URL) + param2).content)
assert r['count'] == 1
assert found_recipe[1].id in [x['id'] for x in r['results']]
# test search for not rated/cooked
r = json.loads(u1_s1.get(reverse(LIST_URL) + param3).content)
assert r['count'] == 11
assert (found_recipe[0].id or found_recipe[1].id) not in [
x['id'] for x in r['results']]
# test matched returns for lte and gte searches
r = json.loads(u2_s1.get(reverse(LIST_URL) + param1).content)
assert r['count'] == 1
assert found_recipe[2].id in [x['id'] for x in r['results']]
r = json.loads(u2_s1.get(reverse(LIST_URL) + param2).content)
assert r['count'] == 1
assert found_recipe[2].id in [x['id'] for x in r['results']]

View File

@ -96,6 +96,7 @@ AUTH_LDAP_USER_SEARCH_FILTER_STR=(uid=%(user)s)
AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn', 'email': 'mail'}
AUTH_LDAP_ALWAYS_UPDATE_USER=1
AUTH_LDAP_CACHE_TIMEOUT=3600
AUTH_LDAP_START_TLS=1
AUTH_LDAP_TLS_CACERTFILE=/etc/ssl/certs/own-ca.pem
```

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,64 +18,68 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr "Englisch"
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr "Deutsch"
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
#, fuzzy
#| msgid "English"
msgid "Polish"
msgstr "Englisch"
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +17,66 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +19,66 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : "
"2);\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +17,66 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-19 19:14+0100\n"
"POT-Creation-Date: 2023-04-26 07:46+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,62 +17,66 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:382
#: .\recipes\settings.py:436
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:383
#: .\recipes\settings.py:437
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:384
#: .\recipes\settings.py:438
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:385
#: .\recipes\settings.py:439
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:386
#: .\recipes\settings.py:440
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:387
#: .\recipes\settings.py:441
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:388
#: .\recipes\settings.py:442
msgid "English"
msgstr ""
#: .\recipes\settings.py:389
#: .\recipes\settings.py:443
msgid "French"
msgstr ""
#: .\recipes\settings.py:390
#: .\recipes\settings.py:444
msgid "German"
msgstr ""
#: .\recipes\settings.py:391
#: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:392
#: .\recipes\settings.py:447
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:393
#: .\recipes\settings.py:448
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:394
#: .\recipes\settings.py:449
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:395
#: .\recipes\settings.py:450
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:396
#: .\recipes\settings.py:451
msgid "Swedish"
msgstr ""

View File

@ -24,7 +24,8 @@ load_dotenv()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Get vars from .env files
SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv('SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV'
SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv(
'SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV'
DEBUG = bool(int(os.getenv('DEBUG', True)))
DEBUG_TOOLBAR = bool(int(os.getenv('DEBUG_TOOLBAR', True)))
@ -35,9 +36,11 @@ 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_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 0))
SPACE_DEFAULT_ALLOW_SHARING = bool(int(os.getenv('SPACE_DEFAULT_ALLOW_SHARING', True)))
SPACE_DEFAULT_ALLOW_SHARING = bool(
int(os.getenv('SPACE_DEFAULT_ALLOW_SHARING', True)))
INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(
',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
# allow djangos wsgi server to server mediafiles
GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', True)))
@ -51,9 +54,11 @@ KJ_PREF_DEFAULT = bool(int(os.getenv('KJ_PREF_DEFAULT', False)))
STICKY_NAV_PREF_DEFAULT = bool(int(os.getenv('STICKY_NAV_PREF_DEFAULT', True)))
# minimum interval that users can set for automatic sync of shopping lists
SHOPPING_MIN_AUTOSYNC_INTERVAL = int(os.getenv('SHOPPING_MIN_AUTOSYNC_INTERVAL', 5))
SHOPPING_MIN_AUTOSYNC_INTERVAL = int(
os.getenv('SHOPPING_MIN_AUTOSYNC_INTERVAL', 5))
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(',') if os.getenv('ALLOWED_HOSTS') else ['*']
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(
',') if os.getenv('ALLOWED_HOSTS') else ['*']
if os.getenv('CSRF_TRUSTED_ORIGINS'):
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',')
@ -131,7 +136,8 @@ try:
plugin_module = f'recipes.plugins.{d}.apps.{app_config_classname}'
if plugin_module not in INSTALLED_APPS:
INSTALLED_APPS.append(plugin_module)
plugin_class = getattr(sys.modules[apps_path], app_config_classname)
plugin_class = getattr(
sys.modules[apps_path], app_config_classname)
plugin_config = {
'name': plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name,
'module': f'recipes.plugins.{d}',
@ -148,7 +154,8 @@ except Exception:
if DEBUG:
print('ERROR failed to initialize plugins')
SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split(',') if os.getenv('SOCIAL_PROVIDERS') else []
SOCIAL_PROVIDERS = os.getenv('SOCIAL_PROVIDERS').split(
',') if os.getenv('SOCIAL_PROVIDERS') else []
SOCIALACCOUNT_EMAIL_VERIFICATION = 'none'
INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS
@ -194,7 +201,8 @@ if DEBUG_TOOLBAR:
INSTALLED_APPS += ('debug_toolbar',)
SORT_TREE_BY_NAME = bool(int(os.getenv('SORT_TREE_BY_NAME', False)))
DISABLE_TREE_FIX_STARTUP = bool(int(os.getenv('DISABLE_TREE_FIX_STARTUP', False)))
DISABLE_TREE_FIX_STARTUP = bool(
int(os.getenv('DISABLE_TREE_FIX_STARTUP', False)))
if bool(int(os.getenv('SQL_DEBUG', False))):
MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware',)
@ -213,6 +221,7 @@ if LDAP_AUTH:
AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend')
AUTH_LDAP_SERVER_URI = os.getenv('AUTH_LDAP_SERVER_URI')
AUTH_LDAP_START_TLS = bool(int(os.getenv('AUTH_LDAP_START_TLS', False)))
AUTH_LDAP_BIND_DN = os.getenv('AUTH_LDAP_BIND_DN')
AUTH_LDAP_BIND_PASSWORD = os.getenv('AUTH_LDAP_BIND_PASSWORD')
AUTH_LDAP_USER_SEARCH = LDAPSearch(
@ -225,10 +234,12 @@ if LDAP_AUTH:
'last_name': 'sn',
'email': 'mail',
}
AUTH_LDAP_ALWAYS_UPDATE_USER = bool(int(os.getenv('AUTH_LDAP_ALWAYS_UPDATE_USER', True)))
AUTH_LDAP_ALWAYS_UPDATE_USER = bool(
int(os.getenv('AUTH_LDAP_ALWAYS_UPDATE_USER', True)))
AUTH_LDAP_CACHE_TIMEOUT = int(os.getenv('AUTH_LDAP_CACHE_TIMEOUT', 3600))
if 'AUTH_LDAP_TLS_CACERTFILE' in os.environ:
AUTH_LDAP_GLOBAL_OPTIONS = {ldap.OPT_X_TLS_CACERTFILE: os.getenv('AUTH_LDAP_TLS_CACERTFILE')}
AUTH_LDAP_GLOBAL_OPTIONS = {
ldap.OPT_X_TLS_CACERTFILE: os.getenv('AUTH_LDAP_TLS_CACERTFILE')}
if DEBUG:
LOGGING = {
"version": 1,
@ -249,7 +260,8 @@ ACCOUNT_ADAPTER = 'cookbook.helper.AllAuthCustomAdapter'
if REVERSE_PROXY_AUTH:
MIDDLEWARE.insert(8, 'recipes.middleware.CustomRemoteUser')
AUTHENTICATION_BACKENDS.append('django.contrib.auth.backends.RemoteUserBackend')
AUTHENTICATION_BACKENDS.append(
'django.contrib.auth.backends.RemoteUserBackend')
# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
@ -444,7 +456,8 @@ LANGUAGES = [
SCRIPT_NAME = os.getenv('SCRIPT_NAME', '')
# path for django_js_reverse to generate the javascript file containing all urls. Only done because the default command (collectstatic_js_reverse) fails to update the manifest
JS_REVERSE_OUTPUT_PATH = os.path.join(BASE_DIR, "cookbook/static/django_js_reverse")
JS_REVERSE_OUTPUT_PATH = os.path.join(
BASE_DIR, "cookbook/static/django_js_reverse")
JS_REVERSE_SCRIPT_PREFIX = os.getenv('JS_REVERSE_SCRIPT_PREFIX', SCRIPT_NAME)
STATIC_URL = os.getenv('STATIC_URL', '/static/')
@ -499,4 +512,5 @@ EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '')
EMAIL_USE_TLS = bool(int(os.getenv('EMAIL_USE_TLS', False)))
EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False)))
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv('ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv(
'ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix

View File

@ -29,12 +29,12 @@ microdata==0.8.0
Jinja2==3.1.2
django-webpack-loader==1.8.1
git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82
django-allauth==0.52.0
recipe-scrapers==14.35.0
django-allauth==0.54.0
recipe-scrapers==14.36.1
django-scopes==1.2.0.post1
pytest==7.2.2
pytest==7.3.1
pytest-django==4.5.2
django-treebeard==4.5.1
django-treebeard==4.7
django-cors-headers==3.13.0
django-storages==1.13.2
boto3==1.26.41
@ -42,7 +42,7 @@ django-prometheus==2.2.0
django-hCaptcha==0.2.0
python-ldap==3.4.3
django-auth-ldap==4.2.0
pytest-factoryboy==2.5.0
pytest-factoryboy==2.5.1
pyppeteer==1.0.2
validators==0.20.0
pytube==12.1.0

View File

@ -12,18 +12,17 @@
<i class="far fa-check-circle text-primary" v-if="!ingredient.checked"></i>
</td>
<td class="text-nowrap" @click="done">
<span v-if="ingredient.amount !== 0 && !ingredient.no_amount"
v-html="calculateAmount(ingredient.amount)"></span>
<span v-if="ingredient.amount !== 0 && !ingredient.no_amount" v-html="calculateAmount(ingredient.amount)"></span>
</td>
<td @click="done">
<template v-if="ingredient.unit !== null && !ingredient.no_amount">
<template >
<template>
<template v-if="ingredient.unit.plural_name === '' || ingredient.unit.plural_name === null">
<span>{{ ingredient.unit.name }}</span>
</template>
<template v-else>
<span v-if="ingredient.always_use_plural_unit">{{ ingredient.unit.plural_name}}</span>
<span v-else-if="(ingredient.amount * this.ingredient_factor) > 1">{{ ingredient.unit.plural_name }}</span>
<span v-if="ingredient.always_use_plural_unit">{{ ingredient.unit.plural_name }}</span>
<span v-else-if="ingredient.amount * this.ingredient_factor > 1">{{ ingredient.unit.plural_name }}</span>
<span v-else>{{ ingredient.unit.name }}</span>
</template>
</template>
@ -31,11 +30,10 @@
</td>
<td @click="done">
<template v-if="ingredient.food !== null">
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)"
v-if="ingredient.food.recipe !== null" target="_blank"
rel="noopener noreferrer">{{ ingredient.food.name }}</a>
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)" v-if="ingredient.food.recipe !== null" target="_blank" rel="noopener noreferrer">{{
ingredient.food.name
}}</a>
<template v-if="ingredient.food.recipe === null">
<template>
<template v-if="ingredient.food.plural_name === '' || ingredient.food.plural_name === null">
<span>{{ ingredient.food.name }}</span>
@ -43,7 +41,7 @@
<template v-else>
<span v-if="ingredient.always_use_plural_food">{{ ingredient.food.plural_name }}</span>
<span v-else-if="ingredient.no_amount">{{ ingredient.food.name }}</span>
<span v-else-if="(ingredient.amount * this.ingredient_factor) > 1">{{ ingredient.food.plural_name }}</span>
<span v-else-if="ingredient.amount * this.ingredient_factor > 1">{{ ingredient.food.plural_name }}</span>
<span v-else>{{ ingredient.food.name }}</span>
</template>
</template>
@ -51,35 +49,32 @@
</template>
</td>
<td v-if="detailed">
<div v-if="ingredient.note">
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable p-0 pl-md-2 pr-md-2">
<template v-if="ingredient.note">
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable py-0 px-2">
<i class="far fa-comment"></i>
</span>
<div class="d-none d-print-block"><i class="far fa-comment-alt d-print-none"></i> {{
ingredient.note
}}
</div>
</div>
<div class="d-none d-print-block"><i class="far fa-comment-alt d-print-none"></i> {{ ingredient.note }}</div>
</template>
</td>
</template>
</tr>
</template>
<script>
import {calculateAmount, ResolveUrlMixin} from "@/utils/utils"
import { calculateAmount, ResolveUrlMixin } from "@/utils/utils"
import Vue from "vue"
import VueSanitize from "vue-sanitize";
import VueSanitize from "vue-sanitize"
Vue.use(VueSanitize);
Vue.use(VueSanitize)
export default {
name: "IngredientComponent",
props: {
ingredient: Object,
ingredient_factor: {type: Number, default: 1},
detailed: {type: Boolean, default: true},
ingredient_factor: { type: Number, default: 1 },
detailed: { type: Boolean, default: true },
},
mixins: [ResolveUrlMixin],
data() {
@ -88,9 +83,7 @@ export default {
}
},
watch: {},
mounted() {
},
mounted() {},
methods: {
calculateAmount: function (x) {
return this.$sanitize(calculateAmount(x, this.ingredient_factor))
@ -106,9 +99,9 @@ export default {
<style scoped>
/* increase size of hover/touchable space without changing spacing */
.touchable {
padding-right: 2em;
padding-left: 2em;
margin-right: -2em;
margin-left: -2em;
/* padding-right: 2em;
padding-left: 2em; */
margin-right: -1em;
margin-left: -1em;
}
</style>

View File

@ -72,7 +72,7 @@
"Cancel": "Abbrechen",
"success_deleting_resource": "Ressource erfolgreich gelöscht!",
"Load_More": "Weitere laden",
"Ok": "Öffnen",
"Ok": "Ok",
"Link": "Link",
"Key_Ctrl": "Strg",
"move_title": "{type} verschieben",
@ -114,7 +114,7 @@
"Create_New_Shopping Category": "Neue Einkaufskategorie erstellen",
"Automate": "Automatisieren",
"Type": "Typ",
"and_up": "& Höher",
"and_up": "& Hoch",
"Unrated": "Unbewertet",
"Shopping_list": "Einkaufsliste",
"step_time_minutes": "Schritt Dauer in Minuten",
@ -206,7 +206,7 @@
"New_Cookbook": "Neues Kochbuch",
"Coming_Soon": "Bald verfügbar",
"Auto_Planner": "Smart Planen",
"Hide_Keyword": "Keywords schließen",
"Hide_Keyword": "Schlüsselwörter verbergen",
"Clear": "Leeren",
"GroupBy": "Gruppieren nach",
"IgnoreThis": "Füge {food} nie automatisch zur Einkaufsliste hinzu",
@ -222,11 +222,11 @@
"NoCategory": "Keine Kategorie ausgewählt.",
"ShowDelayed": "Zeige verschobene Elemente",
"Completed": "Fertig",
"OfflineAlert": "Du bist offline, deine Einkaufsliste wird nicht synchronisiert.",
"OfflineAlert": "Du bist offline. Deine Einkaufsliste wird nicht synchronisiert.",
"shopping_share": "Einkaufsliste teilen",
"mealplan_autoadd_shopping": "Automatisches Hinzufügen zum Essensplan",
"mealplan_autoexclude_onhand": "Ignoriere vorrätige Zutaten",
"mealplan_autoinclude_related": "Füge verwandte Rezepte hinzu",
"mealplan_autoinclude_related": "Ähnliche Rezepte hinzufügen",
"default_delay": "Standard-Verzögerungszeit",
"Added_by": "Hinzugefügt durch",
"AddToShopping": "Zur Einkaufsliste hinzufügen",
@ -241,7 +241,7 @@
"IngredientInShopping": "Diese Zutat befindet sich auf Ihrer Einkaufsliste.",
"NotInShopping": "{food} befindet sich nicht auf Ihrer Einkaufsliste.",
"OnHand": "Aktuell vorrätig",
"FoodNotOnHand": "Sie habe {food} nicht vorrätig.",
"FoodNotOnHand": "Sie haben {food} nicht vorrätig.",
"Undefined": "undefiniert",
"AddFoodToShopping": "Fügen Sie {food} zur Einkaufsliste hinzu",
"RemoveFoodFromShopping": "{food} von der Einkaufsliste löschen",
@ -251,23 +251,23 @@
"mealplan_autoadd_shopping_desc": "Zutaten aus dem Essensplan automatisch zur Einkaufsliste hinzufügen.",
"Pin": "Anheften",
"mark_complete": "Vollständig markieren",
"shopping_add_onhand_desc": "Markiere Lebensmittel als \"Vorrätig\", wenn von der Einkaufsliste abgehakt wurden.",
"shopping_add_onhand_desc": "Zutat beim Abhaken auf der Einkausfliste als \"vorrätig\" kennzeichnen.",
"left_handed": "Linkshänder-Modus",
"left_handed_help": "Optimiert die Benutzeroberfläche für die Bedienung mit der linken Hand.",
"FoodInherit": "Lebensmittel vererbbare Felder",
"SupermarketCategoriesOnly": "Nur Supermarktkategorien",
"InheritWarning": "{food} ist auf Vererbung gesetzt ist, Änderungen werden möglicherweise nicht gespeichert.",
"mealplan_autoexclude_onhand_desc": "Beim (manuellen oder automatischen) Hinzufügen eines Essensplans zur Einkaufsliste vorrätige Zutagen ausnehmen.",
"InheritWarning": "{food} ist auf Vererbung gesetzt, Änderungen werden möglicherweise nicht gespeichert.",
"mealplan_autoexclude_onhand_desc": "Wenn ein Speiseplan zur Einkaufsliste zugefügt wird (manuell oder automatisch), Zutaten ausschliessen, die gerade vorrätig sind.",
"mealplan_autoinclude_related_desc": "Wenn Sie einen Essensplan zur Einkaufsliste hinzufügen (manuell oder automatisch), fügen Sie alle zugehörigen Rezepte hinzu.",
"default_delay_desc": "Voreingestellte Anzahl von Stunden für die Verzögerung eines Einkaufslisteneintrags.",
"filter_to_supermarket": "Nach Supermarkt filtern",
"err_move_self": "Element kann nicht auf sich selbst verschoben werden",
"nothing": "Nichts zu tun",
"err_merge_self": "Element kann nicht mit sich selbst zusammengeführt werden",
"show_sql": "SQL anzeigen",
"show_sql": "Zeige SQL",
"filter_to_supermarket_desc": "Standardmäßig wird die Einkaufsliste so gefiltert, dass sie nur Kategorien für den ausgewählten Supermarkt enthält.",
"CategoryName": "Kategorie Name",
"SupermarketName": "Supermarkt Name",
"CategoryName": "Kategorienname",
"SupermarketName": "Name Supermarkt",
"CategoryInstruction": "Ziehen Sie Kategorien, um die Reihenfolge zu ändern, in der die Kategorien in der Einkaufsliste erscheinen.",
"shopping_recent_days_desc": "Tage der letzten Einträge in der Einkaufsliste, die angezeigt werden sollen.",
"shopping_recent_days": "Letzte Tage",
@ -277,7 +277,7 @@
"csv_delim_help": "Trennzeichen für CSV-Exporte.",
"csv_delim_label": "CSV-Trennzeichen",
"SuccessClipboard": "Einkaufsliste wurde in die Zwischenablage kopiert",
"copy_to_clipboard": "In die Zwischenablage kopieren",
"copy_to_clipboard": "In Zwischenablage kopieren",
"csv_prefix_help": "Präfix, das beim Kopieren der Liste in die Zwischenablage hinzugefügt wird.",
"csv_prefix_label": "Listenpräfix",
"copy_markdown_table": "Als Markdown-Tabelle kopieren",
@ -291,10 +291,10 @@
"remember_search": "Suchbegriff merken",
"remember_hours": "Stunden zu erinnern",
"tree_select": "Baum-Auswahl verwenden",
"CountMore": "...+{count} weitere",
"ignore_shopping_help": "Füge Zutat nie zur Einkaufsliste hinzu (z.B. Wasser)",
"CountMore": "...+{count} mehr",
"ignore_shopping_help": "Zutat nie auf Einkaufsliste setzen (z.B. Wasser)",
"OnHand_help": "Lebensmittel ist \"Vorrätig\" und wird nicht automatisch zur Einkaufsliste hinzugefügt. Der Status \"Vorrätig\" wird mit den Benutzern der Einkaufsliste geteilt.",
"shopping_category_help": "Supermärkte können nach Einkaufskategorien geordnet und gefiltert werden, je nachdem, wie die Gänge angeordnet sind.",
"shopping_category_help": "Einkaufsläden können nach Produktkategorie entsprechend der Anordnung der Regalreihen sortiert werden.",
"Foods": "Lebensmittel",
"food_recipe_help": "Wird ein Rezept hier verknüpft, wird diese Verknüpfung in allen anderen Rezepten übernommen, die dieses Lebensmittel beinhaltet",
"review_shopping": "Überprüfe die Einkaufsliste vor dem Speichern",
@ -356,10 +356,10 @@
"search_rank": "Such-Rang",
"paste_ingredients": "Zutaten einfügen",
"Ingredient Editor": "Zutateneditor",
"Protected": "Geschützt",
"Protected": "Schützen",
"not": "nicht",
"warning_duplicate_filter": "Warnung: Wegen technischen Limitierungen können mehrere Filter der selben Kombination (und/oder/nicht) zu unerwarteten Ergebnissen führen.",
"and_down": "& Niedriger",
"and_down": "& Runter",
"enable_expert": "Expertenmodus aktivieren",
"filter_name": "Name des Filters",
"shared_with": "Geteilt mit",
@ -407,11 +407,11 @@
"Warning_Delete_Supermarket_Category": "Die Löschung einer Supermarktkategorie werden auch alle Beziehungen zu Lebensmitteln gelöscht. Bist du dir sicher?",
"New_Supermarket": "Erstelle einen neuen Supermarkt",
"New_Supermarket_Category": "Erstelle eine neue Supermarktkategorie",
"warning_space_delete": "Du kannst deinen Bereich inklusive all deiner Rezepte, Einkaufslisten, Essensplänen und allem anderen, die du erstellt hast löschen. Dieser Schritt kann nicht rückgängig gemacht werden! Bist du sicher, dass du das tun möchtest?",
"Copy Link": "Link kopieren",
"warning_space_delete": "Du kannst deinen Space inklusive all deiner Rezepte, Shoppinglisten, Essensplänen und allem anderen, das du erstellt hast löschen. Dieser Schritt kann nicht rückgängig gemacht werden! Bist du sicher, dass du das tun möchtest?",
"Copy Link": "Kopiere den Link in die Zwischenablage",
"Users": "Benutzer",
"facet_count_info": "Zeige die Anzahl der Rezepte auf den Suchfiltern.",
"Copy Token": "Token kopieren",
"Copy Token": "Kopiere Token",
"Invites": "Einladungen",
"Message": "Nachricht",
"Bookmarklet": "Lesezeichen",
@ -473,11 +473,13 @@
"UnpinnedConfirmation": "{recipe} wurde gelöst.",
"Description_Replace": "Beschreibung ersetzen",
"Instruction_Replace": "Anleitung ersetzen",
"Split_All_Steps": "Teile alle Zeilen in seperate Schritte auf.",
"Split_All_Steps": "Teile alle Zeilen in separate Schritte auf.",
"Auto_Sort_Help": "Verschiebe alle Zutaten zu dem Schritt, der am Besten passt.",
"Combine_All_Steps": "Fasse alle Schritte in einem einzelnem Feld zusammen.",
"reset_children_help": "Überschreibe alle Kinder mit den Werten der vererbten Felder. Die vererbten Felder der Kinder werden als vererbte Felder gesetzt, es sei denn, das Kind-Vererben-Feld ist gesetzt.",
"Unpin": "Lösen",
"Amount": "Menge",
"Original_Text": "Originaltext"
"Original_Text": "Originaler Text",
"Import Recipe": "Rezept importieren",
"Create Recipe": "Rezept erstellen"
}

View File

@ -469,7 +469,7 @@
"New_Supermarket_Category": "Create new supermarket category",
"Are_You_Sure": "Are you sure?",
"Valid Until": "Valid Until",
"Split_All_Steps": "Split all rows into seperate steps.",
"Split_All_Steps": "Split all rows into separate steps.",
"Combine_All_Steps": "Combine all steps into a single field.",
"Plural": "Plural",
"plural_short": "plural",

482
vue/src/locales/nb_NO.json Normal file
View File

@ -0,0 +1,482 @@
{
"warning_feature_beta": "Denne funksjonen er foreløpig i BETA-versjon (testing). Regn med feil og at det i fremtidige oppdateringer kan komme endringer som gjør funksjonen ubrukelig.",
"err_fetching_resource": "Feil ved henting av ressurs!",
"err_creating_resource": "Feil ved oppretting av ressurs!",
"err_updating_resource": "Feil ved oppdatering av ressurs!",
"err_deleting_resource": "Feil ved sletting av ressurs!",
"err_deleting_protected_resource": "Objektet du prøver å slette er fortsatt i bruk, og kan ikke slettes.",
"err_moving_resource": "Feil ved flytting av ressurs!",
"err_merging_resource": "Feil ved sammenslåing av ressurs!",
"success_fetching_resource": "Vellykket henting av ressurs!",
"success_creating_resource": "Vellykket oppretting av ressurs!",
"success_updating_resource": "Vellykket oppdatering av ressurs!",
"success_deleting_resource": "Vellykket sletting av ressurs!",
"success_moving_resource": "Vellykket flytting av ressurs!",
"success_merging_resource": "Vellykket sammenslåing av ressurs!",
"file_upload_disabled": "Opplasting av filer er ikke aktivert i området ditt.",
"warning_space_delete": "Du kan slette området, inkludert alle oppskrifter, handlelister, måltidsplaner og alt annet du har opprettet. Dette kan ikke angres! Er du sikker på at du vil gjøre dette?",
"food_inherit_info": "Felter på matvarer som skal arves som standard.",
"facet_count_info": "Vis oppskriftsantall i søkefilter.",
"step_time_minutes": "Tid for trinn, i minutter",
"confirm_delete": "Er du sikker på at du vil slette dette {object}?",
"import_running": "Importering pågår. Vennligst vent!",
"all_fields_optional": "Alle felt er valgfri, og kan stå tomme.",
"convert_internal": "Konverter til intern oppskrift",
"show_only_internal": "Vis bare interne oppskrifter",
"show_split_screen": "Delt visning",
"Log_Recipe_Cooking": "Logg oppskriftsbruk",
"External_Recipe_Image": "Bilde av ekstern oppskrift",
"Add_to_Shopping": "Legg til i handleliste",
"Add_to_Plan": "Legg til i Plan",
"Step_start_time": "Trinn starttid",
"Sort_by_new": "Sorter etter nyest",
"Table_of_Contents": "Innholdsfortegnelse",
"Recipes_per_page": "Oppskrifter per side",
"Show_as_header": "Vis som overskrift",
"Hide_as_header": "Skjul overskrift",
"Add_nutrition_recipe": "Legg til næringsinnhold til oppskrift",
"Remove_nutrition_recipe": "Fjern næringsinnhold fra oppskrift",
"Copy_template_reference": "Kopier mal-referanse",
"Save_and_View": "Lagre og vis",
"Manage_Books": "Administrer bøker",
"Meal_Plan": "Måltidsplan",
"Select_Book": "Velg bok",
"Select_File": "Velg fil",
"Recipe_Image": "Oppskriftsbilde",
"Import_finished": "Importering fullført",
"View_Recipes": "Vis oppskrifter",
"Log_Cooking": "Loggfør tilbereding",
"New_Recipe": "Ny oppskrift",
"Url_Import": "Importer lenke",
"Reset_Search": "Nullstill søk",
"Recently_Viewed": "Nylig vist",
"Load_More": "Last inn flere",
"New_Keyword": "Nytt nøkkelord",
"Delete_Keyword": "Slett nøkkelord",
"Edit_Keyword": "Rediger nøkkelord",
"Edit_Recipe": "Rediger oppskrift",
"Move_Keyword": "Flytt nøkkelord",
"Merge_Keyword": "Slå sammen nøkkelord",
"Hide_Keywords": "Skjul nøkkelord",
"Hide_Recipes": "Skjul oppskrifter",
"Move_Up": "Flytt opp",
"Move_Down": "Flytt ned",
"Step_Name": "Trinn navn",
"Step_Type": "Trinn type",
"Make_Header": "Bruk som overskrift",
"Make_Ingredient": "Bruk som ingrediens",
"Amount": "Mengde",
"Enable_Amount": "Aktiver mengde",
"Disable_Amount": "Deaktiver mengde",
"Ingredient Editor": "",
"Description_Replace": "",
"Instruction_Replace": "",
"Auto_Sort": "",
"Auto_Sort_Help": "",
"Private_Recipe": "",
"Private_Recipe_Help": "",
"reusable_help_text": "",
"Add_Step": "",
"Keywords": "",
"Books": "Bøker",
"Proteins": "",
"Fats": "",
"Carbohydrates": "Karbohydrater",
"Calories": "",
"Energy": "",
"Nutrition": "",
"Date": "",
"Share": "",
"Automation": "",
"Parameter": "",
"Export": "",
"Copy": "",
"Rating": "Karakter",
"Close": "Lukk",
"Cancel": "",
"Link": "Lenke",
"Add": "",
"New": "",
"Note": "",
"Success": "",
"Failure": "",
"Protected": "",
"Ingredients": "Ingredienser",
"Supermarket": "Butikk",
"Categories": "",
"Category": "",
"Selected": "",
"min": "",
"Servings": "",
"Waiting": "",
"Preparation": "",
"External": "",
"Size": "",
"Files": "",
"File": "",
"Edit": "",
"Image": "",
"Delete": "",
"Open": "",
"Ok": "",
"Save": "",
"Step": "",
"Search": "",
"Import": "",
"Print": "",
"Settings": "Innstillinger",
"or": "",
"and": "",
"Information": "",
"Download": "",
"Create": "Opprett",
"Search Settings": "",
"View": "",
"Recipes": "",
"Move": "",
"Merge": "",
"Parent": "",
"Copy Link": "",
"Copy Token": "",
"delete_confirmation": "",
"move_confirmation": "",
"merge_confirmation": "",
"create_rule": "",
"move_selection": "",
"merge_selection": "",
"Root": "",
"Ignore_Shopping": "",
"Shopping_Category": "",
"Shopping_Categories": "",
"Edit_Food": "",
"Move_Food": "",
"New_Food": "",
"Hide_Food": "",
"Food_Alias": "",
"Unit_Alias": "",
"Keyword_Alias": "",
"Delete_Food": "",
"No_ID": "",
"Meal_Plan_Days": "",
"merge_title": "",
"move_title": "",
"Food": "",
"Original_Text": "",
"Recipe_Book": "",
"del_confirmation_tree": "",
"delete_title": "",
"create_title": "",
"edit_title": "",
"Name": "",
"Type": "",
"Description": "",
"Recipe": "",
"tree_root": "",
"Icon": "",
"Unit": "",
"Decimals": "",
"Default_Unit": "",
"No_Results": "",
"New_Unit": "",
"Create_New_Shopping Category": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
"Create_New_Unit": "",
"Create_New_Meal_Type": "",
"Create_New_Shopping_Category": "",
"and_up": "",
"and_down": "",
"Instructions": "",
"Unrated": "",
"Automate": "",
"Empty": "",
"Key_Ctrl": "",
"Key_Shift": "",
"Time": "",
"Text": "",
"Shopping_list": "",
"Added_by": "",
"Added_on": "",
"AddToShopping": "",
"IngredientInShopping": "",
"NotInShopping": "",
"OnHand": "",
"FoodOnHand": "",
"FoodNotOnHand": "",
"Undefined": "",
"Create_Meal_Plan_Entry": "",
"Edit_Meal_Plan_Entry": "",
"Title": "",
"Week": "",
"Month": "",
"Year": "",
"Planner": "",
"Planner_Settings": "",
"Period": "",
"Plan_Period_To_Show": "",
"Periods": "",
"Plan_Show_How_Many_Periods": "",
"Starting_Day": "",
"Meal_Types": "",
"Meal_Type": "",
"New_Entry": "",
"Clone": "",
"Drag_Here_To_Delete": "",
"Meal_Type_Required": "",
"Title_or_Recipe_Required": "",
"Color": "",
"New_Meal_Type": "",
"Use_Fractions": "",
"Use_Fractions_Help": "",
"AddFoodToShopping": "",
"RemoveFoodFromShopping": "",
"DeleteShoppingConfirm": "",
"IgnoredFood": "",
"Add_Servings_to_Shopping": "",
"Week_Numbers": "",
"Show_Week_Numbers": "",
"Export_As_ICal": "",
"Export_To_ICal": "",
"Cannot_Add_Notes_To_Shopping": "",
"Added_To_Shopping_List": "",
"Shopping_List_Empty": "",
"Next_Period": "",
"Previous_Period": "",
"Current_Period": "",
"Next_Day": "",
"Previous_Day": "",
"Inherit": "",
"InheritFields": "",
"FoodInherit": "",
"ShowUncategorizedFood": "",
"GroupBy": "",
"Language": "",
"Theme": "",
"SupermarketCategoriesOnly": "",
"MoveCategory": "",
"CountMore": "",
"IgnoreThis": "",
"DelayFor": "",
"Warning": "",
"NoCategory": "",
"InheritWarning": "",
"ShowDelayed": "",
"Completed": "",
"OfflineAlert": "",
"shopping_share": "",
"shopping_auto_sync": "",
"one_url_per_line": "",
"mealplan_autoadd_shopping": "",
"mealplan_autoexclude_onhand": "",
"mealplan_autoinclude_related": "",
"default_delay": "",
"plan_share_desc": "",
"shopping_share_desc": "",
"shopping_auto_sync_desc": "",
"mealplan_autoadd_shopping_desc": "",
"mealplan_autoexclude_onhand_desc": "",
"mealplan_autoinclude_related_desc": "",
"default_delay_desc": "",
"filter_to_supermarket": "",
"Coming_Soon": "",
"Auto_Planner": "",
"New_Cookbook": "",
"Hide_Keyword": "",
"Hour": "",
"Hours": "",
"Day": "",
"Days": "",
"Second": "",
"Seconds": "",
"Clear": "",
"Users": "",
"Invites": "",
"err_move_self": "",
"nothing": "",
"err_merge_self": "",
"show_sql": "",
"filter_to_supermarket_desc": "",
"CategoryName": "",
"SupermarketName": "",
"CategoryInstruction": "",
"shopping_recent_days_desc": "",
"shopping_recent_days": "",
"download_pdf": "",
"download_csv": "",
"csv_delim_help": "",
"csv_delim_label": "",
"SuccessClipboard": "",
"copy_to_clipboard": "",
"csv_prefix_help": "",
"csv_prefix_label": "",
"copy_markdown_table": "",
"in_shopping": "",
"DelayUntil": "",
"Pin": "",
"Unpin": "",
"PinnedConfirmation": "",
"UnpinnedConfirmation": "",
"mark_complete": "",
"QuickEntry": "",
"shopping_add_onhand_desc": "",
"shopping_add_onhand": "",
"related_recipes": "",
"today_recipes": "",
"sql_debug": "",
"remember_search": "",
"remember_hours": "",
"tree_select": "",
"OnHand_help": "",
"ignore_shopping_help": "",
"shopping_category_help": "",
"food_recipe_help": "",
"Foods": "",
"Account": "",
"Cosmetic": "",
"API": "",
"enable_expert": "",
"expert_mode": "",
"simple_mode": "",
"advanced": "",
"fields": "",
"show_keywords": "",
"show_foods": "",
"show_books": "",
"show_rating": "",
"show_units": "",
"show_filters": "",
"not": "",
"save_filter": "",
"filter_name": "",
"left_handed": "",
"left_handed_help": "",
"Custom Filter": "",
"shared_with": "",
"sort_by": "",
"asc": "",
"desc": "",
"date_viewed": "",
"last_cooked": "",
"times_cooked": "",
"date_created": "",
"show_sortby": "",
"search_rank": "",
"make_now": "",
"recipe_filter": "",
"book_filter_help": "",
"review_shopping": "",
"view_recipe": "",
"copy_to_new": "",
"recipe_name": "",
"paste_ingredients_placeholder": "",
"paste_ingredients": "",
"ingredient_list": "",
"explain": "",
"filter": "",
"Website": "",
"App": "",
"Message": "",
"Bookmarklet": "",
"Sticky_Nav": "",
"Sticky_Nav_Help": "",
"Nav_Color": "",
"Nav_Color_Help": "",
"Use_Kj": "",
"Comments_setting": "",
"click_image_import": "",
"no_more_images_found": "",
"import_duplicates": "",
"paste_json": "",
"Click_To_Edit": "",
"search_no_recipes": "",
"search_import_help_text": "",
"search_create_help_text": "",
"warning_duplicate_filter": "",
"reset_children": "",
"reset_children_help": "",
"reset_food_inheritance": "",
"reset_food_inheritance_info": "",
"substitute_help": "",
"substitute_siblings_help": "",
"substitute_children_help": "",
"substitute_siblings": "",
"substitute_children": "",
"SubstituteOnHand": "",
"ChildInheritFields": "",
"ChildInheritFields_help": "",
"InheritFields_help": "",
"show_ingredient_overview": "",
"Ingredient Overview": "",
"last_viewed": "",
"created_on": "",
"updatedon": "",
"Imported_From": "",
"advanced_search_settings": "",
"nothing_planned_today": "",
"no_pinned_recipes": "",
"Planned": "",
"Pinned": "",
"Imported": "",
"Quick actions": "",
"Ratings": "",
"Internal": "",
"Units": "",
"Manage_Emails": "",
"Change_Password": "",
"Social_Authentication": "",
"Random Recipes": "",
"parameter_count": "",
"select_keyword": "",
"add_keyword": "",
"select_file": "",
"select_recipe": "",
"select_unit": "",
"select_food": "",
"remove_selection": "",
"empty_list": "",
"Select": "Velg",
"Supermarkets": "",
"User": "",
"Username": "",
"First_name": "",
"Last_name": "",
"Keyword": "Nøkkelord",
"Advanced": "",
"Page": "",
"Single": "",
"Multiple": "",
"Reset": "",
"Disabled": "",
"Disable": "",
"Options": "",
"Create Food": "",
"create_food_desc": "",
"additional_options": "",
"Importer_Help": "",
"Documentation": "",
"Select_App_To_Import": "",
"Import_Supported": "",
"Export_Supported": "",
"Import_Not_Yet_Supported": "",
"Export_Not_Yet_Supported": "",
"Import_Result_Info": "",
"Recipes_In_Import": "",
"Toggle": "",
"Import_Error": "",
"Warning_Delete_Supermarket_Category": "",
"New_Supermarket": "",
"New_Supermarket_Category": "",
"Are_You_Sure": "",
"Valid Until": "",
"Split_All_Steps": "",
"Combine_All_Steps": "",
"Plural": "",
"plural_short": "",
"Use_Plural_Unit_Always": "",
"Use_Plural_Unit_Simple": "",
"Use_Plural_Food_Always": "",
"Use_Plural_Food_Simple": "",
"plural_usage_info": "",
"Create Recipe": "",
"Import Recipe": ""
}

View File

@ -16,202 +16,468 @@
"convert_internal": "Transformați în rețetă internă",
"show_only_internal": "Arătați doar rețetele interne",
"show_split_screen": "Vedere divizată",
"Log_Recipe_Cooking": "",
"External_Recipe_Image": "",
"Add_to_Shopping": "",
"Add_to_Plan": "",
"Step_start_time": "",
"Sort_by_new": "",
"Table_of_Contents": "",
"Recipes_per_page": "",
"Show_as_header": "",
"Hide_as_header": "",
"Add_nutrition_recipe": "",
"Remove_nutrition_recipe": "",
"Copy_template_reference": "",
"Save_and_View": "",
"Manage_Books": "",
"Meal_Plan": "",
"Select_Book": "",
"Select_File": "",
"Recipe_Image": "",
"Import_finished": "",
"View_Recipes": "",
"Log_Cooking": "",
"New_Recipe": "",
"Url_Import": "",
"Reset_Search": "",
"Recently_Viewed": "",
"Load_More": "",
"New_Keyword": "",
"Delete_Keyword": "",
"Edit_Keyword": "",
"Edit_Recipe": "",
"Move_Keyword": "",
"Merge_Keyword": "",
"Hide_Keywords": "",
"Hide_Recipes": "",
"Move_Up": "",
"Move_Down": "",
"Step_Name": "",
"Step_Type": "",
"Make_Header": "",
"Make_Ingredient": "",
"Enable_Amount": "",
"Disable_Amount": "",
"Add_Step": "",
"Keywords": "",
"Books": "",
"Proteins": "",
"Fats": "",
"Carbohydrates": "",
"Calories": "",
"Energy": "",
"Nutrition": "",
"Date": "",
"Share": "",
"Automation": "",
"Parameter": "",
"Export": "",
"Copy": "",
"Rating": "",
"Close": "",
"Cancel": "",
"Link": "",
"Add": "",
"New": "",
"Note": "",
"Success": "",
"Failure": "",
"Ingredients": "",
"Supermarket": "",
"Categories": "",
"Category": "",
"Selected": "",
"min": "",
"Servings": "",
"Waiting": "",
"Preparation": "",
"External": "",
"Size": "",
"Files": "",
"File": "",
"Edit": "",
"Image": "",
"Delete": "",
"Open": "",
"Ok": "",
"Save": "",
"Step": "",
"Search": "",
"Import": "",
"Print": "",
"Settings": "",
"or": "",
"and": "",
"Information": "",
"Download": "",
"Create": "",
"Log_Recipe_Cooking": "Jurnalul rețetelor de pregătire",
"External_Recipe_Image": "Imagine rețetă externă",
"Add_to_Shopping": "Adaugare la cumpărături",
"Add_to_Plan": "Adăugare la plan",
"Step_start_time": "Pasule de începere a orei",
"Sort_by_new": "Sortare după nou",
"Table_of_Contents": "Cuprins",
"Recipes_per_page": "Rețete pe pagină",
"Show_as_header": "Afișare ca antet",
"Hide_as_header": "Ascunderea ca antet",
"Add_nutrition_recipe": "Adăugare a nutriției la rețetă",
"Remove_nutrition_recipe": "Ștergere a nutriției din rețetă",
"Copy_template_reference": "Copie referința șablonului",
"Save_and_View": "Salvare și vizionare",
"Manage_Books": "Gestionarea cărților",
"Meal_Plan": "Plan de alimentare",
"Select_Book": "Selectare carte",
"Select_File": "Selectare fișier",
"Recipe_Image": "Imagine a rețetei",
"Import_finished": "Importare finalizată",
"View_Recipes": "Vizionare rețete",
"Log_Cooking": "Jurnal de pregătire",
"New_Recipe": "Rețetă nouă",
"Url_Import": "Importă URL",
"Reset_Search": "Resetarea căutării",
"Recently_Viewed": "Vizualizate recent",
"Load_More": "Încărcați mai mult",
"New_Keyword": "Cuvânt cheie nou",
"Delete_Keyword": "Ștergere cuvânt cheie",
"Edit_Keyword": "Editează cuvânt cheie",
"Edit_Recipe": "Editează rețeta",
"Move_Keyword": "Mută cuvânt cheie",
"Merge_Keyword": "Unește cuvânt cheie",
"Hide_Keywords": "Ascunde cuvânt cheie",
"Hide_Recipes": "Ascunde rețetele",
"Move_Up": "Deplasați-vă în sus",
"Move_Down": "Deplasați-vă în jos",
"Step_Name": "Nume pas",
"Step_Type": "Tip pas",
"Make_Header": "Creare antet",
"Make_Ingredient": "Create ingredient",
"Enable_Amount": "Activare cantitate",
"Disable_Amount": "Dezactivare cantitate",
"Add_Step": "Adaugă pas",
"Keywords": "Cuvinte cheie",
"Books": "Cărți",
"Proteins": "Proteine",
"Fats": "Grăsimi",
"Carbohydrates": "Carbohidrați",
"Calories": "Calorii",
"Energy": "Energie",
"Nutrition": "Nutriție",
"Date": "Dată",
"Share": "Împărtășire",
"Automation": "Automatizare",
"Parameter": "Parametru",
"Export": "Exportă",
"Copy": "Copie",
"Rating": "Evaluare",
"Close": "Închide",
"Cancel": "Anulează",
"Link": "Link",
"Add": "Adaugă",
"New": "Nou",
"Note": "Notă",
"Success": "Succes",
"Failure": "Eșec",
"Ingredients": "Ingrediente",
"Supermarket": "Supermarket",
"Categories": "Categorii",
"Category": "Categorie",
"Selected": "Selectat",
"min": "min",
"Servings": "Porții",
"Waiting": "Așteptare",
"Preparation": "Pregătire",
"External": "Extern",
"Size": "Marime",
"Files": "Fișiere",
"File": "Fișier",
"Edit": "Editează",
"Image": "Imagine",
"Delete": "Șterge",
"Open": "Deschide",
"Ok": "Ok",
"Save": "Salvare",
"Step": "Pas",
"Search": "Căutare",
"Import": "Importă",
"Print": "Tipărește",
"Settings": "Setări",
"or": "sau",
"and": "și",
"Information": "Informație",
"Download": "Descarcă",
"Create": "Creează",
"Advanced Search Settings": "",
"View": "",
"Recipes": "",
"Move": "",
"Merge": "",
"Parent": "",
"delete_confirmation": "",
"move_confirmation": "",
"merge_confirmation": "",
"create_rule": "",
"move_selection": "",
"merge_selection": "",
"Root": "",
"Ignore_Shopping": "",
"Shopping_Category": "",
"Edit_Food": "",
"Move_Food": "",
"New_Food": "",
"Hide_Food": "",
"Food_Alias": "",
"Unit_Alias": "",
"Keyword_Alias": "",
"Delete_Food": "",
"No_ID": "",
"Meal_Plan_Days": "",
"merge_title": "",
"move_title": "",
"Food": "",
"Recipe_Book": "",
"del_confirmation_tree": "",
"delete_title": "",
"create_title": "",
"edit_title": "",
"Name": "",
"Type": "",
"Description": "",
"Recipe": "",
"tree_root": "",
"Icon": "",
"Unit": "",
"No_Results": "",
"New_Unit": "",
"Create_New_Shopping Category": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
"Create_New_Unit": "",
"Create_New_Meal_Type": "",
"and_up": "",
"Instructions": "",
"Unrated": "",
"Automate": "",
"Empty": "",
"Key_Ctrl": "",
"Key_Shift": "",
"Time": "",
"Text": "",
"Shopping_list": "",
"Create_Meal_Plan_Entry": "",
"Edit_Meal_Plan_Entry": "",
"Title": "",
"Week": "",
"Month": "",
"Year": "",
"Planner": "",
"Planner_Settings": "",
"Period": "",
"Plan_Period_To_Show": "",
"Periods": "",
"Plan_Show_How_Many_Periods": "",
"Starting_Day": "",
"Meal_Types": "",
"Meal_Type": "",
"Clone": "",
"Drag_Here_To_Delete": "",
"Meal_Type_Required": "",
"Title_or_Recipe_Required": "",
"Color": "",
"New_Meal_Type": "",
"Week_Numbers": "",
"Show_Week_Numbers": "",
"Export_As_ICal": "",
"Export_To_ICal": "",
"Cannot_Add_Notes_To_Shopping": "",
"Added_To_Shopping_List": "",
"Shopping_List_Empty": "",
"Next_Period": "",
"Previous_Period": "",
"Current_Period": "",
"Next_Day": "",
"Previous_Day": "",
"Coming_Soon": "",
"Auto_Planner": "",
"New_Cookbook": "",
"Hide_Keyword": "",
"Clear": "",
"Plural": "",
"plural_short": "",
"Use_Plural_Unit_Always": "",
"Use_Plural_Unit_Simple": "",
"Use_Plural_Food_Always": "",
"Use_Plural_Food_Simple": "",
"plural_usage_info": ""
"View": "Vizualizare",
"Recipes": "Rețete",
"Move": "Mută",
"Merge": "Unire",
"Parent": "Părinte",
"delete_confirmation": "Sunteți sigur că doriți să ștergeți {source}?",
"move_confirmation": "Mutare <i>{copil}</i> la părinte <i>{părinte}</i>",
"merge_confirmation": "Înlocuiți <i>{source}</i> cu <i>{target}</i>",
"create_rule": "și crearea automatizării",
"move_selection": "Selectați un părinte {type} pentru a muta {source} în.",
"merge_selection": "Înlocuiți toate aparițiile {source} cu {type} selectat.",
"Root": "Rădăcină",
"Ignore_Shopping": "Ignoră cumpărăturile",
"Shopping_Category": "Categorie de cumpărături",
"Edit_Food": "Editare mâncare",
"Move_Food": "Mutare mâncare",
"New_Food": "Mâncare nouă",
"Hide_Food": "Ascunde mâncare",
"Food_Alias": "Pseudonim mâncare",
"Unit_Alias": "Pseudonim unitate",
"Keyword_Alias": "Pseudonim cuvânt cheie",
"Delete_Food": "Ștergere mâncare",
"No_ID": "ID-ul nu a fost găsit, nu se poate șterge.",
"Meal_Plan_Days": "Planuri de alimentație pe viitor",
"merge_title": "Unire {type}",
"move_title": "Mutare {type}",
"Food": "Mâncare",
"Recipe_Book": "Carte de rețete",
"del_confirmation_tree": "Sunteți sigur că doriți să ștergeți {sursa} și toți copiii săi?",
"delete_title": "Ștergere {type}",
"create_title": "{type} nou",
"edit_title": "Editare {type}",
"Name": "Nume",
"Type": "Tip",
"Description": "Descriere",
"Recipe": "Rețetă",
"tree_root": "Rădăcina copacului",
"Icon": "Iconiță",
"Unit": "Unitate",
"No_Results": "Fără rezultate",
"New_Unit": "Unitate nouă",
"Create_New_Shopping Category": "Creați o nouă categorie de cumpărături",
"Create_New_Food": "Adaugă mâncare nouă",
"Create_New_Keyword": "Adaugă cuvânt cheie nou",
"Create_New_Unit": "Adaugă unitate nouă",
"Create_New_Meal_Type": "Adaugă tip mâncare nou",
"and_up": "& Sus",
"Instructions": "Instrucțiuni",
"Unrated": "Neevaluat",
"Automate": "Automatizat",
"Empty": "Gol",
"Key_Ctrl": "Ctrl",
"Key_Shift": "Shift",
"Time": "Timp",
"Text": "Text",
"Shopping_list": "Lisă de cumpărături",
"Create_Meal_Plan_Entry": "Crearea înregistrării în planul de alimentare",
"Edit_Meal_Plan_Entry": "Editarea înregistrării în planul de alimentare",
"Title": "Titlu",
"Week": "Săptămână",
"Month": "Lună",
"Year": "An",
"Planner": "Planificator",
"Planner_Settings": "Setări planificator",
"Period": "Perioadă",
"Plan_Period_To_Show": "Afișați săptămâni, luni sau ani",
"Periods": "Perioade",
"Plan_Show_How_Many_Periods": "Câte perioade să afișezi",
"Starting_Day": "Ziua de început a săptămânii",
"Meal_Types": "Tipuri de mese",
"Meal_Type": "Tipul mesei",
"Clone": "Clonă",
"Drag_Here_To_Delete": "Mută aici pentru a șterge",
"Meal_Type_Required": "Tipul mesei este necesar",
"Title_or_Recipe_Required": "Titlul sau selecția rețetei necesare",
"Color": "Culoare",
"New_Meal_Type": "Tip de masă nou",
"Week_Numbers": "Numerele săptămânii",
"Show_Week_Numbers": "Afișați numerele săptămânii?",
"Export_As_ICal": "Exportul perioadei curente în format iCal",
"Export_To_ICal": "Exportă .ics",
"Cannot_Add_Notes_To_Shopping": "Notele nu pot fi adăugate la lista de cumpărături",
"Added_To_Shopping_List": "Adăugat la lista de cumpărături",
"Shopping_List_Empty": "Lista de cumpărături este în prezent goală, puteți adăuga articole prin meniul contextual al unei intrări în planul de alimentație (faceți click dreapta pe card sau faceți click stânga pe iconița meniului)",
"Next_Period": "Perioada următoare",
"Previous_Period": "Perioada precedentă",
"Current_Period": "Perioada curentă",
"Next_Day": "Ziua următoare",
"Previous_Day": "Ziua precedentă",
"Coming_Soon": "În curând",
"Auto_Planner": "Planificator automat",
"New_Cookbook": "Nouă carte de bucate",
"Hide_Keyword": "Ascunde cuvintele cheie",
"Clear": "Curățare",
"Plural": "Plural",
"plural_short": "plural",
"Use_Plural_Unit_Always": "Utilizarea formei plurale pentru unitate întotdeauna",
"Use_Plural_Unit_Simple": "Utilizarea dinamică a formei plurale pentru unitate",
"Use_Plural_Food_Always": "Utilizarea formei plurale pentru alimente întotdeauna",
"Use_Plural_Food_Simple": "Utilizarea dinamica a formei plurale pentru alimente",
"plural_usage_info": "Utilizarea formei plurale pentru unități și alimente în interiorul acestui spațiu.",
"last_viewed": "Ultima vizualizare",
"created_on": "Creat la data de",
"updatedon": "Actualizat la data de",
"Imported_From": "Importat din",
"and_down": "& Jos",
"Warning": "Atenționare",
"ShowDelayed": "Afișarea elementelor întârziate",
"shopping_share_desc": "Utilizatorii vor vedea toate articolele pe care le adăugați în lista de cumpărături. Ei trebuie să vă adauge pentru a vedea elementele din lista lor.",
"mealplan_autoinclude_related_desc": "Atunci când adăugați un plan de alimentare în lista de cumpărături (manual sau automat), includeți toate rețetele asociate.",
"SuccessClipboard": "Lista de cumpărături copiată în clipboard",
"in_shopping": "În lista de cumpărături",
"not": "nu",
"Pin": "Fixează",
"Create Recipe": "Crearea rețetei",
"Import Recipe": "Importă rețeta",
"csv_prefix_label": "Prefix a listei",
"Click_To_Edit": "Faceți click pentru a edita",
"Ingredient Editor": "Editor de ingrediente",
"FoodOnHand": "Aveți {food} la îndemână.",
"AddFoodToShopping": "Adăugă {food} în lista de cumpărături",
"New_Entry": "Înregistrare nouă",
"GroupBy": "Grupat de",
"CountMore": "...+{count} mai mult",
"IgnoreThis": "Nu adăugați niciodată automat {food} la cumpărături",
"InheritWarning": "{food} este setat să moștenească, este posibil ca modificările să nu persiste.",
"err_move_self": "Nu se poate muta elementul în sine",
"CategoryName": "Nume categorie",
"Foods": "Alimente",
"copy_to_new": "Copiere in rețetă nouă",
"reset_children": "Resetarea moștenirii copilului",
"err_moving_resource": "A existat o eroare în mutarea unei resurse!",
"err_merging_resource": "A existat o eroare la fuzionarea unei resurse!",
"success_moving_resource": "Resursă mutată cu succes!",
"success_merging_resource": "A fuzionat cu succes o resursă!",
"Decimals": "Zecimale",
"Default_Unit": "Unitate standard",
"Use_Fractions": "Folosire fracțiuni",
"Use_Fractions_Help": "Convertiți automat zecimalele în fracții atunci când vizualizați o rețetă.",
"RemoveFoodFromShopping": "Șterge {food} din lista de cumpărături",
"IgnoredFood": "{food} este setat să ignore cumpărăturile.",
"Add_Servings_to_Shopping": "Adăugă {servings} porții la cumpărături",
"InheritFields": "Moștenirea valorilor câmpurilor",
"Language": "Limba",
"Theme": "Tema",
"NoCategory": "Nicio categorie selectată.",
"OfflineAlert": "Sunteți offline, este posibil ca lista de cumpărături să nu se sincronizeze.",
"mealplan_autoinclude_related": "Adăugați rețete asociate",
"shopping_auto_sync": "Sincronizare automată",
"mealplan_autoadd_shopping": "Adăugare automată a planului de alimentare",
"default_delay": "Ore de întârziere implicite",
"plan_share_desc": "Noile intrări din Planul de alimentare vor fi partajate automat cu utilizatorii selectați.",
"shopping_auto_sync_desc": "Setarea la 0 va dezactiva sincronizarea automată. Atunci când vizualizați o listă de cumpărături, lista este actualizată la fiecare câteva secunde setate pentru a sincroniza modificările pe care altcineva le-ar fi putut face. Util atunci când faceți cumpărături cu mai multe persoane, dar va folosi mai multe date mobile.",
"mealplan_autoexclude_onhand_desc": "Atunci când adăugați un plan de alimentare în lista de cumpărături (manual sau automat), excludeți ingredientele care sunt în prezent la îndemână.",
"default_delay_desc": "Numărul implicit de ore pentru a întârzia o intrare în lista de cumpărături.",
"Hour": "Oră",
"Hours": "Ore",
"Day": "Zi",
"Days": "Zile",
"Second": "Secundă",
"Seconds": "Secunde",
"Users": "Utilizatori",
"Invites": "Invită",
"nothing": "Nimic de făcut",
"err_merge_self": "Nu se poate uni elementul cu el însuși",
"download_csv": "Descarcă CSV",
"Account": "Cont",
"Cosmetic": "Cosmetice",
"API": "API",
"left_handed_help": "Va optimiza interfața de utilizare pentru utilizare cu mâna stângă.",
"Custom Filter": "Filtru personalizat",
"recipe_name": "Nume rețetă",
"paste_ingredients": "Inserați ingredientele",
"Website": "Site web",
"Nav_Color_Help": "Modificare culoare navigare.",
"Use_Kj": "Utilizare kJ în loc de kcal",
"Username": "Nume utilizator",
"First_name": "Prenume",
"Last_name": "Nume de familie",
"Keyword": "Cuvânt cheie",
"Advanced": "Avansat",
"Page": "Pagină",
"User": "Utilizator",
"Shopping_Categories": "Categorii de cumpărături",
"Single": "Singur",
"Multiple": "Multiplu",
"Reset": "Resetare",
"Disabled": "Dezactivat",
"Disable": "Dezactivare",
"Importer_Help": "Mai multe informații și ajutor cu privire la acest importator:",
"Documentation": "Documentație",
"Import_Error": "A apărut o eroare în timpul importului. Vă rugăm să extindeți detaliile din partea de jos a paginii pentru a le vizualiza.",
"Warning_Delete_Supermarket_Category": "Ștergerea unei categorii de supermarketuri va șterge, de asemenea, toate relațiile cu alimentele. Sunteți sigur?",
"one_url_per_line": "O adresă URL pe linie",
"mealplan_autoexclude_onhand": "Excludeți alimentele la îndemână",
"shopping_recent_days": "Zilele recente",
"download_pdf": "Descarcă PDF",
"filter": "Filtru",
"Search Settings": "Setări de căutare",
"err_deleting_protected_resource": "Obiectul pe care încercați să îl ștergeți este încă utilizat și nu poate fi șters.",
"csv_delim_help": "Delimitatorul utilizat pentru exporturile CSV.",
"csv_delim_label": "Delimitatorul CSV",
"SupermarketCategoriesOnly": "Numai categorii de supermarket-uri",
"shopping_category_help": "Supermarket-urile pot fi ordonate și filtrate în funcție de categoria de cumpărături în conformitate cu aspectul culoarului.",
"food_recipe_help": "Legarea unei rețete aici va include rețeta legată în orice altă rețetă care utilizează acest aliment",
"Private_Recipe": "Rețetă privată",
"DelayUntil": "Amână până la",
"shared_with": "Împărtășit cu",
"asc": "Crescător",
"desc": "Descrescător",
"date_viewed": "Ultimul vizionat",
"show_sortby": "Afișează sortat de",
"Quick actions": "Acțiuni rapide",
"Internal": "Intern",
"parameter_count": "Parametru {count}",
"Ratings": "Evaluări",
"warning_space_delete": "Puteți șterge spațiul, inclusiv toate rețetele, listele de cumpărături, planurile de alimentare și orice altceva ați creat. Acest lucru nu poate fi anulat! Sunteți sigur că doriți să faceți acest lucru?",
"remember_hours": "Ore de reținut",
"tree_select": "Utilizarea selecției arborilor",
"last_cooked": "Ultimul pregătit",
"Auto_Sort": "Sortare automatizată",
"Private_Recipe_Help": "Rețeta este arătată doar ție și oamenilor cu care este împărtășită.",
"save_filter": "Salvare filtru",
"Nav_Color": "Culoare navigare",
"Comments_setting": "Afișează comentarii",
"search_no_recipes": "Nu a putut găsi nici o rețetă!",
"Supermarkets": "Supermarket-uri",
"Undefined": "Nedefinit",
"Select": "Selectare",
"food_inherit_info": "Câmpuri pe alimente care ar trebui să fie moștenite în mod implicit.",
"facet_count_info": "Afișarea numărului de rețete pe filtrele de căutare.",
"Amount": "Cantitate",
"Auto_Sort_Help": "Mutați toate ingredientele la cel mai potrivit pas.",
"search_create_help_text": "Creați o rețetă nouă direct în Tandoor.",
"reusable_help_text": "Ar trebui link-ul de invitație să poată fi utilizat de mai mulți utilizatori.",
"Copy Link": "Copiere link",
"AddToShopping": "Adaugă la lista de cumpărături",
"FoodNotOnHand": "Nu aveți {food} la îndemână.",
"DeleteShoppingConfirm": "Sunteți sigur că doriți să eliminați toate {food} din lista de cumpărături?",
"mealplan_autoadd_shopping_desc": "Adăugați automat ingredientele planului de alimentare în lista de cumpărături.",
"filter_to_supermarket_desc": "În mod implicit, filtrați lista de cumpărături pentru a include numai categoriile pentru supermarketul selectat.",
"CategoryInstruction": "Trageți categoriile pentru a schimba categoriile de comenzi care apar în lista de cumpărături.",
"copy_markdown_table": "Copiere ca tabel Markdown",
"sql_debug": "Depanare SQL",
"remember_search": "Rețineți căutarea",
"OnHand_help": "Alimentele sunt în inventar și nu vor fi adăugate automat la o listă de cumpărături. Starea la îndemână este partajată cu utilizatorii de cumpărături.",
"show_rating": "Afișează evaluarea",
"search_rank": "Rang de căutare",
"book_filter_help": "Includeți rețete din filtrul de rețete în plus față de cele atribuite manual.",
"Sticky_Nav_Help": "Afișați întotdeauna meniul de navigare din partea de sus a ecranului.",
"import_duplicates": "Pentru a preveni duplicatele, rețetele cu același nume ca și cele existente sunt ignorate. Bifați această casetă pentru a importa totul.",
"warning_duplicate_filter": "Atenționare: Din cauza limitărilor tehnice care au mai multe filtre de aceeași combinație (și/sau/nu) ar putea da rezultate neașteptate.",
"substitute_help": "Înlocuitorii sunt luați în considerare atunci când căutați rețete care pot fi făcute cu ingrediente la îndemână.",
"substitute_children": "Înlocuire copii",
"SubstituteOnHand": "Ai un înlocuitor la îndemână.",
"InheritFields_help": "Valorile acestor câmpuri vor fi moștenite de la părinte (Excepție: categoriile de cumpărături necompletate nu sunt moștenite)",
"Social_Authentication": "Autentificare socială",
"empty_list": "Lista este goală.",
"Select_App_To_Import": "Selectați o aplicație din care să importați",
"Recipes_In_Import": "Rețete în fișierul de import",
"Split_All_Steps": "Împărțiți toate rândurile în pași separați.",
"Description_Replace": "Înlocuire descripție",
"Instruction_Replace": "Înlocuire instrucții",
"Copy Token": "Copiere token",
"ShowUncategorizedFood": "Afișează nedefinit",
"MoveCategory": "Mută la: ",
"DelayFor": "Întârziere pentru {hours} ore",
"Completed": "Completat",
"shopping_share": "Partajați lista de cumpărături",
"filter_to_supermarket": "Filtrați la supermarket",
"show_sql": "Afișează SQL",
"SupermarketName": "Numele supermarketului",
"FoodInherit": "Câmpuri moștenite de alimente",
"mark_complete": "Marcare completată",
"shopping_add_onhand_desc": "Marcați mâncarea 'La îndemână' atunci când este bifată de pe lista de cumpărături.",
"shopping_add_onhand": "La îndemână automat",
"related_recipes": "Rețete înrudite",
"ignore_shopping_help": "Nu adăugați niciodată alimente pe lista de cumpărături (ex. apă)",
"today_recipes": "Rețete de astăzi",
"enable_expert": "Activarea modului Expert",
"expert_mode": "Modul Expert",
"simple_mode": "Modul Simplu",
"advanced": "Avansat",
"Unpin": "Anularea fixării",
"Protected": "Protejat",
"Original_Text": "Text original",
"Create_New_Shopping_Category": "Adaugă categorie de cumpărături nouă",
"Added_by": "Adăugat de",
"Added_on": "Adăugat la",
"IngredientInShopping": "Acest ingredient se află în lista de cumpărături.",
"NotInShopping": "{food} nu se află în lista de cumpărături.",
"OnHand": "În prezent, la îndemână",
"Inherit": "Moștenire",
"shopping_recent_days_desc": "Zile de intrări recente lista de cumpărături pentru a afișa.",
"copy_to_clipboard": "Copierea în Clipboard",
"csv_prefix_help": "Prefix de adăugat la copierea listei în clipboard.",
"PinnedConfirmation": "{recipe} a fost fixată.",
"UnpinnedConfirmation": "Fixarea {recipe} a fost anulată.",
"QuickEntry": "Înscriere rapidă",
"fields": "Câmpuri",
"show_keywords": "Afișează cuvinte cheie",
"show_foods": "Afișează mâncări",
"show_books": "Afișează cărți",
"show_units": "Afișează unitățile",
"show_filters": "Afișează filtrele",
"filter_name": "Nume filtru",
"left_handed": "Modul stângaci",
"sort_by": "Sortat de",
"times_cooked": "Ori pregătite",
"date_created": "Data creării",
"make_now": "Creează acum",
"recipe_filter": "Filtru rețete",
"review_shopping": "Examinați intrările de cumpărături înainte de a salva",
"view_recipe": "Vizionează rețeta",
"paste_ingredients_placeholder": "Inserați lista de ingrediente aici...",
"ingredient_list": "Lista de ingrediente",
"explain": "Explicație",
"App": "Aplicație",
"Message": "Mesaj",
"Sticky_Nav": "Navigare lipicioasă",
"click_image_import": "Faceți click pe imaginea pe care doriți să o importați pentru această rețetă",
"no_more_images_found": "Nu există imagini suplimentare găsite pe site-ul web.",
"paste_json": "Inserați sursă JSON sau HTML aici pentru a încărca rețetă.",
"search_import_help_text": "Importați o rețetă de pe un site web sau o aplicație externă.",
"reset_children_help": "Suprascrieți toți copiii cu valori din câmpurile moștenite. Câmpurile moștenite ale copiilor vor fi setate la câmpuri standard, cu excepția cazului în care sunt setate câmpurile moștenite de copii.",
"reset_food_inheritance": "Resetați moștenirea",
"reset_food_inheritance_info": "Resetați toate alimentele la câmpurile moștenite implicit și la valorile părinte ale acestora.",
"substitute_siblings_help": "Toate alimentele care împărtășesc un părinte al acestui aliment sunt considerate înlocuitori.",
"substitute_children_help": "Toate alimentele care sunt copii ai acestui aliment sunt considerate înlocuitori.",
"substitute_siblings": "Înlocuire frați",
"Bookmarklet": "Marcaj",
"ChildInheritFields": "Copiii moștenesc câmpurile",
"ChildInheritFields_help": "Copiii vor moșteni aceste câmpuri în mod implicit.",
"show_ingredient_overview": "Afișați o listă cu toate ingredientele la începutul rețetei.",
"Ingredient Overview": "Prezentare generală a ingredientelor",
"advanced_search_settings": "Setări avansate de căutare",
"nothing_planned_today": "Nu ai nimic planificat pentru ziua de azi!",
"no_pinned_recipes": "Nu ai rețete fixate!",
"Planned": "Planificate",
"Pinned": "Fixate",
"Imported": "Importate",
"Units": "Unități",
"Manage_Emails": "Gestionarea e-mailurilor",
"Change_Password": "Schimbați parola",
"Random Recipes": "Rețete aleatoare",
"select_keyword": "Selectați cuvânt cheie",
"add_keyword": "Adăugare cuvânt cheie",
"select_file": "Selectare fișier",
"select_recipe": "Selectare rețetă",
"select_unit": "Selectare unitate",
"select_food": "Selectare mâncare",
"remove_selection": "Deselectare",
"Options": "Opțiuni",
"Create Food": "Creare mâncare",
"create_food_desc": "Creați un aliment și conectați-l la această rețetă.",
"additional_options": "Opțiuni suplimentare",
"Import_Supported": "Import compatibil",
"Export_Supported": "Export compatibil",
"Import_Not_Yet_Supported": "Importul încă nu este compatibil",
"Export_Not_Yet_Supported": "Exportul încă nu este compatibil",
"Import_Result_Info": "{imported} din {total} rețete au fost importate",
"Toggle": "Comutare",
"New_Supermarket": "Creați un supermarket nou",
"New_Supermarket_Category": "Creați o nouă categorie de supermarket-uri",
"Are_You_Sure": "Sunteți sigur?",
"Valid Until": "Valabil până la",
"Combine_All_Steps": "Combinați toți pașii într-un singur câmp."
}

View File

@ -286,7 +286,7 @@
"expert_mode": "Экспертный режим",
"enable_expert": "Включить экспертный режим",
"review_shopping": "Просмотрите записи о покупках перед сохранением",
"empty_list": "Список пуст",
"empty_list": "Список пуст.",
"default_delay_desc": "Число часов по умолчанию для отсрочки записи в списке покупок.",
"one_url_per_line": "Один URL в строке",
"mealplan_autoinclude_related": "Добавить сопутствующие рецепты",
@ -343,5 +343,8 @@
"DelayFor": "Отложить на {hours} часов",
"New_Entry": "Новая запись",
"GroupBy": "Сгруппировать по",
"facet_count_info": "Показывать количество рецептов в фильтрах поиска."
"facet_count_info": "Показывать количество рецептов в фильтрах поиска.",
"food_inherit_info": "Поля для продуктов питания, которые должны наследоваться по умолчанию.",
"warning_space_delete": "Вы можете удалить свое пространство, включая все рецепты, списки покупок, планы питания и все остальное, что вы создали. Этого нельзя отменить! Ты уверен, что хочешь это сделать?",
"Description_Replace": "Изменить описание"
}