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_BIND_PASSWORD=
#AUTH_LDAP_USER_SEARCH_BASE_DN= #AUTH_LDAP_USER_SEARCH_BASE_DN=
#AUTH_LDAP_TLS_CACERTFILE= #AUTH_LDAP_TLS_CACERTFILE=
#AUTH_LDAP_START_TLS=
# Enables exporting PDF (see export docs) # Enables exporting PDF (see export docs)
# Disabled by default, uncomment to enable # Disabled by default, uncomment to enable

View File

@ -115,13 +115,17 @@ jobs:
needs: build-container needs: build-container
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
steps: steps:
- name: Set tag name
run: |
# Strip "refs/tags/" prefix
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
# Send stable discord notification # Send stable discord notification
- name: Discord notification - name: Discord notification
env: env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2 uses: Ilshidur/action-discord@0.3.2
with: 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: notify-beta:
name: Notify Beta name: Notify Beta

View File

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

View File

@ -1,8 +1,8 @@
import random # import random
import re import re
from html import unescape from html import unescape
from unicodedata import decomposition
from django.core.cache import caches
from django.utils.dateparse import parse_duration from django.utils.dateparse import parse_duration
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from isodate import parse_duration as iso_parse_duration from isodate import parse_duration as iso_parse_duration
@ -10,9 +10,11 @@ from isodate.isoerror import ISO8601Error
from pytube import YouTube from pytube import YouTube
from recipe_scrapers._utils import get_host_name, get_minutes 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.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 # 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: try:
if scrape.author(): if scrape.author():
keywords.append(scrape.author()) keywords.append(scrape.author())
except: except Exception:
pass pass
try: try:
@ -367,10 +369,28 @@ def parse_time(recipe_time):
def parse_keywords(keyword_json, space): def parse_keywords(keyword_json, space):
keywords = [] 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 # keywords as list
for kw in keyword_json: for kw in keyword_json:
kw = normalize_string(kw) kw = normalize_string(kw)
# if alias exists use that instead
if len(kw) != 0: 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(): if k := Keyword.objects.filter(name=kw, space=space).first():
keywords.append({'label': str(k), 'name': k.name, 'id': k.id}) keywords.append({'label': str(k), 'name': k.name, 'id': k.id})
else: else:

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-29 18:42+0200\n" "POT-Creation-Date: 2022-04-29 18:42+0200\n"
"PO-Revision-Date: 2022-05-10 15:32+0000\n" "PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: zeon <zeonbg@gmail.com>\n" "Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Bulgarian <http://translate.tandoor.dev/projects/tandoor/" "Language-Team: Bulgarian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/bg/>\n" "recipes-backend/bg/>\n"
"Language: bg\n" "Language: bg\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.10.1\n" "X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34 #: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
#: .\cookbook\templates\space.html:49 .\cookbook\templates\stats.html:28 #: .\cookbook\templates\space.html:49 .\cookbook\templates\stats.html:28
@ -1433,7 +1433,7 @@ msgstr ""
#: .\cookbook\templates\index.html:29 #: .\cookbook\templates\index.html:29
msgid "Search recipe ..." msgid "Search recipe ..."
msgstr "Търсете рецепта..." msgstr "Търсете рецепта ..."
#: .\cookbook\templates\index.html:44 #: .\cookbook\templates\index.html:44
msgid "New Recipe" 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" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-29 18:42+0200\n" "POT-Creation-Date: 2022-04-29 18:42+0200\n"
"PO-Revision-Date: 2023-03-06 10:55+0000\n" "PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: Anders Obro <oebro@duck.com>\n" "Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Danish <http://translate.tandoor.dev/projects/tandoor/" "Language-Team: Danish <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/da/>\n" "recipes-backend/da/>\n"
"Language: 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" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n" "POT-Creation-Date: 2021-04-11 15:09+0200\n"
"PO-Revision-Date: 2021-04-11 15:23+0000\n" "PO-Revision-Date: 2023-04-17 20:55+0000\n"
"Last-Translator: Allan Nordhøy <epost@anotheragency.no>\n" "Last-Translator: Espen Sellevåg <buskmenn.drammer03@icloud.com>\n"
"Language-Team: Norwegian Bokmål <http://translate.tandoor.dev/projects/" "Language-Team: Norwegian Bokmål <http://translate.tandoor.dev/projects/"
"tandoor/recipes-backend/nb_NO/>\n" "tandoor/recipes-backend/nb_NO/>\n"
"Language: nb_NO\n" "Language: nb_NO\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5.3\n" "X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:91 #: .\cookbook\filters.py:23 .\cookbook\templates\base.html:91
#: .\cookbook\templates\forms\edit_internal_recipe.html:219 #: .\cookbook\templates\forms\edit_internal_recipe.html:219
@ -34,19 +34,23 @@ msgstr ""
#: .\cookbook\forms.py:46 #: .\cookbook\forms.py:46
msgid "Default Unit to be used when inserting a new ingredient into a recipe." 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 #: .\cookbook\forms.py:47
msgid "" msgid ""
"Enables support for fractions in ingredient amounts (e.g. convert decimals " "Enables support for fractions in ingredient amounts (e.g. convert decimals "
"to fractions automatically)" "to fractions automatically)"
msgstr "" msgstr ""
"Aktiverer støtte for deler av ingrediensmengde (konverterer feks. desimaler "
"til deler automatisk)"
#: .\cookbook\forms.py:48 #: .\cookbook\forms.py:48
msgid "" msgid ""
"Users with whom newly created meal plan/shopping list entries should be " "Users with whom newly created meal plan/shopping list entries should be "
"shared by default." "shared by default."
msgstr "" msgstr ""
"Brukere som oppretter nye måltidsplaner/handlelister, deler disse "
"oppføringene som standard."
#: .\cookbook\forms.py:49 #: .\cookbook\forms.py:49
msgid "Show recently viewed recipes on search page." msgid "Show recently viewed recipes on search page."
@ -58,7 +62,7 @@ msgstr "Antall desimaler ingredienser skal avrundes til."
#: .\cookbook\forms.py:51 #: .\cookbook\forms.py:51
msgid "If you want to be able to create and see comments underneath recipes." 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 #: .\cookbook\forms.py:53
msgid "" msgid ""
@ -67,6 +71,11 @@ msgid ""
"Useful when shopping with multiple people but might use a little bit of " "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." "mobile data. If lower than instance limit it is reset when saving."
msgstr "" 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 #: .\cookbook\forms.py:56
msgid "Makes the navbar stick to the top of the page." msgid "Makes the navbar stick to the top of the page."
@ -100,11 +109,11 @@ msgstr ""
#: .\cookbook\forms.py:97 .\cookbook\forms.py:317 #: .\cookbook\forms.py:97 .\cookbook\forms.py:317
msgid "Path" msgid "Path"
msgstr "" msgstr "Sti"
#: .\cookbook\forms.py:98 #: .\cookbook\forms.py:98
msgid "Storage UID" msgid "Storage UID"
msgstr "" msgstr "Lagring UID"
#: .\cookbook\forms.py:121 #: .\cookbook\forms.py:121
msgid "Default" msgid "Default"
@ -129,7 +138,6 @@ msgid "Old Unit"
msgstr "Gammel enhet" msgstr "Gammel enhet"
#: .\cookbook\forms.py:156 #: .\cookbook\forms.py:156
#, fuzzy
msgid "Unit that should be replaced." msgid "Unit that should be replaced."
msgstr "Enhet som skal erstattes." msgstr "Enhet som skal erstattes."
@ -204,12 +212,11 @@ msgstr ""
#: .\cookbook\views\views.py:112 .\cookbook\views\views.py:116 #: .\cookbook\views\views.py:112 .\cookbook\views\views.py:116
#: .\cookbook\views\views.py:184 #: .\cookbook\views\views.py:184
msgid "You do not have the required permissions to view this page!" 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 #: .\cookbook\helper\permission_helper.py:141
#, fuzzy
msgid "You are not logged in and therefore cannot view this page!" 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:145
#: .\cookbook\helper\permission_helper.py:167 #: .\cookbook\helper\permission_helper.py:167
@ -379,7 +386,7 @@ msgstr "Finner ikke siden du leter etter."
#: .\cookbook\templates\404.html:33 #: .\cookbook\templates\404.html:33
msgid "Take me Home" msgid "Take me Home"
msgstr "" msgstr "Tilbake til Startsiden"
#: .\cookbook\templates\404.html:35 #: .\cookbook\templates\404.html:35
msgid "Report a Bug" msgid "Report a Bug"
@ -388,12 +395,12 @@ msgstr "Rapporter en feil"
#: .\cookbook\templates\account\login.html:7 #: .\cookbook\templates\account\login.html:7
#: .\cookbook\templates\base.html:170 #: .\cookbook\templates\base.html:170
msgid "Login" msgid "Login"
msgstr "" msgstr "Logg inn"
#: .\cookbook\templates\account\login.html:13 #: .\cookbook\templates\account\login.html:13
#: .\cookbook\templates\account\login.html:28 #: .\cookbook\templates\account\login.html:28
msgid "Sign In" msgid "Sign In"
msgstr "" msgstr "Opprett bruker"
#: .\cookbook\templates\account\login.html:38 #: .\cookbook\templates\account\login.html:38
msgid "Social Login" msgid "Social Login"
@ -401,7 +408,7 @@ msgstr "Sosial innlogging"
#: .\cookbook\templates\account\login.html:39 #: .\cookbook\templates\account\login.html:39
msgid "You can use any of the following providers to sign in." 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:5
#: .\cookbook\templates\account\logout.html:9 #: .\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.html:5
#: .\cookbook\templates\account\password_reset_done.html:5 #: .\cookbook\templates\account\password_reset_done.html:5
msgid "Password Reset" msgid "Password Reset"
msgstr "" msgstr "Nullstill passord"
#: .\cookbook\templates\account\password_reset.html:9 #: .\cookbook\templates\account\password_reset.html:9
#: .\cookbook\templates\account\password_reset_done.html:9 #: .\cookbook\templates\account\password_reset_done.html:9
msgid "Password reset is not implemented for the time being!" 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 #: .\cookbook\templates\account\signup.html:5
msgid "Register" msgid "Register"
msgstr "" msgstr "Registrer"
#: .\cookbook\templates\account\signup.html:9 #: .\cookbook\templates\account\signup.html:9
msgid "Create your Account" msgid "Create your Account"
msgstr "Opprett din konto" msgstr "Opprett konto"
#: .\cookbook\templates\account\signup.html:14 #: .\cookbook\templates\account\signup.html:14
msgid "Create User" msgid "Create User"
@ -442,11 +449,11 @@ msgstr "API-dokumentasjon"
#: .\cookbook\templates\base.html:78 #: .\cookbook\templates\base.html:78
msgid "Utensils" msgid "Utensils"
msgstr "" msgstr "Redskaper"
#: .\cookbook\templates\base.html:88 #: .\cookbook\templates\base.html:88
msgid "Shopping" msgid "Shopping"
msgstr "" msgstr "Handle"
#: .\cookbook\templates\base.html:102 .\cookbook\views\delete.py:84 #: .\cookbook\templates\base.html:102 .\cookbook\views\delete.py:84
#: .\cookbook\views\edit.py:93 .\cookbook\views\lists.py:26 #: .\cookbook\views\edit.py:93 .\cookbook\views\lists.py:26
@ -456,27 +463,27 @@ msgstr "Nøkkelord"
#: .\cookbook\templates\base.html:104 #: .\cookbook\templates\base.html:104
msgid "Batch Edit" msgid "Batch Edit"
msgstr "" msgstr "Oppdatere flere"
#: .\cookbook\templates\base.html:109 #: .\cookbook\templates\base.html:109
msgid "Storage Data" msgid "Storage Data"
msgstr "" msgstr "Datalagring"
#: .\cookbook\templates\base.html:113 #: .\cookbook\templates\base.html:113
msgid "Storage Backends" msgid "Storage Backends"
msgstr "" msgstr "Lagringsplasser"
#: .\cookbook\templates\base.html:115 #: .\cookbook\templates\base.html:115
msgid "Configure Sync" msgid "Configure Sync"
msgstr "" msgstr "Konfigurer synkronisering"
#: .\cookbook\templates\base.html:117 #: .\cookbook\templates\base.html:117
msgid "Discovered Recipes" msgid "Discovered Recipes"
msgstr "" msgstr "Oppdagede oppskrifter"
#: .\cookbook\templates\base.html:119 #: .\cookbook\templates\base.html:119
msgid "Discovery Log" msgid "Discovery Log"
msgstr "" msgstr "Logg Oppdagelser"
#: .\cookbook\templates\base.html:121 .\cookbook\templates\stats.html:10 #: .\cookbook\templates\base.html:121 .\cookbook\templates\stats.html:10
msgid "Statistics" msgid "Statistics"
@ -484,7 +491,7 @@ msgstr "Statistikk"
#: .\cookbook\templates\base.html:123 #: .\cookbook\templates\base.html:123
msgid "Units & Ingredients" msgid "Units & Ingredients"
msgstr "" msgstr "Enheter & Ingredienser"
#: .\cookbook\templates\base.html:125 #: .\cookbook\templates\base.html:125
msgid "Import Recipe" msgid "Import Recipe"
@ -521,58 +528,61 @@ msgid "API Browser"
msgstr "API-utforsker" msgstr "API-utforsker"
#: .\cookbook\templates\base.html:165 #: .\cookbook\templates\base.html:165
#, fuzzy
msgid "Logout" msgid "Logout"
msgstr "Logg ut" msgstr "Logg ut"
#: .\cookbook\templates\batch\edit.html:6 #: .\cookbook\templates\batch\edit.html:6
msgid "Batch edit Category" msgid "Batch edit Category"
msgstr "" msgstr "Oppdater flere kategorier"
#: .\cookbook\templates\batch\edit.html:15 #: .\cookbook\templates\batch\edit.html:15
msgid "Batch edit Recipes" msgid "Batch edit Recipes"
msgstr "" msgstr "Oppdater flere oppskrifter"
#: .\cookbook\templates\batch\edit.html:20 #: .\cookbook\templates\batch\edit.html:20
msgid "Add the specified keywords to all recipes containing a word" 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 #: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:76
msgid "Sync" msgid "Sync"
msgstr "" msgstr "Synkronisering"
#: .\cookbook\templates\batch\monitor.html:10 #: .\cookbook\templates\batch\monitor.html:10
msgid "Manage watched Folders" msgid "Manage watched Folders"
msgstr "" msgstr "Behandle overvåkede mapper"
#: .\cookbook\templates\batch\monitor.html:14 #: .\cookbook\templates\batch\monitor.html:14
msgid "" msgid ""
"On this Page you can manage all storage folder locations that should be " "On this Page you can manage all storage folder locations that should be "
"monitored and synced." "monitored and synced."
msgstr "" msgstr ""
"Her kan du behandle alle lagringsmapper og plasseringer for monitorering og "
"synkronisering."
#: .\cookbook\templates\batch\monitor.html:16 #: .\cookbook\templates\batch\monitor.html:16
msgid "The path must be in the following format" 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 #: .\cookbook\templates\batch\monitor.html:27
msgid "Sync Now!" msgid "Sync Now!"
msgstr "" msgstr "Synkroniser nå!"
#: .\cookbook\templates\batch\waiting.html:4 #: .\cookbook\templates\batch\waiting.html:4
#: .\cookbook\templates\batch\waiting.html:10 #: .\cookbook\templates\batch\waiting.html:10
msgid "Importing Recipes" msgid "Importing Recipes"
msgstr "" msgstr "Importerer oppskrifter"
#: .\cookbook\templates\batch\waiting.html:23 #: .\cookbook\templates\batch\waiting.html:23
msgid "" msgid ""
"This can take a few minutes, depending on the number of recipes in sync, " "This can take a few minutes, depending on the number of recipes in sync, "
"please wait." "please wait."
msgstr "" 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 #: .\cookbook\templates\books.html:5 .\cookbook\templates\books.html:11
msgid "Recipe Books" msgid "Recipe Books"
msgstr "" msgstr "Oppskriftsbøker"
#: .\cookbook\templates\books.html:15 #: .\cookbook\templates\books.html:15
msgid "New Book" msgid "New Book"
@ -584,32 +594,32 @@ msgstr "av"
#: .\cookbook\templates\books.html:34 #: .\cookbook\templates\books.html:34
msgid "Toggle Recipes" msgid "Toggle Recipes"
msgstr "" msgstr "Veksle oppskrifter"
#: .\cookbook\templates\books.html:54 #: .\cookbook\templates\books.html:54
#: .\cookbook\templates\meal_plan_entry.html:48 #: .\cookbook\templates\meal_plan_entry.html:48
#: .\cookbook\templates\recipes_table.html:64 #: .\cookbook\templates\recipes_table.html:64
msgid "Last cooked" msgid "Last cooked"
msgstr "" msgstr "Forrige tilbereding"
#: .\cookbook\templates\books.html:71 #: .\cookbook\templates\books.html:71
msgid "There are no recipes in this book yet." 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 #: .\cookbook\templates\export.html:6 .\cookbook\templates\test2.html:6
msgid "Export Recipes" msgid "Export Recipes"
msgstr "" msgstr "Eksporter oppskrifter"
#: .\cookbook\templates\export.html:14 .\cookbook\templates\export.html:20 #: .\cookbook\templates\export.html:14 .\cookbook\templates\export.html:20
#: .\cookbook\templates\shopping_list.html:347 #: .\cookbook\templates\shopping_list.html:347
#: .\cookbook\templates\test2.html:14 .\cookbook\templates\test2.html:20 #: .\cookbook\templates\test2.html:14 .\cookbook\templates\test2.html:20
msgid "Export" msgid "Export"
msgstr "" msgstr "Eksporter"
#: .\cookbook\templates\forms\edit_import_recipe.html:5 #: .\cookbook\templates\forms\edit_import_recipe.html:5
#: .\cookbook\templates\forms\edit_import_recipe.html:9 #: .\cookbook\templates\forms\edit_import_recipe.html:9
msgid "Import new Recipe" msgid "Import new Recipe"
msgstr "" msgstr "Importer ny oppskrift"
#: .\cookbook\templates\forms\edit_import_recipe.html:14 #: .\cookbook\templates\forms\edit_import_recipe.html:14
#: .\cookbook\templates\forms\edit_internal_recipe.html:389 #: .\cookbook\templates\forms\edit_internal_recipe.html:389
@ -635,29 +645,29 @@ msgstr "Beskrivelse"
#: .\cookbook\templates\forms\edit_internal_recipe.html:72 #: .\cookbook\templates\forms\edit_internal_recipe.html:72
msgid "Waiting Time" msgid "Waiting Time"
msgstr "" msgstr "Ventetid"
#: .\cookbook\templates\forms\edit_internal_recipe.html:78 #: .\cookbook\templates\forms\edit_internal_recipe.html:78
msgid "Servings Text" msgid "Servings Text"
msgstr "" msgstr "Porsjon beskrivelse"
#: .\cookbook\templates\forms\edit_internal_recipe.html:89 #: .\cookbook\templates\forms\edit_internal_recipe.html:89
msgid "Select Keywords" msgid "Select Keywords"
msgstr "" msgstr "Velg nøkkelord"
#: .\cookbook\templates\forms\edit_internal_recipe.html:90 #: .\cookbook\templates\forms\edit_internal_recipe.html:90
#: .\cookbook\templates\url_import.html:212 #: .\cookbook\templates\url_import.html:212
msgid "Add Keyword" msgid "Add Keyword"
msgstr "" msgstr "Legg til nøkkelord"
#: .\cookbook\templates\forms\edit_internal_recipe.html:108 #: .\cookbook\templates\forms\edit_internal_recipe.html:108
msgid "Nutrition" msgid "Nutrition"
msgstr "" msgstr "Næringsinnhold"
#: .\cookbook\templates\forms\edit_internal_recipe.html:112 #: .\cookbook\templates\forms\edit_internal_recipe.html:112
#: .\cookbook\templates\forms\edit_internal_recipe.html:162 #: .\cookbook\templates\forms\edit_internal_recipe.html:162
msgid "Delete Step" msgid "Delete Step"
msgstr "" msgstr "Fjern trinn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:116 #: .\cookbook\templates\forms\edit_internal_recipe.html:116
msgid "Calories" msgid "Calories"
@ -678,15 +688,15 @@ msgstr "Proteiner"
#: .\cookbook\templates\forms\edit_internal_recipe.html:146 #: .\cookbook\templates\forms\edit_internal_recipe.html:146
#: .\cookbook\templates\forms\edit_internal_recipe.html:454 #: .\cookbook\templates\forms\edit_internal_recipe.html:454
msgid "Step" msgid "Step"
msgstr "" msgstr "Trinn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:167 #: .\cookbook\templates\forms\edit_internal_recipe.html:167
msgid "Show as header" msgid "Show as header"
msgstr "" msgstr "Vis som overskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:173 #: .\cookbook\templates\forms\edit_internal_recipe.html:173
msgid "Hide as header" msgid "Hide as header"
msgstr "" msgstr "Skjul overskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:178 #: .\cookbook\templates\forms\edit_internal_recipe.html:178
msgid "Move Up" msgid "Move Up"
@ -698,15 +708,15 @@ msgstr "Flytt nedover"
#: .\cookbook\templates\forms\edit_internal_recipe.html:192 #: .\cookbook\templates\forms\edit_internal_recipe.html:192
msgid "Step Name" msgid "Step Name"
msgstr "" msgstr "Trinn navn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:196 #: .\cookbook\templates\forms\edit_internal_recipe.html:196
msgid "Step Type" msgid "Step Type"
msgstr "" msgstr "Trinn type"
#: .\cookbook\templates\forms\edit_internal_recipe.html:207 #: .\cookbook\templates\forms\edit_internal_recipe.html:207
msgid "Step time in Minutes" msgid "Step time in Minutes"
msgstr "" msgstr "Trinn tid i minutter"
#: .\cookbook\templates\forms\edit_internal_recipe.html:261 #: .\cookbook\templates\forms\edit_internal_recipe.html:261
#: .\cookbook\templates\shopping_list.html:183 #: .\cookbook\templates\shopping_list.html:183
@ -740,7 +750,7 @@ msgstr "Velg mat"
#: .\cookbook\templates\meal_plan.html:256 #: .\cookbook\templates\meal_plan.html:256
#: .\cookbook\templates\url_import.html:171 #: .\cookbook\templates\url_import.html:171
msgid "Note" msgid "Note"
msgstr "" msgstr "Notis"
#: .\cookbook\templates\forms\edit_internal_recipe.html:319 #: .\cookbook\templates\forms\edit_internal_recipe.html:319
msgid "Delete Ingredient" msgid "Delete Ingredient"
@ -748,7 +758,7 @@ msgstr "Slett ingrediens"
#: .\cookbook\templates\forms\edit_internal_recipe.html:325 #: .\cookbook\templates\forms\edit_internal_recipe.html:325
msgid "Make Header" msgid "Make Header"
msgstr "" msgstr "Bruk som overskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:331 #: .\cookbook\templates\forms\edit_internal_recipe.html:331
msgid "Make Ingredient" msgid "Make Ingredient"
@ -756,15 +766,15 @@ msgstr "Opprett ingrediens"
#: .\cookbook\templates\forms\edit_internal_recipe.html:337 #: .\cookbook\templates\forms\edit_internal_recipe.html:337
msgid "Disable Amount" msgid "Disable Amount"
msgstr "" msgstr "Deaktiver mengde"
#: .\cookbook\templates\forms\edit_internal_recipe.html:343 #: .\cookbook\templates\forms\edit_internal_recipe.html:343
msgid "Enable Amount" msgid "Enable Amount"
msgstr "" msgstr "Aktiver mengde"
#: .\cookbook\templates\forms\edit_internal_recipe.html:348 #: .\cookbook\templates\forms\edit_internal_recipe.html:348
msgid "Copy Template Reference" msgid "Copy Template Reference"
msgstr "" msgstr "Kopier mal-referanse"
#: .\cookbook\templates\forms\edit_internal_recipe.html:374 #: .\cookbook\templates\forms\edit_internal_recipe.html:374
#: .\cookbook\templates\url_import.html:196 #: .\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:387
#: .\cookbook\templates\forms\edit_internal_recipe.html:418 #: .\cookbook\templates\forms\edit_internal_recipe.html:418
#, fuzzy
msgid "Save & View" msgid "Save & View"
msgstr "Lagre og vis" msgstr "Lagre og vis"
#: .\cookbook\templates\forms\edit_internal_recipe.html:391 #: .\cookbook\templates\forms\edit_internal_recipe.html:391
#: .\cookbook\templates\forms\edit_internal_recipe.html:424 #: .\cookbook\templates\forms\edit_internal_recipe.html:424
msgid "Add Step" msgid "Add Step"
msgstr "" msgstr "Legg til trinn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:394 #: .\cookbook\templates\forms\edit_internal_recipe.html:394
#: .\cookbook\templates\forms\edit_internal_recipe.html:428 #: .\cookbook\templates\forms\edit_internal_recipe.html:428
msgid "Add Nutrition" msgid "Add Nutrition"
msgstr "" msgstr "Legg til næringsinnhold"
#: .\cookbook\templates\forms\edit_internal_recipe.html:396 #: .\cookbook\templates\forms\edit_internal_recipe.html:396
#: .\cookbook\templates\forms\edit_internal_recipe.html:430 #: .\cookbook\templates\forms\edit_internal_recipe.html:430
msgid "Remove Nutrition" msgid "Remove Nutrition"
msgstr "" msgstr "Fjern næringsinnhold"
#: .\cookbook\templates\forms\edit_internal_recipe.html:398 #: .\cookbook\templates\forms\edit_internal_recipe.html:398
#: .\cookbook\templates\forms\edit_internal_recipe.html:433 #: .\cookbook\templates\forms\edit_internal_recipe.html:433
msgid "View Recipe" msgid "View Recipe"
msgstr "" msgstr "Vis oppskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:400 #: .\cookbook\templates\forms\edit_internal_recipe.html:400
#: .\cookbook\templates\forms\edit_internal_recipe.html:435 #: .\cookbook\templates\forms\edit_internal_recipe.html:435
@ -804,11 +813,11 @@ msgstr "Slett oppskrift"
#: .\cookbook\templates\forms\edit_internal_recipe.html:441 #: .\cookbook\templates\forms\edit_internal_recipe.html:441
msgid "Steps" msgid "Steps"
msgstr "" msgstr "Trinn"
#: .\cookbook\templates\forms\ingredients.html:15 #: .\cookbook\templates\forms\ingredients.html:15
msgid "Edit Ingredients" msgid "Edit Ingredients"
msgstr "" msgstr "Rediger ingrediens"
#: .\cookbook\templates\forms\ingredients.html:16 #: .\cookbook\templates\forms\ingredients.html:16
msgid "" msgid ""
@ -820,54 +829,61 @@ msgid ""
"them.\n" "them.\n"
" " " "
msgstr "" 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\forms\ingredients.html:24
#: .\cookbook\templates\stats.html:26 #: .\cookbook\templates\stats.html:26
msgid "Units" msgid "Units"
msgstr "" msgstr "Enheter"
#: .\cookbook\templates\forms\ingredients.html:26 #: .\cookbook\templates\forms\ingredients.html:26
msgid "Are you sure that you want to merge these two units?" 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:31
#: .\cookbook\templates\forms\ingredients.html:40 #: .\cookbook\templates\forms\ingredients.html:40
msgid "Merge" msgid "Merge"
msgstr "Flett" msgstr "Slå sammen"
#: .\cookbook\templates\forms\ingredients.html:36 #: .\cookbook\templates\forms\ingredients.html:36
msgid "Are you sure that you want to merge these two ingredients?" 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 #: .\cookbook\templates\generic\delete_template.html:18
#, python-format #, python-format
msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> " 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 #: .\cookbook\templates\generic\delete_template.html:21
msgid "Confirm" msgid "Confirm"
msgstr "" msgstr "Bekreft"
#: .\cookbook\templates\generic\edit_template.html:30 #: .\cookbook\templates\generic\edit_template.html:30
msgid "View" msgid "View"
msgstr "" msgstr "Vis"
#: .\cookbook\templates\generic\edit_template.html:34 #: .\cookbook\templates\generic\edit_template.html:34
msgid "Delete original file" msgid "Delete original file"
msgstr "" msgstr "Slett opprinnelig fil"
#: .\cookbook\templates\generic\list_template.html:6 #: .\cookbook\templates\generic\list_template.html:6
#: .\cookbook\templates\generic\list_template.html:12 #: .\cookbook\templates\generic\list_template.html:12
msgid "List" msgid "List"
msgstr "" msgstr "Liste"
#: .\cookbook\templates\generic\list_template.html:25 #: .\cookbook\templates\generic\list_template.html:25
msgid "Filter" msgid "Filter"
msgstr "" msgstr "Filtrer"
#: .\cookbook\templates\generic\list_template.html:30 #: .\cookbook\templates\generic\list_template.html:30
msgid "Import all" msgid "Import all"
msgstr "" msgstr "Importer alle"
#: .\cookbook\templates\generic\new_template.html:6 #: .\cookbook\templates\generic\new_template.html:6
#: .\cookbook\templates\generic\new_template.html:14 #: .\cookbook\templates\generic\new_template.html:14
@ -891,19 +907,19 @@ msgstr "Vis logg"
#: .\cookbook\templates\history.html:24 #: .\cookbook\templates\history.html:24
msgid "Cook Log" msgid "Cook Log"
msgstr "" msgstr "Tilberedingslogg"
#: .\cookbook\templates\import.html:6 .\cookbook\templates\test.html:6 #: .\cookbook\templates\import.html:6 .\cookbook\templates\test.html:6
msgid "Import Recipes" msgid "Import Recipes"
msgstr "" msgstr "Importer oppskrifter"
#: .\cookbook\templates\include\log_cooking.html:7 #: .\cookbook\templates\include\log_cooking.html:7
msgid "Log Recipe Cooking" msgid "Log Recipe Cooking"
msgstr "" msgstr "Loggfør tilberedt oppskrift"
#: .\cookbook\templates\include\log_cooking.html:13 #: .\cookbook\templates\include\log_cooking.html:13
msgid "All fields are optional and can be left empty." 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 #: .\cookbook\templates\include\log_cooking.html:19
msgid "Rating" msgid "Rating"
@ -943,44 +959,53 @@ msgid ""
"can be used.\n" "can be used.\n"
" " " "
msgstr "" 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 #: .\cookbook\templates\index.html:29
msgid "Search recipe ..." msgid "Search recipe ..."
msgstr "" msgstr "Søk etter oppskrift..."
#: .\cookbook\templates\index.html:44 #: .\cookbook\templates\index.html:44
msgid "New Recipe" msgid "New Recipe"
msgstr "" msgstr "Ny oppskrift"
#: .\cookbook\templates\index.html:47 #: .\cookbook\templates\index.html:47
msgid "Website Import" msgid "Website Import"
msgstr "" msgstr "Importer fra nettside"
#: .\cookbook\templates\index.html:53 #: .\cookbook\templates\index.html:53
msgid "Advanced Search" msgid "Advanced Search"
msgstr "" msgstr "Avansert søk"
#: .\cookbook\templates\index.html:57 #: .\cookbook\templates\index.html:57
msgid "Reset Search" msgid "Reset Search"
msgstr "" msgstr "Nullstill søk"
#: .\cookbook\templates\index.html:85 #: .\cookbook\templates\index.html:85
msgid "Last viewed" msgid "Last viewed"
msgstr "" msgstr "Sist sett"
#: .\cookbook\templates\index.html:87 .\cookbook\templates\meal_plan.html:178 #: .\cookbook\templates\index.html:87 .\cookbook\templates\meal_plan.html:178
#: .\cookbook\templates\stats.html:22 #: .\cookbook\templates\stats.html:22
msgid "Recipes" msgid "Recipes"
msgstr "" msgstr "Oppskrifter"
#: .\cookbook\templates\index.html:94 #: .\cookbook\templates\index.html:94
msgid "Log in to view recipes" msgid "Log in to view recipes"
msgstr "" msgstr "Logg inn for å se oppskrifter"
#: .\cookbook\templates\markdown_info.html:5 #: .\cookbook\templates\markdown_info.html:5
#: .\cookbook\templates\markdown_info.html:13 #: .\cookbook\templates\markdown_info.html:13
msgid "Markdown Info" msgid "Markdown Info"
msgstr "" msgstr "Markdown informasjon"
#: .\cookbook\templates\markdown_info.html:14 #: .\cookbook\templates\markdown_info.html:14
msgid "" msgid ""
@ -997,43 +1022,56 @@ msgid ""
"below.\n" "below.\n"
" " " "
msgstr "" 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 #: .\cookbook\templates\markdown_info.html:25
msgid "Headers" msgid "Headers"
msgstr "" msgstr "Overskrifter"
#: .\cookbook\templates\markdown_info.html:54 #: .\cookbook\templates\markdown_info.html:54
msgid "Formatting" msgid "Formatting"
msgstr "" msgstr "Formatering"
#: .\cookbook\templates\markdown_info.html:56 #: .\cookbook\templates\markdown_info.html:56
#: .\cookbook\templates\markdown_info.html:72 #: .\cookbook\templates\markdown_info.html:72
msgid "Line breaks are inserted by adding two spaces after the end of a line" msgid "Line breaks are inserted by adding two spaces after the end of a line"
msgstr "" 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:57
#: .\cookbook\templates\markdown_info.html:73 #: .\cookbook\templates\markdown_info.html:73
msgid "or by leaving a blank line inbetween." 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:59
#: .\cookbook\templates\markdown_info.html:74 #: .\cookbook\templates\markdown_info.html:74
msgid "This text is bold" msgid "This text is bold"
msgstr "" msgstr "Denne teksten er Fet"
#: .\cookbook\templates\markdown_info.html:60 #: .\cookbook\templates\markdown_info.html:60
#: .\cookbook\templates\markdown_info.html:75 #: .\cookbook\templates\markdown_info.html:75
msgid "This text is italic" msgid "This text is italic"
msgstr "" msgstr "Denne teksten er Kursiv"
#: .\cookbook\templates\markdown_info.html:61 #: .\cookbook\templates\markdown_info.html:61
#: .\cookbook\templates\markdown_info.html:77 #: .\cookbook\templates\markdown_info.html:77
msgid "Blockquotes are also possible" msgid "Blockquotes are also possible"
msgstr "" msgstr "Det er også mulig å sitere avsnitt"
#: .\cookbook\templates\markdown_info.html:84 #: .\cookbook\templates\markdown_info.html:84
msgid "Lists" msgid "Lists"
msgstr "" msgstr "Lister"
#: .\cookbook\templates\markdown_info.html:85 #: .\cookbook\templates\markdown_info.html:85
msgid "" msgid ""
@ -1264,7 +1302,7 @@ msgstr ""
#: .\cookbook\templates\no_groups_info.html:5 #: .\cookbook\templates\no_groups_info.html:5
#: .\cookbook\templates\no_groups_info.html:12 #: .\cookbook\templates\no_groups_info.html:12
msgid "No Permissions" msgid "No Permissions"
msgstr "Ingen tilganger." msgstr "Ingen tilgang"
#: .\cookbook\templates\no_groups_info.html:17 #: .\cookbook\templates\no_groups_info.html:17
msgid "You do not have any groups and therefor cannot use this application." msgid "You do not have any groups and therefor cannot use this application."
@ -1298,12 +1336,11 @@ msgstr ""
#: .\cookbook\templates\offline.html:6 #: .\cookbook\templates\offline.html:6
msgid "Offline" msgid "Offline"
msgstr "Frakoblet." msgstr "Frakoblet"
#: .\cookbook\templates\offline.html:19 #: .\cookbook\templates\offline.html:19
#, fuzzy
msgid "You are currently offline!" msgid "You are currently offline!"
msgstr "Du er ikke tilkoblet Internett." msgstr "Du er ikke tilkoblet!"
#: .\cookbook\templates\offline.html:20 #: .\cookbook\templates\offline.html:20
msgid "" msgid ""
@ -1366,7 +1403,7 @@ msgstr "Stil"
#: .\cookbook\templates\settings.html:79 #: .\cookbook\templates\settings.html:79
msgid "API Token" msgid "API Token"
msgstr "API-symbol" msgstr "API nøkkel"
#: .\cookbook\templates\settings.html:80 #: .\cookbook\templates\settings.html:80
msgid "" msgid ""
@ -1389,9 +1426,8 @@ msgid "Cookbook Setup"
msgstr "Kokeboksoppsett" msgstr "Kokeboksoppsett"
#: .\cookbook\templates\setup.html:14 #: .\cookbook\templates\setup.html:14
#, fuzzy
msgid "Setup" msgid "Setup"
msgstr "Sett opp" msgstr "Installering"
#: .\cookbook\templates\setup.html:15 #: .\cookbook\templates\setup.html:15
msgid "" msgid ""
@ -1424,11 +1460,11 @@ msgstr "Mengde"
#: .\cookbook\templates\shopping_list.html:226 #: .\cookbook\templates\shopping_list.html:226
msgid "Supermarket" msgid "Supermarket"
msgstr "Matbutikk" msgstr "Butikk"
#: .\cookbook\templates\shopping_list.html:236 #: .\cookbook\templates\shopping_list.html:236
msgid "Select Supermarket" msgid "Select Supermarket"
msgstr "Velg matbutikk" msgstr "Velg butikk"
#: .\cookbook\templates\shopping_list.html:260 #: .\cookbook\templates\shopping_list.html:260
msgid "Select User" msgid "Select User"
@ -1540,7 +1576,6 @@ msgstr ""
#: .\cookbook\templates\system.html:49 .\cookbook\templates\system.html:64 #: .\cookbook\templates\system.html:49 .\cookbook\templates\system.html:64
#: .\cookbook\templates\system.html:80 .\cookbook\templates\system.html:95 #: .\cookbook\templates\system.html:80 .\cookbook\templates\system.html:95
#, fuzzy
msgid "Ok" msgid "Ok"
msgstr "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" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-02-11 08:52+0100\n" "POT-Creation-Date: 2022-02-11 08:52+0100\n"
"PO-Revision-Date: 2023-02-18 10:55+0000\n" "PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: Joachim Weber <joachim.weber@gmx.de>\n" "Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Portuguese (Brazil) <http://translate.tandoor.dev/projects/" "Language-Team: Portuguese (Brazil) <http://translate.tandoor.dev/projects/"
"tandoor/recipes-backend/pt_BR/>\n" "tandoor/recipes-backend/pt_BR/>\n"
"Language: pt_BR\n" "Language: pt_BR\n"
@ -2208,7 +2208,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:38 #: .\cookbook\templates\url_import.html:38
msgid "URL" msgid "URL"
msgstr "" msgstr "URL"
#: .\cookbook\templates\url_import.html:40 #: .\cookbook\templates\url_import.html:40
msgid "App" 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" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-13 22:40+0200\n" "POT-Creation-Date: 2021-09-13 22:40+0200\n"
"PO-Revision-Date: 2022-11-30 19:09+0000\n" "PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: Alex <kovsharoff@gmail.com>\n" "Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Russian <http://translate.tandoor.dev/projects/tandoor/" "Language-Team: Russian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/ru/>\n" "recipes-backend/ru/>\n"
"Language: ru\n" "Language: ru\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.14.1\n" "X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125 #: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
#: .\cookbook\templates\forms\ingredients.html:34 #: .\cookbook\templates\forms\ingredients.html:34
@ -861,7 +861,7 @@ msgstr ""
#: .\cookbook\templates\base.html:220 #: .\cookbook\templates\base.html:220
msgid "GitHub" msgid "GitHub"
msgstr "" msgstr "GitHub"
#: .\cookbook\templates\base.html:224 #: .\cookbook\templates\base.html:224
msgid "API Browser" msgid "API Browser"
@ -1937,7 +1937,7 @@ msgstr ""
#: .\cookbook\templates\space.html:106 #: .\cookbook\templates\space.html:106
msgid "user" msgid "user"
msgstr "" msgstr "пользователь"
#: .\cookbook\templates\space.html:107 #: .\cookbook\templates\space.html:107
msgid "guest" msgid "guest"

View File

@ -8,17 +8,17 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-08 16:27+0100\n" "POT-Creation-Date: 2021-11-08 16:27+0100\n"
"PO-Revision-Date: 2022-02-02 15:31+0000\n" "PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: Mario Dvorsek <mario.dvorsek@gmail.com>\n" "Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Slovenian <http://translate.tandoor.dev/projects/tandoor/" "Language-Team: Slovenian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/sl/>\n" "recipes-backend/sl/>\n"
"Language: sl\n" "Language: sl\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n" "Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || "
"%100==4 ? 2 : 3;\n" "n%100==4 ? 2 : 3;\n"
"X-Generator: Weblate 4.10.1\n" "X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125 #: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
#: .\cookbook\templates\forms\ingredients.html:34 #: .\cookbook\templates\forms\ingredients.html:34
@ -2107,7 +2107,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:36 #: .\cookbook\templates\url_import.html:36
msgid "URL" msgid "URL"
msgstr "" msgstr "URL"
#: .\cookbook\templates\url_import.html:38 #: .\cookbook\templates\url_import.html:38
msgid "App" 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" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-29 18:42+0200\n" "POT-Creation-Date: 2022-04-29 18:42+0200\n"
"PO-Revision-Date: 2023-02-09 13:55+0000\n" "PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: vertilo <vertilo.dev@gmail.com>\n" "Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: Ukrainian <http://translate.tandoor.dev/projects/tandoor/" "Language-Team: Ukrainian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/uk/>\n" "recipes-backend/uk/>\n"
"Language: uk\n" "Language: uk\n"
@ -1091,7 +1091,7 @@ msgstr ""
#: .\cookbook\templates\base.html:311 #: .\cookbook\templates\base.html:311
msgid "GitHub" msgid "GitHub"
msgstr "" msgstr "GitHub"
#: .\cookbook\templates\base.html:313 #: .\cookbook\templates\base.html:313
msgid "Translate Tandoor" 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 django_scopes import scope, scopes_disabled
from pytest_factoryboy import LazyFixture, register 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, from cookbook.tests.factories import (FoodFactory, IngredientFactory, ShoppingListEntryFactory,
SupermarketCategoryFactory) SupermarketCategoryFactory)
@ -56,23 +56,32 @@ def obj_tree_1(request, space_1):
params = request.param # request.param is a magic variable params = request.param # request.param is a magic variable
except AttributeError: except AttributeError:
params = {} params = {}
objs = []
inherit = params.pop('inherit', False) 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 # set all foods to inherit everything
if inherit: if inherit:
inherit = Food.inheritable_fields 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: for i in inherit:
Through.objects.bulk_create([ Through.objects.bulk_create([
Through(food_id=x, foodinheritfield_id=i.id) Through(food_id=x, foodinheritfield_id=i.id)
for x in Food.objects.filter(space=space_1).values_list('id', flat=True) for x in Food.objects.filter(space=space_1).values_list('id', flat=True)
]) ])
objs[0].move(objs[1], node_location) Food.objects.get(id=child_id).move(
objs[1].move(objs[2], node_location) Food.objects.get(id=obj_id), 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=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", [ @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 obj_2.name in [x['name'] for x in response['results']]
assert response['results'][0]['name'] < response['results'][1]['name'] 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 assert len(response['results']) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content) response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
assert len(response['results']) == 1 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 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 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 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): def test_move(u1_s1, obj_tree_1, obj_2, obj_3, space_1):
with scope(space=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() parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
assert parent.get_num_children() == 1 assert parent.get_num_children() == 1
assert parent.get_descendant_count() == 2 assert parent.get_descendant_count() == 2
assert Food.get_root_nodes().filter(space=space_1).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): def test_move_errors(u1_s1, obj_tree_1, obj_3, space_1):
with scope(space=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() parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
# move child to root # move child to root
r = u1_s1.put(reverse(MOVE_URL, args=[obj_tree_1.id, 0])) r = u1_s1.put(reverse(MOVE_URL, args=[obj_tree_1.id, 0]))
assert r.status_code == 200 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): def test_merge(u1_s1, obj_tree_1, obj_1, obj_3, space_1):
with scope(space=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() parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0] child = obj_tree_1.get_descendants()[0]
assert parent.get_num_children() == 1 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): def test_merge_errors(u1_s1, obj_tree_1, obj_3, space_1):
with scope(space=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() parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
# attempt to merge with non-existent parent # attempt to merge with non-existent parent
r = u1_s1.put( 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): def test_root_filter(obj_tree_1, obj_2, obj_3, u1_s1):
with scope(space=obj_tree_1.space): 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() 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 # 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) response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root=0').content)
assert len(response['results']) == 2 assert len(response['results']) == 2
# django_tree bypasses ORM - best to retrieve all changed objects
with scopes_disabled(): with scopes_disabled():
obj_2.move(parent, node_location) 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 # 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 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 assert response['count'] == 2
def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1): def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1):
with scope(space=obj_tree_1.space): 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() parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
obj_2.move(parent, node_location) 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 # 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 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 assert response['count'] == 4
# This is more about the model than the API - should this be moved to a different test? # 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", [ @pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [
({'has_category': True, 'inherit': True}, 'supermarket_category', True, 'cat_1'), ({'has_category': True, 'inherit': True},
({'has_category': True, 'inherit': False}, 'supermarket_category', False, 'cat_1'), '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': True}, 'ignore_shopping', True, 'false'),
({'ignore_shopping': True, 'inherit': False}, 'ignore_shopping', False, 'false'), ({'ignore_shopping': True, 'inherit': False},
({'substitute_children': True, 'inherit': True}, 'substitute_children', True, 'false'), 'ignore_shopping', False, 'false'),
({'substitute_children': True, 'inherit': False}, 'substitute_children', False, 'false'), ({'substitute_children': True, 'inherit': True},
({'substitute_siblings': True, 'inherit': True}, 'substitute_siblings', True, 'false'), 'substitute_children', True, 'false'),
({'substitute_siblings': True, 'inherit': False}, 'substitute_siblings', False, '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 ], 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): def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
with scope(space=obj_tree_1.space): 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) new_val = request.getfixturevalue(new_val)
# if this test passes it demonstrates that inheritance works # if this test passes it demonstrates that inheritance works
# when moving to a parent as each food is created with a different category # 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(parent, field) == getattr(
assert (getattr(obj_tree_1, field) == getattr(child, field)) in [inherit, True] 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 # change parent to a new value
setattr(parent, field, new_val) setattr(parent, field, new_val)
with scope(space=parent.space): 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", [ @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']) ], indirect=['obj_tree_1'])
@pytest.mark.parametrize("global_reset", [True, False]) @pytest.mark.parametrize("global_reset", [True, False])
@pytest.mark.parametrize("field", ['ignore_shopping', 'substitute_children', 'substitute_siblings', 'supermarket_category']) @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) assert getattr(parent, field) != getattr(obj_tree_1, field)
if global_reset: 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) parent.reset_inheritance(space=space_1)
else: 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() obj_tree_1.save()
parent.reset_inheritance(space=space_1, food=obj_tree_1) parent.reset_inheritance(space=space_1, food=obj_tree_1)
# djangotree bypasses ORM and need to be retrieved again # 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) parent = Food.objects.get(id=parent.id)
child = Food.objects.get(id=child.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) assert getattr(obj_tree_1, field) == getattr(child, field)
@pytest.mark.parametrize("obj_tree_1", [ @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']) ], indirect=['obj_tree_1'])
@pytest.mark.parametrize("field", ['ignore_shopping', 'substitute_children', 'substitute_siblings', 'supermarket_category']) @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): 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() parent = obj_tree_1.get_parent()
Food.objects.all().delete() 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) parent.reset_inheritance(space=space_1)
def test_onhand(obj_1, u1_s1, u2_s1): 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(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False 'food_onhand'] == False
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
'food_onhand'] == False
u1_s1.patch( u1_s1.patch(
reverse( reverse(
@ -574,10 +623,13 @@ def test_onhand(obj_1, u1_s1, u2_s1):
{'food_onhand': True}, {'food_onhand': True},
content_type='application/json' content_type='application/json'
) )
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False '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) user1 = auth.get_user(u1_s1)
user2 = auth.get_user(u2_s1) user2 = auth.get_user(u2_s1)
user1.userpreference.shopping_share.add(user2) 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 import json
from datetime import timedelta
import factory
import pytest import pytest
# work around for bug described here https://stackoverflow.com/a/70312265/15762829 # work around for bug described here https://stackoverflow.com/a/70312265/15762829
from django.conf import settings from django.conf import settings
from django.contrib import auth from django.contrib import auth
from django.forms import model_to_dict
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django_scopes import scopes_disabled
from django_scopes import scope, scopes_disabled
from pytest_factoryboy import LazyFixture, register
from cookbook.models import Food, Ingredient, ShoppingListEntry, Step from cookbook.models import Food, Ingredient
from cookbook.tests.factories import (IngredientFactory, MealPlanFactory, RecipeFactory, from cookbook.tests.factories import MealPlanFactory, RecipeFactory, StepFactory, UserFactory
StepFactory, UserFactory)
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']: 'django.db.backends.postgresql']:
@ -32,9 +26,12 @@ def user2(request, u1_s1):
except AttributeError: except AttributeError:
params = {} params = {}
user = auth.get_user(u1_s1) user = auth.get_user(u1_s1)
user.userpreference.mealplan_autoadd_shopping = params.get('mealplan_autoadd_shopping', True) user.userpreference.mealplan_autoadd_shopping = params.get(
user.userpreference.mealplan_autoinclude_related = params.get('mealplan_autoinclude_related', True) 'mealplan_autoadd_shopping', True)
user.userpreference.mealplan_autoexclude_onhand = params.get('mealplan_autoexclude_onhand', 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() user.userpreference.save()
return u1_s1 return u1_s1
@ -50,7 +47,6 @@ def recipe(request, space_1, u1_s1):
return RecipeFactory(**params) return RecipeFactory(**params)
@pytest.mark.parametrize("arg", [ @pytest.mark.parametrize("arg", [
['g1_s1', 204], ['g1_s1', 204],
['u1_s1', 204], ['u1_s1', 204],
@ -59,9 +55,12 @@ def recipe(request, space_1, u1_s1):
]) ])
@pytest.mark.parametrize("recipe, sle_count", [ @pytest.mark.parametrize("recipe, sle_count", [
({}, 10), ({}, 10),
({'steps__recipe_count': 1}, 20), # shopping list from recipe with StepRecipe # shopping list from recipe with StepRecipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19), # shopping list from recipe with food recipe ({'steps__recipe_count': 1}, 20),
({'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 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']) ], indirect=['recipe'])
def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1): def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1):
c = request.getfixturevalue(arg[0]) 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 if r.status_code == 204: # skip anonymous user
r = json.loads(c.get(reverse(SHOPPING_LIST_URL)).content) 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 assert [x['created_by']['id'] for x in r].count(user.id) == sle_count
# user in space can't see shopping list # 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)) user.userpreference.shopping_share.add(auth.get_user(u2_s1))
# after share, user in space can see shopping list # 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 # confirm that the author of the recipe doesn't have access to shopping list
if c != u1_s1: 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) r = c.get(url)
assert r.status_code == 405 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", [ @pytest.mark.parametrize("recipe, sle_count", [
({}, 10), ({}, 10),
({'steps__recipe_count': 1}, 20), # shopping list from recipe with StepRecipe # shopping list from recipe with StepRecipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19), # shopping list from recipe with food recipe ({'steps__recipe_count': 1}, 20),
({'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 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']) ], indirect=['recipe'])
@pytest.mark.parametrize("use_mealplan", [(False), (True), ]) @pytest.mark.parametrize("use_mealplan", [(False), (True), ])
def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u2_s1): 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() user.userpreference.save()
if use_mealplan: 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: else:
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id})) u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}))
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content) 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 assert [x['created_by']['id'] for x in r].count(user.id) == sle_count
all_ing = [x['ingredient'] for x in r] all_ing = [x['ingredient'] for x in r]
keep_ing = all_ing[1:-1] # remove first and last element 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'] list_recipe = r[0]['list_recipe']
amount_sum = sum([x['amount'] for x in r]) amount_sum = sum([x['amount'] for x in r])
# test modifying shopping list as different user # test modifying shopping list as different user
# test increasing servings size of recipe shopping list # test increasing servings size of recipe shopping list
if use_mealplan: if use_mealplan:
mealplan.servings = 2*recipe.servings mealplan.servings = 2 * recipe.servings
mealplan.save() mealplan.save()
else: else:
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}), 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' content_type='application/json'
) )
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content) r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert sum([x['amount'] for x in r]) == amount_sum * 2 assert sum([x['amount'] for x in r]) == amount_sum * 2
assert len(r) == sle_count 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 # testing decreasing servings size of recipe shopping list
if use_mealplan: 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) r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert sum([x['amount'] for x in r]) == amount_sum * .5 assert sum([x['amount'] for x in r]) == amount_sum * .5
assert len(r) == sle_count 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 # test removing 3 items from shopping list
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}), 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) r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert len(r) == sle_count - 3 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 # add all ingredients to existing shopping list - don't change serving size
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}), 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) r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert sum([x['amount'] for x in r]) == amount_sum * .5 assert sum([x['amount'] for x in r]) == amount_sum * .5
assert len(r) == sle_count 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", [ @pytest.mark.parametrize("user2, sle_count", [
({'mealplan_autoadd_shopping': False}, (0, 18)), ({'mealplan_autoadd_shopping': False}, (0, 18)),
({'mealplan_autoinclude_related': False}, (9, 9)), ({'mealplan_autoinclude_related': False}, (9, 9)),
({'mealplan_autoexclude_onhand': False}, (20, 20)), ({'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']) ], indirect=['user2'])
@pytest.mark.parametrize("use_mealplan", [(False), (True), ]) @pytest.mark.parametrize("use_mealplan", [(False), (True), ])
@pytest.mark.parametrize("recipe", [({'steps__recipe_count': 1})], indirect=['recipe']) @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 = Food.objects.get(id=ingredients[2].food.id)
food.onhand_users.add(user) food.onhand_users.add(user)
food.save() 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 = Food.objects.get(id=food.id)
food.onhand_users.add(user) food.onhand_users.add(user)
food.save() food.save()
if use_mealplan: if use_mealplan:
mealplan = MealPlanFactory(space=recipe.space, created_by=user, servings=recipe.servings, recipe=recipe) MealPlanFactory(
assert len(json.loads(user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[0] 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: else:
user2.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id})) 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(): with scopes_disabled():
user1 = auth.get_user(u1_s1) user1 = auth.get_user(u1_s1)
user2 = auth.get_user(u2_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) recipe1 = RecipeFactory(created_by=user1, space=space)
recipe2 = RecipeFactory(created_by=user2, space=space) recipe2 = RecipeFactory(created_by=user2, space=space)
recipe3 = RecipeFactory(created_by=user3, 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.recipe = recipe2
food.save() 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() recipe1.save()
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe1.id})) 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(
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0 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']) @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)) # recipe.step_set.first().ingredient_set.add(IngredientFactory(ingredients__header=1))
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id})) 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(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 import pytest
from django.contrib import auth from django.contrib import auth
from django.contrib.auth.models import Group, User
from django_scopes import scopes_disabled 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.models import Food, Ingredient, Recipe, Step, Unit
from cookbook.tests.factories import FoodFactory, SpaceFactory, UserFactory from cookbook.tests.factories import SpaceFactory, UserFactory
register(SpaceFactory, 'space_1') register(SpaceFactory, 'space_1')
register(SpaceFactory, 'space_2') register(SpaceFactory, 'space_2')
@ -60,8 +59,10 @@ def get_random_recipe(space_1, u1_s1):
internal=True, internal=True,
) )
s1 = Step.objects.create(name=str(uuid.uuid4()), instruction=str(uuid.uuid4()), space=space_1, ) s1 = Step.objects.create(name=str(uuid.uuid4()),
s2 = Step.objects.create(name=str(uuid.uuid4()), instruction=str(uuid.uuid4()), space=space_1, ) 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(s1)
r.steps.add(s2) r.steps.add(s2)
@ -70,8 +71,10 @@ def get_random_recipe(space_1, u1_s1):
s1.ingredients.add( s1.ingredients.add(
Ingredient.objects.create( Ingredient.objects.create(
amount=1, amount=1,
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0], food=Food.objects.get_or_create(
unit=Unit.objects.create(name=str(uuid.uuid4()), space=space_1, ), name=str(uuid.uuid4()), space=space_1)[0],
unit=Unit.objects.create(
name=str(uuid.uuid4()), space=space_1, ),
note=str(uuid.uuid4()), note=str(uuid.uuid4()),
space=space_1, space=space_1,
) )
@ -80,8 +83,10 @@ def get_random_recipe(space_1, u1_s1):
s2.ingredients.add( s2.ingredients.add(
Ingredient.objects.create( Ingredient.objects.create(
amount=1, amount=1,
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0], food=Food.objects.get_or_create(
unit=Unit.objects.create(name=str(uuid.uuid4()), space=space_1, ), name=str(uuid.uuid4()), space=space_1)[0],
unit=Unit.objects.create(
name=str(uuid.uuid4()), space=space_1, ),
note=str(uuid.uuid4()), note=str(uuid.uuid4()),
space=space_1, space=space_1,
) )
@ -99,8 +104,10 @@ def get_random_json_recipe():
{ {
"instruction": str(uuid.uuid4()), "instruction": str(uuid.uuid4()),
"ingredients": [ "ingredients": [
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(uuid.uuid4())}, "amount": random.randint(0, 10)}, {"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(
{"food": {"name": str(uuid.uuid4())}, "unit": {"name": str(uuid.uuid4())}, "amount": random.randint(0, 10)}, 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 key in expected_lists:
for k in expected_lists[key]: for k in expected_lists[key]:
try: 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]]) assert any([dict_compare(k, i) for i in target_lists[key]])
except AssertionError: except AssertionError:
for result in [dict_compare(k, i, details=True) for i in target_lists[key]]: 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 added = d1_keys - d2_keys
removed = d2_keys - d1_keys removed = d2_keys - d1_keys
modified = {o: (d1[o], d2[o]) for o in not_dicts if d1[o] != d2[o]} 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: if details:
return added, removed, modified, modified_dicts return added, removed, modified, modified_dicts
else: else:
@ -173,12 +182,12 @@ def transpose(text, number=2):
positions = random.sample(range(len(tokens[token_pos])), number) positions = random.sample(range(len(tokens[token_pos])), number)
# swap the positions # swap the positions
l = list(tokens[token_pos]) lt = list(tokens[token_pos])
for first, second in zip(positions[::2], positions[1::2]): 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 # replace original tokens with swapped
tokens[token_pos] = ''.join(l) tokens[token_pos] = ''.join(lt)
# return text with the swapped token # return text with the swapped token
return ' '.join(tokens) return ' '.join(tokens)

View File

@ -4,13 +4,12 @@ from decimal import Decimal
import factory import factory
import pytest import pytest
from django.contrib import auth
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from faker import Factory as FakerFactory from faker import Factory as FakerFactory
from pytest_factoryboy import register 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 # this code will run immediately prior to creating the model object useful when you want a reverse relationship
# log = factory.RelatedFactory( # log = factory.RelatedFactory(
@ -53,7 +52,8 @@ class SpaceFactory(factory.django.DjangoModelFactory):
class UserFactory(factory.django.DjangoModelFactory): class UserFactory(factory.django.DjangoModelFactory):
"""User factory.""" """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()) first_name = factory.LazyAttribute(lambda x: faker.first_name())
last_name = factory.LazyAttribute(lambda x: faker.last_name()) last_name = factory.LazyAttribute(lambda x: faker.last_name())
email = factory.LazyAttribute(lambda x: faker.email()) email = factory.LazyAttribute(lambda x: faker.email())
@ -65,7 +65,8 @@ class UserFactory(factory.django.DjangoModelFactory):
return return
if extracted: 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)) us.groups.add(Group.objects.get(name=extracted))
@factory.post_generation @factory.post_generation
@ -75,10 +76,12 @@ class UserFactory(factory.django.DjangoModelFactory):
if extracted: if extracted:
for prefs in 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: class Meta:
model = User model = User
django_get_or_create = ('username', 'space',)
@register @register
@ -98,18 +101,22 @@ class SupermarketCategoryFactory(factory.django.DjangoModelFactory):
class FoodFactory(factory.django.DjangoModelFactory): class FoodFactory(factory.django.DjangoModelFactory):
"""Food factory.""" """Food factory."""
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10)[:128]) 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)) description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
supermarket_category = factory.Maybe( supermarket_category = factory.Maybe(
factory.LazyAttribute(lambda x: x.has_category), 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 no_declaration=None
) )
recipe = factory.Maybe( recipe = factory.Maybe(
factory.LazyAttribute(lambda x: x.has_recipe), 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 no_declaration=None
) )
path = None
space = factory.SubFactory(SpaceFactory) space = factory.SubFactory(SpaceFactory)
@factory.post_generation @factory.post_generation
@ -127,17 +134,19 @@ class FoodFactory(factory.django.DjangoModelFactory):
class Meta: class Meta:
model = 'cookbook.Food' model = 'cookbook.Food'
django_get_or_create = ('name', 'plural_name', 'space',) django_get_or_create = ('name', 'plural_name', 'path', 'space',)
@register @register
class RecipeBookFactory(factory.django.DjangoModelFactory): class RecipeBookFactory(factory.django.DjangoModelFactory):
"""RecipeBook factory.""" """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)) description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
icon = None icon = None
# shared = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space')) # 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 filter = None
space = factory.SubFactory(SpaceFactory) space = factory.SubFactory(SpaceFactory)
@ -149,7 +158,8 @@ class RecipeBookFactory(factory.django.DjangoModelFactory):
@register @register
class RecipeBookEntryFactory(factory.django.DjangoModelFactory): class RecipeBookEntryFactory(factory.django.DjangoModelFactory):
"""RecipeBookEntry factory.""" """RecipeBookEntry factory."""
book = factory.SubFactory(RecipeBookFactory, space=factory.SelfAttribute('..recipe.space')) book = factory.SubFactory(
RecipeBookFactory, space=factory.SelfAttribute('..recipe.space'))
recipe = None recipe = None
class Meta: class Meta:
@ -173,7 +183,8 @@ class UnitFactory(factory.django.DjangoModelFactory):
@register @register
class KeywordFactory(factory.django.DjangoModelFactory): class KeywordFactory(factory.django.DjangoModelFactory):
"""Keyword factory.""" """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) # icon = models.CharField(max_length=16, blank=True, null=True)
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10)) description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
space = factory.SubFactory(SpaceFactory) space = factory.SubFactory(SpaceFactory)
@ -184,15 +195,17 @@ class KeywordFactory(factory.django.DjangoModelFactory):
class Meta: class Meta:
model = 'cookbook.Keyword' model = 'cookbook.Keyword'
django_get_or_create = ('name', 'space',) django_get_or_create = ('name', 'space')
exclude = ('num') exclude = ('num')
@register @register
class IngredientFactory(factory.django.DjangoModelFactory): class IngredientFactory(factory.django.DjangoModelFactory):
"""Ingredient factory.""" """Ingredient factory."""
food = factory.SubFactory(FoodFactory, space=factory.SelfAttribute('..space')) food = factory.SubFactory(
unit = factory.SubFactory(UnitFactory, space=factory.SelfAttribute('..space')) 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)) amount = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=10))
note = factory.LazyAttribute(lambda x: faker.sentence(nb_words=8)) note = factory.LazyAttribute(lambda x: faker.sentence(nb_words=8))
is_header = False is_header = False
@ -210,7 +223,8 @@ class MealTypeFactory(factory.django.DjangoModelFactory):
# icon = # icon =
color = factory.LazyAttribute(lambda x: faker.safe_hex_color()) color = factory.LazyAttribute(lambda x: faker.safe_hex_color())
default = False default = False
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space')) created_by = factory.SubFactory(
UserFactory, space=factory.SelfAttribute('..space'))
space = factory.SubFactory(SpaceFactory) space = factory.SubFactory(SpaceFactory)
class Meta: class Meta:
@ -221,13 +235,17 @@ class MealTypeFactory(factory.django.DjangoModelFactory):
class MealPlanFactory(factory.django.DjangoModelFactory): class MealPlanFactory(factory.django.DjangoModelFactory):
recipe = factory.Maybe( recipe = factory.Maybe(
factory.LazyAttribute(lambda x: x.has_recipe), 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 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)) title = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5))
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space')) created_by = factory.SubFactory(
meal_type = factory.SubFactory(MealTypeFactory, space=factory.SelfAttribute('..space')) UserFactory, space=factory.SelfAttribute('..space'))
meal_type = factory.SubFactory(
MealTypeFactory, space=factory.SelfAttribute('..space'))
note = factory.LazyAttribute(lambda x: faker.paragraph()) note = factory.LazyAttribute(lambda x: faker.paragraph())
date = factory.LazyAttribute(lambda x: faker.future_date()) date = factory.LazyAttribute(lambda x: faker.future_date())
space = factory.SubFactory(SpaceFactory) space = factory.SubFactory(SpaceFactory)
@ -244,11 +262,13 @@ class ShoppingListRecipeFactory(factory.django.DjangoModelFactory):
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5)) name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5))
recipe = factory.Maybe( recipe = factory.Maybe(
factory.LazyAttribute(lambda x: x.has_recipe), 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 no_declaration=None
) )
servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=10)) 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) space = factory.SubFactory(SpaceFactory)
class Params: class Params:
@ -264,25 +284,32 @@ class ShoppingListEntryFactory(factory.django.DjangoModelFactory):
list_recipe = factory.Maybe( list_recipe = factory.Maybe(
factory.LazyAttribute(lambda x: x.has_mealplan), 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 no_declaration=None
) )
food = factory.SubFactory(FoodFactory, space=factory.SelfAttribute('..space')) food = factory.SubFactory(
unit = factory.SubFactory(UnitFactory, space=factory.SelfAttribute('..space')) FoodFactory, space=factory.SelfAttribute('..space'))
unit = factory.SubFactory(
UnitFactory, space=factory.SelfAttribute('..space'))
# # ingredient = factory.SubFactory(IngredientFactory) # # 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) order = factory.Sequence(int)
checked = False 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()) created_at = factory.LazyAttribute(lambda x: faker.past_date())
completed_at = None completed_at = None
delay_until = None delay_until = None
space = factory.SubFactory(SpaceFactory) space = factory.SubFactory(SpaceFactory)
@classmethod @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) 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: if created_at is not None:
obj.created_at = created_at obj.created_at = created_at
obj.save() obj.save()
@ -298,7 +325,8 @@ class ShoppingListEntryFactory(factory.django.DjangoModelFactory):
@register @register
class StepFactory(factory.django.DjangoModelFactory): class StepFactory(factory.django.DjangoModelFactory):
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=5)) 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 # 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__count = 10 # default number of ingredients to add
ingredients__header = 0 ingredients__header = 0
@ -330,14 +358,16 @@ class StepFactory(factory.django.DjangoModelFactory):
for i in range(num_ing): for i in range(num_ing):
if num_food_recipe > 0: if num_food_recipe > 0:
has_recipe = True has_recipe = True
num_food_recipe = num_food_recipe-1 num_food_recipe = num_food_recipe - 1
else: else:
has_recipe = False 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) num_header = kwargs.get('header', 0)
if num_header > 0: if num_header > 0:
for i in range(num_header): 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: elif extracted:
for ing in extracted: for ing in extracted:
self.ingredients.add(ing) self.ingredients.add(ing)
@ -351,20 +381,27 @@ class RecipeFactory(factory.django.DjangoModelFactory):
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=7)) name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=7))
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10)) description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=20)) 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 keywords__count = 5 # default number of keywords to generate
steps__count = 1 # default number of steps to create steps__count = 1 # default number of steps to create
steps__recipe_count = 0 # default number of step recipes 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}} # 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)) steps__food_recipe_count = {}
waiting_time = factory.LazyAttribute(lambda x: faker.random_int(min=0, max=360)) 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 internal = False
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space')) created_by = factory.SubFactory(
created_at = factory.LazyAttribute(lambda x: faker.date_between_dates(date_start=date(2000, 1, 1), date_end=date(2020, 12, 31))) 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) space = factory.SubFactory(SpaceFactory)
@classmethod @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) created_at = kwargs.pop('created_at', None)
# updated_at = kwargs.pop('updated_at', None) # updated_at = kwargs.pop('updated_at', None)
obj = super(RecipeFactory, cls)._create(target_class, *args, **kwargs) obj = super(RecipeFactory, cls)._create(target_class, *args, **kwargs)
@ -401,11 +438,13 @@ class RecipeFactory(factory.django.DjangoModelFactory):
ing_recipe_count = 0 ing_recipe_count = 0
if food_recipe_count.get('step', None) == i: if food_recipe_count.get('step', None) == i:
ing_recipe_count = food_recipe_count.get('count', 0) 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)) self.steps.add(StepFactory(
num_ing_headers+-1 space=self.space, ingredients__food_recipe_count=ing_recipe_count, ingredients__header=num_ing_headers))
num_ing_headers + - 1
if num_recipe_steps > 0: if num_recipe_steps > 0:
for j in range(num_recipe_steps): 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): if extracted and (num_steps + num_recipe_steps == 0):
for step in extracted: for step in extracted:
self.steps.add(step) self.steps.add(step)
@ -428,15 +467,18 @@ class RecipeFactory(factory.django.DjangoModelFactory):
@register @register
class CookLogFactory(factory.django.DjangoModelFactory): class CookLogFactory(factory.django.DjangoModelFactory):
"""CookLog factory.""" """CookLog factory."""
recipe = factory.SubFactory(RecipeFactory, space=factory.SelfAttribute('..space')) recipe = factory.SubFactory(
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space')) RecipeFactory, space=factory.SelfAttribute('..space'))
created_by = factory.SubFactory(
UserFactory, space=factory.SelfAttribute('..space'))
created_at = factory.LazyAttribute(lambda x: faker.date_this_decade()) created_at = factory.LazyAttribute(lambda x: faker.date_this_decade())
rating = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=5)) rating = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=5))
servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=32)) servings = factory.LazyAttribute(lambda x: faker.random_int(min=1, max=32))
space = factory.SubFactory(SpaceFactory) space = factory.SubFactory(SpaceFactory)
@classmethod @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) created_at = kwargs.pop('created_at', None)
obj = super(CookLogFactory, cls)._create(target_class, *args, **kwargs) obj = super(CookLogFactory, cls)._create(target_class, *args, **kwargs)
if created_at is not None: if created_at is not None:
@ -451,13 +493,17 @@ class CookLogFactory(factory.django.DjangoModelFactory):
@register @register
class ViewLogFactory(factory.django.DjangoModelFactory): class ViewLogFactory(factory.django.DjangoModelFactory):
"""ViewLog factory.""" """ViewLog factory."""
recipe = factory.SubFactory(RecipeFactory, space=factory.SelfAttribute('..space')) recipe = factory.SubFactory(
created_by = factory.SubFactory(UserFactory, space=factory.SelfAttribute('..space')) RecipeFactory, space=factory.SelfAttribute('..space'))
created_at = factory.LazyAttribute(lambda x: faker.past_datetime(start_date='-365d')) 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) space = factory.SubFactory(SpaceFactory)
@classmethod @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) created_at = kwargs.pop('created_at', None)
obj = super(ViewLogFactory, cls)._create(target_class, *args, **kwargs) obj = super(ViewLogFactory, cls)._create(target_class, *args, **kwargs)
if created_at is not None: 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 child substitute
# TODO returns recipes with all ingredients via sibling 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 @pytest.fixture
def recipes(space_1): def recipes(space_1):
@ -19,7 +24,8 @@ def recipes(space_1):
@pytest.fixture @pytest.fixture
def makenow_recipe(request, space_1): 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) recipe = RecipeFactory.create(space=space_1)
for food in Food.objects.filter(ingredient__step__recipe=recipe.id): 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})() request = type('', (object,), {'space': space_1, 'user': user1})()
search = RecipeSearch(request, makenow='true') search = RecipeSearch(request, makenow='true')
with scope(space=space_1): 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() food.onhand_users.clear()
assert search.get_queryset(Recipe.objects.all()).count() == 0 assert search.get_queryset(Recipe.objects.all()).count() == 0
food.ignore_shopping = True food.ignore_shopping = True
food.save() food.save()
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9 assert Food.objects.filter(
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, ignore_shopping=True).count() == 1 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()) search = search.get_queryset(Recipe.objects.all())
assert search.count() == 1 assert search.count() == 1
assert search.first().id == makenow_recipe.id 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})() request = type('', (object,), {'space': space_1, 'user': user1})()
search = RecipeSearch(request, makenow='true') search = RecipeSearch(request, makenow='true')
with scope(space=space_1): 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() onhand_user = food.onhand_users.first()
food.onhand_users.clear() food.onhand_users.clear()
assert search.get_queryset(Recipe.objects.all()).count() == 0 assert search.get_queryset(Recipe.objects.all()).count() == 0
food.substitute.add(FoodFactory.create(space=space_1, onhand_users=[onhand_user])) food.substitute.add(FoodFactory.create(
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9 space=space_1, onhand_users=[onhand_user]))
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, substitute__isnull=False).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, substitute__isnull=False).count() == 1
search = search.get_queryset(Recipe.objects.all()) search = search.get_queryset(Recipe.objects.all())
assert search.count() == 1 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})() request = type('', (object,), {'space': space_1, 'user': user1})()
search = RecipeSearch(request, makenow='true') search = RecipeSearch(request, makenow='true')
with scope(space=space_1): 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() onhand_user = food.onhand_users.first()
food.onhand_users.clear() food.onhand_users.clear()
food.substitute_children = True food.substitute_children = True
food.save() food.save()
assert search.get_queryset(Recipe.objects.all()).count() == 0 assert search.get_queryset(Recipe.objects.all()).count() == 0
new_food = FoodFactory.create(space=space_1, onhand_users=[onhand_user]) new_food = FoodFactory.create(
new_food.move(food, 'first-child') space=space_1, onhand_users=[onhand_user])
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9 new_food.move(food, node_location)
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, numchild__gt=0).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, numchild__gt=0).count() == 1
search = search.get_queryset(Recipe.objects.all()) search = search.get_queryset(Recipe.objects.all())
assert search.count() == 1 assert search.count() == 1
assert search.first().id == makenow_recipe.id 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})() request = type('', (object,), {'space': space_1, 'user': user1})()
search = RecipeSearch(request, makenow='true') search = RecipeSearch(request, makenow='true')
with scope(space=space_1): 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() onhand_user = food.onhand_users.first()
food.onhand_users.clear() food.onhand_users.clear()
food.substitute_siblings = True food.substitute_siblings = True
food.save() food.save()
assert search.get_queryset(Recipe.objects.all()).count() == 0 assert search.get_queryset(Recipe.objects.all()).count() == 0
new_parent = FoodFactory.create(space=space_1) new_parent = FoodFactory.create(space=space_1)
new_sibling = FoodFactory.create(space=space_1, onhand_users=[onhand_user]) new_sibling = FoodFactory.create(
new_sibling.move(new_parent, 'first-child') space=space_1, onhand_users=[onhand_user])
food.move(new_parent, 'first-child') new_sibling.move(new_parent, node_location)
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, onhand_users__isnull=False).count() == 9 food.move(new_parent, node_location)
assert Food.objects.filter(ingredient__step__recipe=makenow_recipe.id, depth=2).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, depth=2).count() == 1
search = search.get_queryset(Recipe.objects.all()) search = search.get_queryset(Recipe.objects.all())
assert search.count() == 1 assert search.count() == 1
assert search.first().id == makenow_recipe.id 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.contrib import auth
from django.urls import reverse from django.urls import reverse
from django.utils import timezone 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.conftest import transpose
from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFactory, from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFactory,
KeywordFactory, RecipeBookEntryFactory, RecipeFactory, KeywordFactory, RecipeBookEntryFactory, RecipeFactory,
@ -23,7 +23,8 @@ from cookbook.tests.factories import (CookLogFactory, FoodFactory, IngredientFac
# TODO makenow with above filters # TODO makenow with above filters
# TODO test search food/keywords including/excluding children # TODO test search food/keywords including/excluding children
LIST_URL = 'api:recipe-list' 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 @pytest.fixture
@ -50,26 +51,43 @@ def user1(request, space_1, u1_s1, unaccent):
if params.get('fuzzy_lookups', False): if params.get('fuzzy_lookups', False):
user.searchpreference.lookup = True user.searchpreference.lookup = True
misspelled_result = 1 misspelled_result = 1
else:
user.searchpreference.lookup = False
if params.get('fuzzy_search', False): if params.get('fuzzy_search', False):
user.searchpreference.trigram.set(SearchFields.objects.all()) user.searchpreference.trigram.set(SearchFields.objects.all())
misspelled_result = 1 misspelled_result = 1
else:
user.searchpreference.trigram.set([])
if params.get('icontains', False): if params.get('icontains', False):
user.searchpreference.icontains.set(SearchFields.objects.all()) user.searchpreference.icontains.set(SearchFields.objects.all())
search_term = 'ghijklmn' search_term = 'ghijklmn'
else:
user.searchpreference.icontains.set([])
if params.get('istartswith', False): if params.get('istartswith', False):
user.searchpreference.istartswith.set(SearchFields.objects.all()) user.searchpreference.istartswith.set(SearchFields.objects.all())
search_term = 'abcdef' search_term = 'abcdef'
else:
user.searchpreference.istartswith.set([])
if params.get('unaccent', False): if params.get('unaccent', False):
user.searchpreference.unaccent.set(SearchFields.objects.all()) user.searchpreference.unaccent.set(SearchFields.objects.all())
misspelled_result *= 2 misspelled_result *= 2
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 # full text vectors are hard coded to use unaccent - put this after unaccent to override result
if params.get('fulltext', False): if params.get('fulltext', False):
user.searchpreference.fulltext.set(SearchFields.objects.all()) user.searchpreference.fulltext.set(SearchFields.objects.all())
# user.searchpreference.search = 'websearch' # user.searchpreference.search = 'websearch'
search_term = 'ghijklmn uvwxyz' search_term = 'ghijklmn uvwxyz'
result = 2 result = 2
else:
user.searchpreference.fulltext.set([])
user.searchpreference.save() user.searchpreference.save()
misspelled_term = transpose(search_term, number=3) misspelled_term = transpose(search_term, number=3)
return (u1_s1, result, misspelled_result, search_term, misspelled_term, params) 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) obj2 = FoodFactory.create(name=accent, space=space_1)
recipe1.steps.first().ingredients.add(IngredientFactory.create(food=obj1)) recipe1.steps.first().ingredients.add(IngredientFactory.create(food=obj1))
recipe2.steps.first().ingredients.add(IngredientFactory.create(food=obj2)) 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): if request.param.get('keyword', None):
obj1 = KeywordFactory.create(name=unaccent, space=space_1) obj1 = KeywordFactory.create(name=unaccent, space=space_1)
obj2 = KeywordFactory.create(name=accent, 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) obj2 = UnitFactory.create(name=accent, space=space_1)
recipe1.steps.first().ingredients.add(IngredientFactory.create(unit=obj1)) recipe1.steps.first().ingredients.add(IngredientFactory.create(unit=obj1))
recipe2.steps.first().ingredients.add(IngredientFactory.create(unit=obj2)) 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): if request.param.get('name', None):
recipe1.name = unaccent recipe1.name = unaccent
recipe2.name = accent recipe2.name = accent
@ -145,21 +165,32 @@ def found_recipe(request, space_1, accent, unaccent, u1_s1, u2_s1):
i2.save() i2.save()
if request.param.get('viewedon', None): if request.param.get('viewedon', None):
ViewLogFactory.create(recipe=recipe1, created_by=user1, created_at=days_3, space=space_1) ViewLogFactory.create(recipe=recipe1, created_by=user1,
ViewLogFactory.create(recipe=recipe2, created_by=user1, created_at=days_30, space=space_1) created_at=days_3, space=space_1)
ViewLogFactory.create(recipe=recipe3, created_by=user2, created_at=days_15, 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): if request.param.get('cookedon', None):
CookLogFactory.create(recipe=recipe1, created_by=user1, created_at=days_3, space=space_1) CookLogFactory.create(recipe=recipe1, created_by=user1,
CookLogFactory.create(recipe=recipe2, created_by=user1, created_at=days_30, space=space_1) created_at=days_3, space=space_1)
CookLogFactory.create(recipe=recipe3, created_by=user2, created_at=days_15, 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): 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(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): if request.param.get('rating', None):
CookLogFactory.create(recipe=recipe1, created_by=user1, rating=5.0, space=space_1) CookLogFactory.create(
CookLogFactory.create(recipe=recipe2, created_by=user1, rating=1.0, space=space_1) recipe=recipe1, created_by=user1, rating=5.0, space=space_1)
CookLogFactory.create(recipe=recipe3, created_by=user2, rating=3.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) 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[1].id in [x['id'] for x in r['results']]
assert found_recipe[2].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 r['count'] == operator[1]
assert found_recipe[2].id in [x['id'] for x in r['results']] 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[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']] 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 r['count'] == 10 + operator[2]
assert found_recipe[2].id not in [x['id'] for x in r['results']] 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[1].id in [x['id'] for x in r['results']]
assert found_recipe[2].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 r['count'] == 3
assert found_recipe[2].id in [x['id'] for x in r['results']] 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]}" param1 = f"query={user1[3]}"
param2 = f"query={user1[4]}" param2 = f"query={user1[4]}"
r = json.loads(user1[0].get(reverse(list_url) + f'?{param1}&limit=2').content) r = json.loads(user1[0].get(reverse(list_url) +
assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[3].id, found_recipe[4].id]]) == user1[1] 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) r = json.loads(user1[0].get(reverse(list_url) +
assert len([x['id'] for x in r['results'] if x['id'] in [found_recipe[3].id, found_recipe[4].id]]) == user1[2] 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 # commenting this out for general use - it is really slow
# it should be run on occasion to ensure everything still works # 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}), # ({'keyword': True}),
# ({'food': True}), # ({'food': True}),
# ], indirect=['found_recipe']) # ], 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): # def test_search_string(found_recipe, recipes, user1, space_1):
# with scope(space=space_1): # with scope(space=space_1):
# param1 = f"query={user1[3]}" # param1 = f"query={user1[3]}"
# param2 = f"query={user1[4]}" # param2 = f"query={user1[4]}"
# r = json.loads(user1[0].get(reverse(LIST_URL) + f'?{param1}').content) # 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) # 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", [ @pytest.mark.parametrize("found_recipe, param_type, result", [
({'viewedon': True}, 'viewedon', (1, 1)), ({'viewedon': True}, 'viewedon', (1, 1)),
({'cookedon': True}, 'cookedon', (1, 1)), ({'cookedon': True}, 'cookedon', (1, 1)),
({'createdon': True}, 'createdon', (2, 12)), # created dates are not filtered by user # created dates are not filtered by user
({'createdon': True}, 'updatedon', (2, 12)), # updated 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']) ], indirect=['found_recipe'])
def test_search_date(found_recipe, recipes, param_type, result, u1_s1, u2_s1, space_1): def test_search_date(found_recipe, recipes, param_type, result, u1_s1, u2_s1, space_1):
# force updated_at to equal created_at datetime # force updated_at to equal created_at datetime
with scope(space=space_1): with scope(space=space_1):
for recipe in Recipe.objects.all(): 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") date = (timezone.now() - timedelta(days=15)).strftime("%Y-%m-%d")
param1 = f"?{param_type}={date}" 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']] 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", [
# @pytest.mark.parametrize("found_recipe, param_type", [ ({'rating': True}, 'rating'),
# ({'rating': True}, 'rating'), ({'timescooked': True}, 'timescooked'),
# ({'timescooked': True}, 'timescooked'), ], indirect=['found_recipe'])
# ], indirect=['found_recipe']) def test_search_count(found_recipe, recipes, param_type, u1_s1, u2_s1, space_1):
# def test_search_count(found_recipe, recipes, param_type, u1_s1, u2_s1, space_1): param1 = f'?{param_type}=3'
# param1 = f'?{param_type}=3' param2 = f'?{param_type}=-3'
# param2 = f'?{param_type}=-3' param3 = f'?{param_type}=0'
# param3 = f'?{param_type}=0'
# r = json.loads(u1_s1.get(reverse(LIST_URL) + param1).content)
# r = json.loads(u1_s1.get(reverse(LIST_URL) + param1).content) assert r['count'] == 1
# assert r['count'] == 1 assert found_recipe[0].id in [x['id'] for x in r['results']]
# assert found_recipe[0].id in [x['id'] for x in r['results']]
# r = json.loads(u1_s1.get(reverse(LIST_URL) + param2).content)
# r = json.loads(u1_s1.get(reverse(LIST_URL) + param2).content) assert r['count'] == 1
# assert r['count'] == 1 assert found_recipe[1].id in [x['id'] for x in r['results']]
# assert found_recipe[1].id in [x['id'] for x in r['results']]
# # test search for not rated/cooked
# # test search for not rated/cooked r = json.loads(u1_s1.get(reverse(LIST_URL) + param3).content)
# r = json.loads(u1_s1.get(reverse(LIST_URL) + param3).content) assert r['count'] == 11
# assert r['count'] == 11 assert (found_recipe[0].id or found_recipe[1].id) not in [
# assert (found_recipe[0].id or found_recipe[1].id) not in [x['id'] for x in r['results']] x['id'] for x in r['results']]
#
# # test matched returns for lte and gte searches # test matched returns for lte and gte searches
# r = json.loads(u2_s1.get(reverse(LIST_URL) + param1).content) r = json.loads(u2_s1.get(reverse(LIST_URL) + param1).content)
# assert r['count'] == 1 assert r['count'] == 1
# assert found_recipe[2].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(u2_s1.get(reverse(LIST_URL) + param2).content) r = json.loads(u2_s1.get(reverse(LIST_URL) + param2).content)
# assert r['count'] == 1 assert r['count'] == 1
# assert found_recipe[2].id in [x['id'] for x in r['results']] 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_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn', 'email': 'mail'}
AUTH_LDAP_ALWAYS_UPDATE_USER=1 AUTH_LDAP_ALWAYS_UPDATE_USER=1
AUTH_LDAP_CACHE_TIMEOUT=3600 AUTH_LDAP_CACHE_TIMEOUT=3600
AUTH_LDAP_START_TLS=1
AUTH_LDAP_TLS_CACERTFILE=/etc/ssl/certs/own-ca.pem AUTH_LDAP_TLS_CACERTFILE=/etc/ssl/certs/own-ca.pem
``` ```

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,64 +18,68 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "Englisch" msgstr "Englisch"
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "Deutsch" msgstr "Deutsch"
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
#, fuzzy #, fuzzy
#| msgid "English" #| msgid "English"
msgid "Polish" msgid "Polish"
msgstr "Englisch" msgstr "Englisch"
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,62 +17,66 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -19,62 +19,66 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : " "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : "
"2);\n" "2);\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,62 +17,66 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,62 +18,66 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,62 +17,66 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: .\recipes\settings.py:382 #: .\recipes\settings.py:436
msgid "Armenian " msgid "Armenian "
msgstr "" msgstr ""
#: .\recipes\settings.py:383 #: .\recipes\settings.py:437
msgid "Bulgarian" msgid "Bulgarian"
msgstr "" msgstr ""
#: .\recipes\settings.py:384 #: .\recipes\settings.py:438
msgid "Catalan" msgid "Catalan"
msgstr "" msgstr ""
#: .\recipes\settings.py:385 #: .\recipes\settings.py:439
msgid "Czech" msgid "Czech"
msgstr "" msgstr ""
#: .\recipes\settings.py:386 #: .\recipes\settings.py:440
msgid "Danish" msgid "Danish"
msgstr "" msgstr ""
#: .\recipes\settings.py:387 #: .\recipes\settings.py:441
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: .\recipes\settings.py:388 #: .\recipes\settings.py:442
msgid "English" msgid "English"
msgstr "" msgstr ""
#: .\recipes\settings.py:389 #: .\recipes\settings.py:443
msgid "French" msgid "French"
msgstr "" msgstr ""
#: .\recipes\settings.py:390 #: .\recipes\settings.py:444
msgid "German" msgid "German"
msgstr "" msgstr ""
#: .\recipes\settings.py:391 #: .\recipes\settings.py:445
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: .\recipes\settings.py:392 #: .\recipes\settings.py:447
msgid "Latvian" msgid "Latvian"
msgstr "" msgstr ""
#: .\recipes\settings.py:393 #: .\recipes\settings.py:448
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: .\recipes\settings.py:394 #: .\recipes\settings.py:449
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: .\recipes\settings.py:395 #: .\recipes\settings.py:450
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: .\recipes\settings.py:396 #: .\recipes\settings.py:451
msgid "Swedish" msgid "Swedish"
msgstr "" msgstr ""

View File

@ -24,7 +24,8 @@ load_dotenv()
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Get vars from .env files # Get vars from .env files
SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv('SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV' SECRET_KEY = os.getenv('SECRET_KEY') if os.getenv(
'SECRET_KEY') else 'INSECURE_STANDARD_KEY_SET_IN_ENV'
DEBUG = bool(int(os.getenv('DEBUG', True))) DEBUG = bool(int(os.getenv('DEBUG', True)))
DEBUG_TOOLBAR = bool(int(os.getenv('DEBUG_TOOLBAR', 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_RECIPES = int(os.getenv('SPACE_DEFAULT_MAX_RECIPES', 0))
SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0)) SPACE_DEFAULT_MAX_USERS = int(os.getenv('SPACE_DEFAULT_MAX_USERS', 0))
SPACE_DEFAULT_MAX_FILES = int(os.getenv('SPACE_DEFAULT_MAX_FILES', 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 # allow djangos wsgi server to server mediafiles
GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', True))) 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))) 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 # 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'): if os.getenv('CSRF_TRUSTED_ORIGINS'):
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',') CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',')
@ -131,7 +136,8 @@ try:
plugin_module = f'recipes.plugins.{d}.apps.{app_config_classname}' plugin_module = f'recipes.plugins.{d}.apps.{app_config_classname}'
if plugin_module not in INSTALLED_APPS: if plugin_module not in INSTALLED_APPS:
INSTALLED_APPS.append(plugin_module) 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 = { plugin_config = {
'name': plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name, 'name': plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name,
'module': f'recipes.plugins.{d}', 'module': f'recipes.plugins.{d}',
@ -148,7 +154,8 @@ except Exception:
if DEBUG: if DEBUG:
print('ERROR failed to initialize plugins') 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' SOCIALACCOUNT_EMAIL_VERIFICATION = 'none'
INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS INSTALLED_APPS = INSTALLED_APPS + SOCIAL_PROVIDERS
@ -194,7 +201,8 @@ if DEBUG_TOOLBAR:
INSTALLED_APPS += ('debug_toolbar',) INSTALLED_APPS += ('debug_toolbar',)
SORT_TREE_BY_NAME = bool(int(os.getenv('SORT_TREE_BY_NAME', False))) 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))): if bool(int(os.getenv('SQL_DEBUG', False))):
MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware',) MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware',)
@ -213,6 +221,7 @@ if LDAP_AUTH:
AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend') AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend')
AUTH_LDAP_SERVER_URI = os.getenv('AUTH_LDAP_SERVER_URI') 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_DN = os.getenv('AUTH_LDAP_BIND_DN')
AUTH_LDAP_BIND_PASSWORD = os.getenv('AUTH_LDAP_BIND_PASSWORD') AUTH_LDAP_BIND_PASSWORD = os.getenv('AUTH_LDAP_BIND_PASSWORD')
AUTH_LDAP_USER_SEARCH = LDAPSearch( AUTH_LDAP_USER_SEARCH = LDAPSearch(
@ -225,10 +234,12 @@ if LDAP_AUTH:
'last_name': 'sn', 'last_name': 'sn',
'email': 'mail', '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)) AUTH_LDAP_CACHE_TIMEOUT = int(os.getenv('AUTH_LDAP_CACHE_TIMEOUT', 3600))
if 'AUTH_LDAP_TLS_CACERTFILE' in os.environ: 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: if DEBUG:
LOGGING = { LOGGING = {
"version": 1, "version": 1,
@ -249,7 +260,8 @@ ACCOUNT_ADAPTER = 'cookbook.helper.AllAuthCustomAdapter'
if REVERSE_PROXY_AUTH: if REVERSE_PROXY_AUTH:
MIDDLEWARE.insert(8, 'recipes.middleware.CustomRemoteUser') MIDDLEWARE.insert(8, 'recipes.middleware.CustomRemoteUser')
AUTHENTICATION_BACKENDS.append('django.contrib.auth.backends.RemoteUserBackend') AUTHENTICATION_BACKENDS.append(
'django.contrib.auth.backends.RemoteUserBackend')
# Password validation # Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
@ -444,7 +456,8 @@ LANGUAGES = [
SCRIPT_NAME = os.getenv('SCRIPT_NAME', '') 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 # 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) JS_REVERSE_SCRIPT_PREFIX = os.getenv('JS_REVERSE_SCRIPT_PREFIX', SCRIPT_NAME)
STATIC_URL = os.getenv('STATIC_URL', '/static/') 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_TLS = bool(int(os.getenv('EMAIL_USE_TLS', False)))
EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False))) EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False)))
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost') 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 Jinja2==3.1.2
django-webpack-loader==1.8.1 django-webpack-loader==1.8.1
git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82 git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82
django-allauth==0.52.0 django-allauth==0.54.0
recipe-scrapers==14.35.0 recipe-scrapers==14.36.1
django-scopes==1.2.0.post1 django-scopes==1.2.0.post1
pytest==7.2.2 pytest==7.3.1
pytest-django==4.5.2 pytest-django==4.5.2
django-treebeard==4.5.1 django-treebeard==4.7
django-cors-headers==3.13.0 django-cors-headers==3.13.0
django-storages==1.13.2 django-storages==1.13.2
boto3==1.26.41 boto3==1.26.41
@ -42,7 +42,7 @@ django-prometheus==2.2.0
django-hCaptcha==0.2.0 django-hCaptcha==0.2.0
python-ldap==3.4.3 python-ldap==3.4.3
django-auth-ldap==4.2.0 django-auth-ldap==4.2.0
pytest-factoryboy==2.5.0 pytest-factoryboy==2.5.1
pyppeteer==1.0.2 pyppeteer==1.0.2
validators==0.20.0 validators==0.20.0
pytube==12.1.0 pytube==12.1.0

View File

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

View File

@ -72,7 +72,7 @@
"Cancel": "Abbrechen", "Cancel": "Abbrechen",
"success_deleting_resource": "Ressource erfolgreich gelöscht!", "success_deleting_resource": "Ressource erfolgreich gelöscht!",
"Load_More": "Weitere laden", "Load_More": "Weitere laden",
"Ok": "Öffnen", "Ok": "Ok",
"Link": "Link", "Link": "Link",
"Key_Ctrl": "Strg", "Key_Ctrl": "Strg",
"move_title": "{type} verschieben", "move_title": "{type} verschieben",
@ -114,7 +114,7 @@
"Create_New_Shopping Category": "Neue Einkaufskategorie erstellen", "Create_New_Shopping Category": "Neue Einkaufskategorie erstellen",
"Automate": "Automatisieren", "Automate": "Automatisieren",
"Type": "Typ", "Type": "Typ",
"and_up": "& Höher", "and_up": "& Hoch",
"Unrated": "Unbewertet", "Unrated": "Unbewertet",
"Shopping_list": "Einkaufsliste", "Shopping_list": "Einkaufsliste",
"step_time_minutes": "Schritt Dauer in Minuten", "step_time_minutes": "Schritt Dauer in Minuten",
@ -206,7 +206,7 @@
"New_Cookbook": "Neues Kochbuch", "New_Cookbook": "Neues Kochbuch",
"Coming_Soon": "Bald verfügbar", "Coming_Soon": "Bald verfügbar",
"Auto_Planner": "Smart Planen", "Auto_Planner": "Smart Planen",
"Hide_Keyword": "Keywords schließen", "Hide_Keyword": "Schlüsselwörter verbergen",
"Clear": "Leeren", "Clear": "Leeren",
"GroupBy": "Gruppieren nach", "GroupBy": "Gruppieren nach",
"IgnoreThis": "Füge {food} nie automatisch zur Einkaufsliste hinzu", "IgnoreThis": "Füge {food} nie automatisch zur Einkaufsliste hinzu",
@ -222,11 +222,11 @@
"NoCategory": "Keine Kategorie ausgewählt.", "NoCategory": "Keine Kategorie ausgewählt.",
"ShowDelayed": "Zeige verschobene Elemente", "ShowDelayed": "Zeige verschobene Elemente",
"Completed": "Fertig", "Completed": "Fertig",
"OfflineAlert": "Du bist offline, deine Einkaufsliste wird nicht synchronisiert.", "OfflineAlert": "Du bist offline. Deine Einkaufsliste wird nicht synchronisiert.",
"shopping_share": "Einkaufsliste teilen", "shopping_share": "Einkaufsliste teilen",
"mealplan_autoadd_shopping": "Automatisches Hinzufügen zum Essensplan", "mealplan_autoadd_shopping": "Automatisches Hinzufügen zum Essensplan",
"mealplan_autoexclude_onhand": "Ignoriere vorrätige Zutaten", "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", "default_delay": "Standard-Verzögerungszeit",
"Added_by": "Hinzugefügt durch", "Added_by": "Hinzugefügt durch",
"AddToShopping": "Zur Einkaufsliste hinzufügen", "AddToShopping": "Zur Einkaufsliste hinzufügen",
@ -241,7 +241,7 @@
"IngredientInShopping": "Diese Zutat befindet sich auf Ihrer Einkaufsliste.", "IngredientInShopping": "Diese Zutat befindet sich auf Ihrer Einkaufsliste.",
"NotInShopping": "{food} befindet sich nicht auf Ihrer Einkaufsliste.", "NotInShopping": "{food} befindet sich nicht auf Ihrer Einkaufsliste.",
"OnHand": "Aktuell vorrätig", "OnHand": "Aktuell vorrätig",
"FoodNotOnHand": "Sie habe {food} nicht vorrätig.", "FoodNotOnHand": "Sie haben {food} nicht vorrätig.",
"Undefined": "undefiniert", "Undefined": "undefiniert",
"AddFoodToShopping": "Fügen Sie {food} zur Einkaufsliste hinzu", "AddFoodToShopping": "Fügen Sie {food} zur Einkaufsliste hinzu",
"RemoveFoodFromShopping": "{food} von der Einkaufsliste löschen", "RemoveFoodFromShopping": "{food} von der Einkaufsliste löschen",
@ -251,23 +251,23 @@
"mealplan_autoadd_shopping_desc": "Zutaten aus dem Essensplan automatisch zur Einkaufsliste hinzufügen.", "mealplan_autoadd_shopping_desc": "Zutaten aus dem Essensplan automatisch zur Einkaufsliste hinzufügen.",
"Pin": "Anheften", "Pin": "Anheften",
"mark_complete": "Vollständig markieren", "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": "Linkshänder-Modus",
"left_handed_help": "Optimiert die Benutzeroberfläche für die Bedienung mit der linken Hand.", "left_handed_help": "Optimiert die Benutzeroberfläche für die Bedienung mit der linken Hand.",
"FoodInherit": "Lebensmittel vererbbare Felder", "FoodInherit": "Lebensmittel vererbbare Felder",
"SupermarketCategoriesOnly": "Nur Supermarktkategorien", "SupermarketCategoriesOnly": "Nur Supermarktkategorien",
"InheritWarning": "{food} ist auf Vererbung gesetzt ist, Änderungen werden möglicherweise nicht gespeichert.", "InheritWarning": "{food} ist auf Vererbung gesetzt, Änderungen werden möglicherweise nicht gespeichert.",
"mealplan_autoexclude_onhand_desc": "Beim (manuellen oder automatischen) Hinzufügen eines Essensplans zur Einkaufsliste vorrätige Zutagen ausnehmen.", "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.", "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.", "default_delay_desc": "Voreingestellte Anzahl von Stunden für die Verzögerung eines Einkaufslisteneintrags.",
"filter_to_supermarket": "Nach Supermarkt filtern", "filter_to_supermarket": "Nach Supermarkt filtern",
"err_move_self": "Element kann nicht auf sich selbst verschoben werden", "err_move_self": "Element kann nicht auf sich selbst verschoben werden",
"nothing": "Nichts zu tun", "nothing": "Nichts zu tun",
"err_merge_self": "Element kann nicht mit sich selbst zusammengeführt werden", "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.", "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", "CategoryName": "Kategorienname",
"SupermarketName": "Supermarkt Name", "SupermarketName": "Name Supermarkt",
"CategoryInstruction": "Ziehen Sie Kategorien, um die Reihenfolge zu ändern, in der die Kategorien in der Einkaufsliste erscheinen.", "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_desc": "Tage der letzten Einträge in der Einkaufsliste, die angezeigt werden sollen.",
"shopping_recent_days": "Letzte Tage", "shopping_recent_days": "Letzte Tage",
@ -277,7 +277,7 @@
"csv_delim_help": "Trennzeichen für CSV-Exporte.", "csv_delim_help": "Trennzeichen für CSV-Exporte.",
"csv_delim_label": "CSV-Trennzeichen", "csv_delim_label": "CSV-Trennzeichen",
"SuccessClipboard": "Einkaufsliste wurde in die Zwischenablage kopiert", "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_help": "Präfix, das beim Kopieren der Liste in die Zwischenablage hinzugefügt wird.",
"csv_prefix_label": "Listenpräfix", "csv_prefix_label": "Listenpräfix",
"copy_markdown_table": "Als Markdown-Tabelle kopieren", "copy_markdown_table": "Als Markdown-Tabelle kopieren",
@ -291,10 +291,10 @@
"remember_search": "Suchbegriff merken", "remember_search": "Suchbegriff merken",
"remember_hours": "Stunden zu erinnern", "remember_hours": "Stunden zu erinnern",
"tree_select": "Baum-Auswahl verwenden", "tree_select": "Baum-Auswahl verwenden",
"CountMore": "...+{count} weitere", "CountMore": "...+{count} mehr",
"ignore_shopping_help": "Füge Zutat nie zur Einkaufsliste hinzu (z.B. Wasser)", "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.", "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", "Foods": "Lebensmittel",
"food_recipe_help": "Wird ein Rezept hier verknüpft, wird diese Verknüpfung in allen anderen Rezepten übernommen, die dieses Lebensmittel beinhaltet", "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", "review_shopping": "Überprüfe die Einkaufsliste vor dem Speichern",
@ -356,10 +356,10 @@
"search_rank": "Such-Rang", "search_rank": "Such-Rang",
"paste_ingredients": "Zutaten einfügen", "paste_ingredients": "Zutaten einfügen",
"Ingredient Editor": "Zutateneditor", "Ingredient Editor": "Zutateneditor",
"Protected": "Geschützt", "Protected": "Schützen",
"not": "nicht", "not": "nicht",
"warning_duplicate_filter": "Warnung: Wegen technischen Limitierungen können mehrere Filter der selben Kombination (und/oder/nicht) zu unerwarteten Ergebnissen führen.", "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", "enable_expert": "Expertenmodus aktivieren",
"filter_name": "Name des Filters", "filter_name": "Name des Filters",
"shared_with": "Geteilt mit", "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?", "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": "Erstelle einen neuen Supermarkt",
"New_Supermarket_Category": "Erstelle eine neue Supermarktkategorie", "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?", "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": "Link kopieren", "Copy Link": "Kopiere den Link in die Zwischenablage",
"Users": "Benutzer", "Users": "Benutzer",
"facet_count_info": "Zeige die Anzahl der Rezepte auf den Suchfiltern.", "facet_count_info": "Zeige die Anzahl der Rezepte auf den Suchfiltern.",
"Copy Token": "Token kopieren", "Copy Token": "Kopiere Token",
"Invites": "Einladungen", "Invites": "Einladungen",
"Message": "Nachricht", "Message": "Nachricht",
"Bookmarklet": "Lesezeichen", "Bookmarklet": "Lesezeichen",
@ -473,11 +473,13 @@
"UnpinnedConfirmation": "{recipe} wurde gelöst.", "UnpinnedConfirmation": "{recipe} wurde gelöst.",
"Description_Replace": "Beschreibung ersetzen", "Description_Replace": "Beschreibung ersetzen",
"Instruction_Replace": "Anleitung 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.", "Auto_Sort_Help": "Verschiebe alle Zutaten zu dem Schritt, der am Besten passt.",
"Combine_All_Steps": "Fasse alle Schritte in einem einzelnem Feld zusammen.", "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.", "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", "Unpin": "Lösen",
"Amount": "Menge", "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", "New_Supermarket_Category": "Create new supermarket category",
"Are_You_Sure": "Are you sure?", "Are_You_Sure": "Are you sure?",
"Valid Until": "Valid Until", "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.", "Combine_All_Steps": "Combine all steps into a single field.",
"Plural": "Plural", "Plural": "Plural",
"plural_short": "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ă", "convert_internal": "Transformați în rețetă internă",
"show_only_internal": "Arătați doar rețetele interne", "show_only_internal": "Arătați doar rețetele interne",
"show_split_screen": "Vedere divizată", "show_split_screen": "Vedere divizată",
"Log_Recipe_Cooking": "", "Log_Recipe_Cooking": "Jurnalul rețetelor de pregătire",
"External_Recipe_Image": "", "External_Recipe_Image": "Imagine rețetă externă",
"Add_to_Shopping": "", "Add_to_Shopping": "Adaugare la cumpărături",
"Add_to_Plan": "", "Add_to_Plan": "Adăugare la plan",
"Step_start_time": "", "Step_start_time": "Pasule de începere a orei",
"Sort_by_new": "", "Sort_by_new": "Sortare după nou",
"Table_of_Contents": "", "Table_of_Contents": "Cuprins",
"Recipes_per_page": "", "Recipes_per_page": "Rețete pe pagină",
"Show_as_header": "", "Show_as_header": "Afișare ca antet",
"Hide_as_header": "", "Hide_as_header": "Ascunderea ca antet",
"Add_nutrition_recipe": "", "Add_nutrition_recipe": "Adăugare a nutriției la rețetă",
"Remove_nutrition_recipe": "", "Remove_nutrition_recipe": "Ștergere a nutriției din rețetă",
"Copy_template_reference": "", "Copy_template_reference": "Copie referința șablonului",
"Save_and_View": "", "Save_and_View": "Salvare și vizionare",
"Manage_Books": "", "Manage_Books": "Gestionarea cărților",
"Meal_Plan": "", "Meal_Plan": "Plan de alimentare",
"Select_Book": "", "Select_Book": "Selectare carte",
"Select_File": "", "Select_File": "Selectare fișier",
"Recipe_Image": "", "Recipe_Image": "Imagine a rețetei",
"Import_finished": "", "Import_finished": "Importare finalizată",
"View_Recipes": "", "View_Recipes": "Vizionare rețete",
"Log_Cooking": "", "Log_Cooking": "Jurnal de pregătire",
"New_Recipe": "", "New_Recipe": "Rețetă nouă",
"Url_Import": "", "Url_Import": "Importă URL",
"Reset_Search": "", "Reset_Search": "Resetarea căutării",
"Recently_Viewed": "", "Recently_Viewed": "Vizualizate recent",
"Load_More": "", "Load_More": "Încărcați mai mult",
"New_Keyword": "", "New_Keyword": "Cuvânt cheie nou",
"Delete_Keyword": "", "Delete_Keyword": "Ștergere cuvânt cheie",
"Edit_Keyword": "", "Edit_Keyword": "Editează cuvânt cheie",
"Edit_Recipe": "", "Edit_Recipe": "Editează rețeta",
"Move_Keyword": "", "Move_Keyword": "Mută cuvânt cheie",
"Merge_Keyword": "", "Merge_Keyword": "Unește cuvânt cheie",
"Hide_Keywords": "", "Hide_Keywords": "Ascunde cuvânt cheie",
"Hide_Recipes": "", "Hide_Recipes": "Ascunde rețetele",
"Move_Up": "", "Move_Up": "Deplasați-vă în sus",
"Move_Down": "", "Move_Down": "Deplasați-vă în jos",
"Step_Name": "", "Step_Name": "Nume pas",
"Step_Type": "", "Step_Type": "Tip pas",
"Make_Header": "", "Make_Header": "Creare antet",
"Make_Ingredient": "", "Make_Ingredient": "Create ingredient",
"Enable_Amount": "", "Enable_Amount": "Activare cantitate",
"Disable_Amount": "", "Disable_Amount": "Dezactivare cantitate",
"Add_Step": "", "Add_Step": "Adaugă pas",
"Keywords": "", "Keywords": "Cuvinte cheie",
"Books": "", "Books": "Cărți",
"Proteins": "", "Proteins": "Proteine",
"Fats": "", "Fats": "Grăsimi",
"Carbohydrates": "", "Carbohydrates": "Carbohidrați",
"Calories": "", "Calories": "Calorii",
"Energy": "", "Energy": "Energie",
"Nutrition": "", "Nutrition": "Nutriție",
"Date": "", "Date": "Dată",
"Share": "", "Share": "Împărtășire",
"Automation": "", "Automation": "Automatizare",
"Parameter": "", "Parameter": "Parametru",
"Export": "", "Export": "Exportă",
"Copy": "", "Copy": "Copie",
"Rating": "", "Rating": "Evaluare",
"Close": "", "Close": "Închide",
"Cancel": "", "Cancel": "Anulează",
"Link": "", "Link": "Link",
"Add": "", "Add": "Adaugă",
"New": "", "New": "Nou",
"Note": "", "Note": "Notă",
"Success": "", "Success": "Succes",
"Failure": "", "Failure": "Eșec",
"Ingredients": "", "Ingredients": "Ingrediente",
"Supermarket": "", "Supermarket": "Supermarket",
"Categories": "", "Categories": "Categorii",
"Category": "", "Category": "Categorie",
"Selected": "", "Selected": "Selectat",
"min": "", "min": "min",
"Servings": "", "Servings": "Porții",
"Waiting": "", "Waiting": "Așteptare",
"Preparation": "", "Preparation": "Pregătire",
"External": "", "External": "Extern",
"Size": "", "Size": "Marime",
"Files": "", "Files": "Fișiere",
"File": "", "File": "Fișier",
"Edit": "", "Edit": "Editează",
"Image": "", "Image": "Imagine",
"Delete": "", "Delete": "Șterge",
"Open": "", "Open": "Deschide",
"Ok": "", "Ok": "Ok",
"Save": "", "Save": "Salvare",
"Step": "", "Step": "Pas",
"Search": "", "Search": "Căutare",
"Import": "", "Import": "Importă",
"Print": "", "Print": "Tipărește",
"Settings": "", "Settings": "Setări",
"or": "", "or": "sau",
"and": "", "and": "și",
"Information": "", "Information": "Informație",
"Download": "", "Download": "Descarcă",
"Create": "", "Create": "Creează",
"Advanced Search Settings": "", "Advanced Search Settings": "",
"View": "", "View": "Vizualizare",
"Recipes": "", "Recipes": "Rețete",
"Move": "", "Move": "Mută",
"Merge": "", "Merge": "Unire",
"Parent": "", "Parent": "Părinte",
"delete_confirmation": "", "delete_confirmation": "Sunteți sigur că doriți să ștergeți {source}?",
"move_confirmation": "", "move_confirmation": "Mutare <i>{copil}</i> la părinte <i>{părinte}</i>",
"merge_confirmation": "", "merge_confirmation": "Înlocuiți <i>{source}</i> cu <i>{target}</i>",
"create_rule": "", "create_rule": "și crearea automatizării",
"move_selection": "", "move_selection": "Selectați un părinte {type} pentru a muta {source} în.",
"merge_selection": "", "merge_selection": "Înlocuiți toate aparițiile {source} cu {type} selectat.",
"Root": "", "Root": "Rădăcină",
"Ignore_Shopping": "", "Ignore_Shopping": "Ignoră cumpărăturile",
"Shopping_Category": "", "Shopping_Category": "Categorie de cumpărături",
"Edit_Food": "", "Edit_Food": "Editare mâncare",
"Move_Food": "", "Move_Food": "Mutare mâncare",
"New_Food": "", "New_Food": "Mâncare nouă",
"Hide_Food": "", "Hide_Food": "Ascunde mâncare",
"Food_Alias": "", "Food_Alias": "Pseudonim mâncare",
"Unit_Alias": "", "Unit_Alias": "Pseudonim unitate",
"Keyword_Alias": "", "Keyword_Alias": "Pseudonim cuvânt cheie",
"Delete_Food": "", "Delete_Food": "Ștergere mâncare",
"No_ID": "", "No_ID": "ID-ul nu a fost găsit, nu se poate șterge.",
"Meal_Plan_Days": "", "Meal_Plan_Days": "Planuri de alimentație pe viitor",
"merge_title": "", "merge_title": "Unire {type}",
"move_title": "", "move_title": "Mutare {type}",
"Food": "", "Food": "Mâncare",
"Recipe_Book": "", "Recipe_Book": "Carte de rețete",
"del_confirmation_tree": "", "del_confirmation_tree": "Sunteți sigur că doriți să ștergeți {sursa} și toți copiii săi?",
"delete_title": "", "delete_title": "Ștergere {type}",
"create_title": "", "create_title": "{type} nou",
"edit_title": "", "edit_title": "Editare {type}",
"Name": "", "Name": "Nume",
"Type": "", "Type": "Tip",
"Description": "", "Description": "Descriere",
"Recipe": "", "Recipe": "Rețetă",
"tree_root": "", "tree_root": "Rădăcina copacului",
"Icon": "", "Icon": "Iconiță",
"Unit": "", "Unit": "Unitate",
"No_Results": "", "No_Results": "Fără rezultate",
"New_Unit": "", "New_Unit": "Unitate nouă",
"Create_New_Shopping Category": "", "Create_New_Shopping Category": "Creați o nouă categorie de cumpărături",
"Create_New_Food": "", "Create_New_Food": "Adaugă mâncare nouă",
"Create_New_Keyword": "", "Create_New_Keyword": "Adaugă cuvânt cheie nou",
"Create_New_Unit": "", "Create_New_Unit": "Adaugă unitate nouă",
"Create_New_Meal_Type": "", "Create_New_Meal_Type": "Adaugă tip mâncare nou",
"and_up": "", "and_up": "& Sus",
"Instructions": "", "Instructions": "Instrucțiuni",
"Unrated": "", "Unrated": "Neevaluat",
"Automate": "", "Automate": "Automatizat",
"Empty": "", "Empty": "Gol",
"Key_Ctrl": "", "Key_Ctrl": "Ctrl",
"Key_Shift": "", "Key_Shift": "Shift",
"Time": "", "Time": "Timp",
"Text": "", "Text": "Text",
"Shopping_list": "", "Shopping_list": "Lisă de cumpărături",
"Create_Meal_Plan_Entry": "", "Create_Meal_Plan_Entry": "Crearea înregistrării în planul de alimentare",
"Edit_Meal_Plan_Entry": "", "Edit_Meal_Plan_Entry": "Editarea înregistrării în planul de alimentare",
"Title": "", "Title": "Titlu",
"Week": "", "Week": "Săptămână",
"Month": "", "Month": "Lună",
"Year": "", "Year": "An",
"Planner": "", "Planner": "Planificator",
"Planner_Settings": "", "Planner_Settings": "Setări planificator",
"Period": "", "Period": "Perioadă",
"Plan_Period_To_Show": "", "Plan_Period_To_Show": "Afișați săptămâni, luni sau ani",
"Periods": "", "Periods": "Perioade",
"Plan_Show_How_Many_Periods": "", "Plan_Show_How_Many_Periods": "Câte perioade să afișezi",
"Starting_Day": "", "Starting_Day": "Ziua de început a săptămânii",
"Meal_Types": "", "Meal_Types": "Tipuri de mese",
"Meal_Type": "", "Meal_Type": "Tipul mesei",
"Clone": "", "Clone": "Clonă",
"Drag_Here_To_Delete": "", "Drag_Here_To_Delete": "Mută aici pentru a șterge",
"Meal_Type_Required": "", "Meal_Type_Required": "Tipul mesei este necesar",
"Title_or_Recipe_Required": "", "Title_or_Recipe_Required": "Titlul sau selecția rețetei necesare",
"Color": "", "Color": "Culoare",
"New_Meal_Type": "", "New_Meal_Type": "Tip de masă nou",
"Week_Numbers": "", "Week_Numbers": "Numerele săptămânii",
"Show_Week_Numbers": "", "Show_Week_Numbers": "Afișați numerele săptămânii?",
"Export_As_ICal": "", "Export_As_ICal": "Exportul perioadei curente în format iCal",
"Export_To_ICal": "", "Export_To_ICal": "Exportă .ics",
"Cannot_Add_Notes_To_Shopping": "", "Cannot_Add_Notes_To_Shopping": "Notele nu pot fi adăugate la lista de cumpărături",
"Added_To_Shopping_List": "", "Added_To_Shopping_List": "Adăugat la lista de cumpărături",
"Shopping_List_Empty": "", "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": "", "Next_Period": "Perioada următoare",
"Previous_Period": "", "Previous_Period": "Perioada precedentă",
"Current_Period": "", "Current_Period": "Perioada curentă",
"Next_Day": "", "Next_Day": "Ziua următoare",
"Previous_Day": "", "Previous_Day": "Ziua precedentă",
"Coming_Soon": "", "Coming_Soon": "În curând",
"Auto_Planner": "", "Auto_Planner": "Planificator automat",
"New_Cookbook": "", "New_Cookbook": "Nouă carte de bucate",
"Hide_Keyword": "", "Hide_Keyword": "Ascunde cuvintele cheie",
"Clear": "", "Clear": "Curățare",
"Plural": "", "Plural": "Plural",
"plural_short": "", "plural_short": "plural",
"Use_Plural_Unit_Always": "", "Use_Plural_Unit_Always": "Utilizarea formei plurale pentru unitate întotdeauna",
"Use_Plural_Unit_Simple": "", "Use_Plural_Unit_Simple": "Utilizarea dinamică a formei plurale pentru unitate",
"Use_Plural_Food_Always": "", "Use_Plural_Food_Always": "Utilizarea formei plurale pentru alimente întotdeauna",
"Use_Plural_Food_Simple": "", "Use_Plural_Food_Simple": "Utilizarea dinamica a formei plurale pentru alimente",
"plural_usage_info": "" "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": "Экспертный режим", "expert_mode": "Экспертный режим",
"enable_expert": "Включить экспертный режим", "enable_expert": "Включить экспертный режим",
"review_shopping": "Просмотрите записи о покупках перед сохранением", "review_shopping": "Просмотрите записи о покупках перед сохранением",
"empty_list": "Список пуст", "empty_list": "Список пуст.",
"default_delay_desc": "Число часов по умолчанию для отсрочки записи в списке покупок.", "default_delay_desc": "Число часов по умолчанию для отсрочки записи в списке покупок.",
"one_url_per_line": "Один URL в строке", "one_url_per_line": "Один URL в строке",
"mealplan_autoinclude_related": "Добавить сопутствующие рецепты", "mealplan_autoinclude_related": "Добавить сопутствующие рецепты",
@ -343,5 +343,8 @@
"DelayFor": "Отложить на {hours} часов", "DelayFor": "Отложить на {hours} часов",
"New_Entry": "Новая запись", "New_Entry": "Новая запись",
"GroupBy": "Сгруппировать по", "GroupBy": "Сгруппировать по",
"facet_count_info": "Показывать количество рецептов в фильтрах поиска." "facet_count_info": "Показывать количество рецептов в фильтрах поиска.",
"food_inherit_info": "Поля для продуктов питания, которые должны наследоваться по умолчанию.",
"warning_space_delete": "Вы можете удалить свое пространство, включая все рецепты, списки покупок, планы питания и все остальное, что вы создали. Этого нельзя отменить! Ты уверен, что хочешь это сделать?",
"Description_Replace": "Изменить описание"
} }