diff --git a/.env.template b/.env.template
index 5c4370fe1c..c1ba71f7b5 100644
--- a/.env.template
+++ b/.env.template
@@ -13,9 +13,18 @@ DEBUG_TOOLBAR=0
# hosts the application can run under e.g. recipes.mydomain.com,cooking.mydomain.com,...
ALLOWED_HOSTS=*
+# Cross Site Request Forgery protection
+# (https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-CSRF_TRUSTED_ORIGINS)
+# CSRF_TRUSTED_ORIGINS = []
+
+# Cross Origin Resource Sharing
+# (https://github.com/adamchainz/django-cors-header)
+# CORS_ALLOW_ALL_ORIGINS = True
+
# random secret key, use for example `base64 /dev/urandom | head -c50` to generate one
-# ---------------------------- REQUIRED -------------------------
+# ---------------------------- AT LEAST ONE REQUIRED -------------------------
SECRET_KEY=
+SECRET_KEY_FILE=
# ---------------------------------------------------------------
# your default timezone See https://timezonedb.com/time-zones for a list of timezones
@@ -27,8 +36,9 @@ DB_ENGINE=django.db.backends.postgresql
POSTGRES_HOST=db_recipes
POSTGRES_PORT=5432
POSTGRES_USER=djangouser
-# ---------------------------- REQUIRED -------------------------
+# ---------------------------- AT LEAST ONE REQUIRED -------------------------
POSTGRES_PASSWORD=
+POSTGRES_PASSWORD_FILE=
# ---------------------------------------------------------------
POSTGRES_DB=djangodb
@@ -113,7 +123,8 @@ REMOTE_USER_AUTH=0
# SPACE_DEFAULT_MAX_FILES=0 # Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.
# SPACE_DEFAULT_ALLOW_SHARING=1 # Allow users to share recipes with public links
-# allow people to create accounts on your application instance (without an invite link)
+# allow people to create local accounts on your application instance (without an invite link)
+# social accounts will always be able to sign up
# when unset: 0 (false)
# ENABLE_SIGNUP=0
diff --git a/.github/workflows/build-docker-open-data.yml b/.github/workflows/build-docker-open-data.yml
index 881e7824b6..09df195672 100644
--- a/.github/workflows/build-docker-open-data.yml
+++ b/.github/workflows/build-docker-open-data.yml
@@ -64,17 +64,17 @@ jobs:
run: yarn build
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@v3
- name: Set up Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
if: github.secret_source == 'Actions'
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
if: github.secret_source == 'Actions'
with:
registry: ghcr.io
@@ -82,7 +82,7 @@ jobs:
password: ${{ github.token }}
- name: Docker meta
id: meta
- uses: docker/metadata-action@v4
+ uses: docker/metadata-action@v5
with:
images: |
vabene1111/recipes
@@ -97,7 +97,7 @@ jobs:
type=semver,suffix=-open-data-plugin,pattern={{major}}
type=ref,suffix=-open-data-plugin,event=branch
- name: Build and Push
- uses: docker/build-push-action@v4
+ uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.dockerfile }}
diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml
index aea8a52654..8c29b9fefc 100644
--- a/.github/workflows/build-docker.yml
+++ b/.github/workflows/build-docker.yml
@@ -48,17 +48,17 @@ jobs:
run: yarn build
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@v3
- name: Set up Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
if: github.secret_source == 'Actions'
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
if: github.secret_source == 'Actions'
with:
registry: ghcr.io
@@ -66,7 +66,7 @@ jobs:
password: ${{ github.token }}
- name: Docker meta
id: meta
- uses: docker/metadata-action@v4
+ uses: docker/metadata-action@v5
with:
images: |
vabene1111/recipes
@@ -81,7 +81,7 @@ jobs:
type=semver,pattern={{major}}
type=ref,event=branch
- name: Build and Push
- uses: docker/build-push-action@v4
+ uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.dockerfile }}
diff --git a/.gitignore b/.gitignore
index ad9c1fc7f4..553403a5fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,6 +74,7 @@ mediafiles/
\.env
staticfiles/
postgresql/
+data/
/docker-compose.override.yml
diff --git a/boot.sh b/boot.sh
index 0ff1fba164..3faacad710 100644
--- a/boot.sh
+++ b/boot.sh
@@ -19,9 +19,14 @@ if [ ! -f "$NGINX_CONF_FILE" ] && [ $GUNICORN_MEDIA -eq 0 ]; then
display_warning "Nginx configuration file could not be found at the default location!\nPath: ${NGINX_CONF_FILE}"
fi
-# SECRET_KEY must be set in .env file
+# SECRET_KEY (or a valid file at SECRET_KEY_FILE) must be set in .env file
+
+if [ -f "${SECRET_KEY_FILE}" ]; then
+ export SECRET_KEY=$(cat "$SECRET_KEY_FILE")
+fi
+
if [ -z "${SECRET_KEY}" ]; then
- display_warning "The environment variable 'SECRET_KEY' is not set but REQUIRED for running Tandoor!"
+ display_warning "The environment variable 'SECRET_KEY' (or 'SECRET_KEY_FILE' that points to an existing file) is not set but REQUIRED for running Tandoor!"
fi
@@ -30,11 +35,16 @@ echo "Waiting for database to be ready..."
attempt=0
max_attempts=20
-if [ "${DB_ENGINE}" != 'django.db.backends.sqlite3' ]; then
+if [ "${DB_ENGINE}" == 'django.db.backends.postgresql' ] || [ "${DATABASE_URL}" == 'postgres'* ]; then
+
+ # POSTGRES_PASSWORD (or a valid file at POSTGRES_PASSWORD_FILE) must be set in .env file
+
+ if [ -f "${POSTGRES_PASSWORD_FILE}" ]; then
+ export POSTGRES_PASSWORD=$(cat "$POSTGRES_PASSWORD_FILE")
+ fi
- # POSTGRES_PASSWORD must be set in .env file
if [ -z "${POSTGRES_PASSWORD}" ]; then
- display_warning "The environment variable 'POSTGRES_PASSWORD' is not set but REQUIRED for running Tandoor!"
+ display_warning "The environment variable 'POSTGRES_PASSWORD' (or 'POSTGRES_PASSWORD_FILE' that points to an existing file) is not set but REQUIRED for running Tandoor!"
fi
while pg_isready --host=${POSTGRES_HOST} --port=${POSTGRES_PORT} --user=${POSTGRES_USER} -q; status=$?; attempt=$((attempt+1)); [ $status -ne 0 ] && [ $attempt -le $max_attempts ]; do
diff --git a/cookbook/admin.py b/cookbook/admin.py
index 1207b9da5e..9f7df90a63 100644
--- a/cookbook/admin.py
+++ b/cookbook/admin.py
@@ -10,13 +10,13 @@
from cookbook.managers import DICTIONARY
-from .models import (Automation, BookmarkletImport, Comment, CookLog, Food, FoodInheritField,
- ImportLog, Ingredient, InviteLink, Keyword, MealPlan, MealType,
- NutritionInformation, Property, PropertyType, Recipe, RecipeBook,
- RecipeBookEntry, RecipeImport, SearchPreference, ShareLink, ShoppingList,
- ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket,
- SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, TelegramBot,
- Unit, UnitConversion, UserFile, UserPreference, UserSpace, ViewLog)
+from .models import (BookmarkletImport, Comment, CookLog, Food, ImportLog, Ingredient, InviteLink,
+ Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType,
+ Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
+ ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
+ Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
+ TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
+ ViewLog)
class CustomUserAdmin(UserAdmin):
@@ -192,7 +192,7 @@ class RecipeAdmin(admin.ModelAdmin):
def created_by(obj):
return obj.created_by.get_user_display_name()
- if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
+ if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql':
actions = [rebuild_index]
@@ -277,7 +277,7 @@ class RecipeBookEntryAdmin(admin.ModelAdmin):
class MealPlanAdmin(admin.ModelAdmin):
- list_display = ('user', 'recipe', 'meal_type', 'date')
+ list_display = ('user', 'recipe', 'meal_type', 'from_date', 'to_date')
@staticmethod
def user(obj):
diff --git a/cookbook/forms.py b/cookbook/forms.py
index 12bbce4456..36b35ed598 100644
--- a/cookbook/forms.py
+++ b/cookbook/forms.py
@@ -9,8 +9,8 @@
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
from hcaptcha.fields import hCaptchaField
-from .models import (Comment, Food, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook,
- RecipeBookEntry, SearchPreference, Space, Storage, Sync, User, UserPreference)
+from .models import (Comment, Food, InviteLink, Keyword, Recipe, RecipeBook, RecipeBookEntry,
+ SearchPreference, Space, Storage, Sync, User, UserPreference)
class SelectWidget(widgets.Select):
@@ -45,8 +45,7 @@ class Meta:
model = UserPreference
fields = (
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
- 'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed',
- 'show_step_ingredients',
+ 'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed', 'show_step_ingredients',
)
labels = {
@@ -67,21 +66,19 @@ class Meta:
help_texts = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
-
- 'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
+ 'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
'use_fractions': _(
'Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'),
-
- 'use_kj': _('Display nutritional energy amounts in joules instead of calories'),
+ 'use_kj': _('Display nutritional energy amounts in joules instead of calories'),
'plan_share': _('Users with whom newly created meal plans should be shared by default.'),
'shopping_share': _('Users with whom to share shopping lists.'),
- 'ingredient_decimals': _('Number of decimals to round ingredients.'),
- 'comments': _('If you want to be able to create and see comments underneath recipes.'),
+ 'ingredient_decimals': _('Number of decimals to round ingredients.'),
+ 'comments': _('If you want to be able to create and see comments underneath recipes.'),
'shopping_auto_sync': _(
- 'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. 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.'
+ 'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. 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.'
),
- 'sticky_navbar': _('Makes the navbar stick to the top of the page.'),
+ 'sticky_navbar': _('Makes the navbar stick to the top of the page.'),
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
'mealplan_autoexclude_onhand': _('Exclude ingredients that are on hand.'),
'left_handed': _('Will optimize the UI for use with your left hand.'),
@@ -187,6 +184,7 @@ def clean(self, data, initial=None):
result = single_file_clean(data, initial)
return result
+
class ImportForm(ImportExportBase):
files = MultipleFileField(required=True)
duplicates = forms.BooleanField(help_text=_(
@@ -325,50 +323,6 @@ class Meta:
}
-# TODO deprecate
-class MealPlanForm(forms.ModelForm):
- def __init__(self, *args, **kwargs):
- space = kwargs.pop('space')
- super().__init__(*args, **kwargs)
- self.fields['recipe'].queryset = Recipe.objects.filter(space=space).all()
- self.fields['meal_type'].queryset = MealType.objects.filter(space=space).all()
- self.fields['shared'].queryset = User.objects.filter(userpreference__space=space).all()
-
- def clean(self):
- cleaned_data = super(MealPlanForm, self).clean()
-
- if cleaned_data['title'] == '' and cleaned_data['recipe'] is None:
- raise forms.ValidationError(
- _('You must provide at least a recipe or a title.')
- )
-
- return cleaned_data
-
- class Meta:
- model = MealPlan
- fields = (
- 'recipe', 'title', 'meal_type', 'note',
- 'servings', 'date', 'shared'
- )
-
- help_texts = {
- 'shared': _('You can list default users to share recipes with in the settings.'),
- 'note': _('You can use markdown to format this field. See the docs here')
-
- }
-
- widgets = {
- 'recipe': SelectWidget,
- 'date': DateWidget,
- 'shared': MultiSelectWidget
- }
- field_classes = {
- 'recipe': SafeModelChoiceField,
- 'meal_type': SafeModelChoiceField,
- 'shared': SafeModelMultipleChoiceField,
- }
-
-
class InviteLinkForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
@@ -509,8 +463,8 @@ class Meta:
help_texts = {
'shopping_share': _('Users will see all items you add to your shopping list. They must add you to see items on their list.'),
'shopping_auto_sync': _(
- 'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. 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.'
+ 'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. 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.'
),
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
'mealplan_autoinclude_related': _('When adding a meal plan to the shopping list (manually or automatically), include all related recipes.'),
@@ -554,11 +508,10 @@ def __init__(self, *args, **kwargs):
class Meta:
model = Space
- fields = ('food_inherit', 'reset_food_inherit', 'show_facet_count', 'use_plural')
+ fields = ('food_inherit', 'reset_food_inherit', 'use_plural')
help_texts = {
'food_inherit': _('Fields on food that should be inherited by default.'),
- 'show_facet_count': _('Show recipe counts on search filters'),
'use_plural': _('Use the plural form for units and food inside this space.'),
}
diff --git a/cookbook/helper/AllAuthCustomAdapter.py b/cookbook/helper/AllAuthCustomAdapter.py
index 4975251a41..a1affa4ce4 100644
--- a/cookbook/helper/AllAuthCustomAdapter.py
+++ b/cookbook/helper/AllAuthCustomAdapter.py
@@ -1,11 +1,10 @@
import datetime
-
-from django.conf import settings
+from gettext import gettext as _
from allauth.account.adapter import DefaultAccountAdapter
+from django.conf import settings
from django.contrib import messages
from django.core.cache import caches
-from gettext import gettext as _
from cookbook.models import InviteLink
@@ -17,10 +16,13 @@ def is_open_for_signup(self, request):
Whether to allow sign-ups.
"""
signup_token = False
- if 'signup_token' in request.session and InviteLink.objects.filter(valid_until__gte=datetime.datetime.today(), used_by=None, uuid=request.session['signup_token']).exists():
+ if 'signup_token' in request.session and InviteLink.objects.filter(
+ valid_until__gte=datetime.datetime.today(), used_by=None, uuid=request.session['signup_token']).exists():
signup_token = True
- if (request.resolver_match.view_name == 'account_signup' or request.resolver_match.view_name == 'socialaccount_signup') and not settings.ENABLE_SIGNUP and not signup_token:
+ if request.resolver_match.view_name == 'account_signup' and not settings.ENABLE_SIGNUP and not signup_token:
+ return False
+ elif request.resolver_match.view_name == 'socialaccount_signup' and len(settings.SOCIAL_PROVIDERS) < 1:
return False
else:
return super(AllAuthCustomAdapter, self).is_open_for_signup(request)
@@ -33,7 +35,7 @@ def send_mail(self, template_prefix, email, context):
if c == default:
try:
super(AllAuthCustomAdapter, self).send_mail(template_prefix, email, context)
- except Exception: # dont fail signup just because confirmation mail could not be send
+ except Exception: # dont fail signup just because confirmation mail could not be send
pass
else:
messages.add_message(self.request, messages.ERROR, _('In order to prevent spam, the requested email was not send. Please wait a few minutes and try again.'))
diff --git a/cookbook/helper/automation_helper.py b/cookbook/helper/automation_helper.py
new file mode 100644
index 0000000000..a86d405bda
--- /dev/null
+++ b/cookbook/helper/automation_helper.py
@@ -0,0 +1,227 @@
+import re
+
+from django.core.cache import caches
+from django.db.models.functions import Lower
+
+from cookbook.models import Automation
+
+
+class AutomationEngine:
+ request = None
+ source = None
+ use_cache = None
+ food_aliases = None
+ keyword_aliases = None
+ unit_aliases = None
+ never_unit = None
+ transpose_words = None
+ regex_replace = {
+ Automation.DESCRIPTION_REPLACE: None,
+ Automation.INSTRUCTION_REPLACE: None,
+ Automation.FOOD_REPLACE: None,
+ Automation.UNIT_REPLACE: None,
+ Automation.NAME_REPLACE: None,
+ }
+
+ def __init__(self, request, use_cache=True, source=None):
+ self.request = request
+ self.use_cache = use_cache
+ if not source:
+ self.source = "default_string_to_avoid_false_regex_match"
+ else:
+ self.source = source
+
+ def apply_keyword_automation(self, keyword):
+ keyword = keyword.strip()
+ if self.use_cache and self.keyword_aliases is None:
+ self.keyword_aliases = {}
+ KEYWORD_CACHE_KEY = f'automation_keyword_alias_{self.request.space.pk}'
+ if c := caches['default'].get(KEYWORD_CACHE_KEY, None):
+ self.keyword_aliases = c
+ caches['default'].touch(KEYWORD_CACHE_KEY, 30)
+ else:
+ for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.KEYWORD_ALIAS).only('param_1', 'param_2').order_by('order').all():
+ self.keyword_aliases[a.param_1.lower()] = a.param_2
+ caches['default'].set(KEYWORD_CACHE_KEY, self.keyword_aliases, 30)
+ else:
+ self.keyword_aliases = {}
+ if self.keyword_aliases:
+ try:
+ keyword = self.keyword_aliases[keyword.lower()]
+ except KeyError:
+ pass
+ else:
+ if automation := Automation.objects.filter(space=self.request.space, type=Automation.KEYWORD_ALIAS, param_1__iexact=keyword, disabled=False).order_by('order').first():
+ return automation.param_2
+ return keyword
+
+ def apply_unit_automation(self, unit):
+ unit = unit.strip()
+ if self.use_cache and self.unit_aliases is None:
+ self.unit_aliases = {}
+ UNIT_CACHE_KEY = f'automation_unit_alias_{self.request.space.pk}'
+ if c := caches['default'].get(UNIT_CACHE_KEY, None):
+ self.unit_aliases = c
+ caches['default'].touch(UNIT_CACHE_KEY, 30)
+ else:
+ for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all():
+ self.unit_aliases[a.param_1.lower()] = a.param_2
+ caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
+ else:
+ self.unit_aliases = {}
+ if self.unit_aliases:
+ try:
+ unit = self.unit_aliases[unit.lower()]
+ except KeyError:
+ pass
+ else:
+ if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1__iexact=unit, disabled=False).order_by('order').first():
+ return automation.param_2
+ return self.apply_regex_replace_automation(unit, Automation.UNIT_REPLACE)
+
+ def apply_food_automation(self, food):
+ food = food.strip()
+ if self.use_cache and self.food_aliases is None:
+ self.food_aliases = {}
+ FOOD_CACHE_KEY = f'automation_food_alias_{self.request.space.pk}'
+ if c := caches['default'].get(FOOD_CACHE_KEY, None):
+ self.food_aliases = c
+ caches['default'].touch(FOOD_CACHE_KEY, 30)
+ else:
+ for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').order_by('order').all():
+ self.food_aliases[a.param_1.lower()] = a.param_2
+ caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
+ else:
+ self.food_aliases = {}
+
+ if self.food_aliases:
+ try:
+ return self.food_aliases[food.lower()]
+ except KeyError:
+ return food
+ else:
+ if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1__iexact=food, disabled=False).order_by('order').first():
+ return automation.param_2
+ return self.apply_regex_replace_automation(food, Automation.FOOD_REPLACE)
+
+ def apply_never_unit_automation(self, tokens):
+ """
+ Moves a string that should never be treated as a unit to next token and optionally replaced with default unit
+ e.g. NEVER_UNIT: param1: egg, param2: None would modify ['1', 'egg', 'white'] to ['1', '', 'egg', 'white']
+ or NEVER_UNIT: param1: egg, param2: pcs would modify ['1', 'egg', 'yolk'] to ['1', 'pcs', 'egg', 'yolk']
+ :param1 string: string that should never be considered a unit, will be moved to token[2]
+ :param2 (optional) unit as string: will insert unit string into token[1]
+ :return: unit as string (possibly changed by automation)
+ """
+
+ if self.use_cache and self.never_unit is None:
+ self.never_unit = {}
+ NEVER_UNIT_CACHE_KEY = f'automation_never_unit_{self.request.space.pk}'
+ if c := caches['default'].get(NEVER_UNIT_CACHE_KEY, None):
+ self.never_unit = c
+ caches['default'].touch(NEVER_UNIT_CACHE_KEY, 30)
+ else:
+ for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.NEVER_UNIT).only('param_1', 'param_2').order_by('order').all():
+ self.never_unit[a.param_1.lower()] = a.param_2
+ caches['default'].set(NEVER_UNIT_CACHE_KEY, self.never_unit, 30)
+ else:
+ self.never_unit = {}
+
+ new_unit = None
+ alt_unit = self.apply_unit_automation(tokens[1])
+ never_unit = False
+ if self.never_unit:
+ try:
+ new_unit = self.never_unit[tokens[1].lower()]
+ never_unit = True
+ except KeyError:
+ return tokens
+ else:
+ if a := Automation.objects.annotate(param_1_lower=Lower('param_1')).filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1_lower__in=[
+ tokens[1].lower(), alt_unit.lower()], disabled=False).order_by('order').first():
+ new_unit = a.param_2
+ never_unit = True
+
+ if never_unit:
+ tokens.insert(1, new_unit)
+ return tokens
+
+ def apply_transpose_automation(self, string):
+ """
+ If two words (param_1 & param_2) are detected in sequence, swap their position in the ingredient string
+ :param 1: first word to detect
+ :param 2: second word to detect
+ return: new ingredient string
+ """
+ if self.use_cache and self.transpose_words is None:
+ self.transpose_words = {}
+ TRANSPOSE_WORDS_CACHE_KEY = f'automation_transpose_words_{self.request.space.pk}'
+ if c := caches['default'].get(TRANSPOSE_WORDS_CACHE_KEY, None):
+ self.transpose_words = c
+ caches['default'].touch(TRANSPOSE_WORDS_CACHE_KEY, 30)
+ else:
+ i = 0
+ for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.TRANSPOSE_WORDS).only(
+ 'param_1', 'param_2').order_by('order').all()[:512]:
+ self.transpose_words[i] = [a.param_1.lower(), a.param_2.lower()]
+ i += 1
+ caches['default'].set(TRANSPOSE_WORDS_CACHE_KEY, self.transpose_words, 30)
+ else:
+ self.transpose_words = {}
+
+ tokens = [x.lower() for x in string.replace(',', ' ').split()]
+ if self.transpose_words:
+ for key, value in self.transpose_words.items():
+ if value[0] in tokens and value[1] in tokens:
+ string = re.sub(rf"\b({value[0]})\W*({value[1]})\b", r"\2 \1", string, flags=re.IGNORECASE)
+ else:
+ for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False) \
+ .annotate(param_1_lower=Lower('param_1'), param_2_lower=Lower('param_2')) \
+ .filter(param_1_lower__in=tokens, param_2_lower__in=tokens).order_by('order')[:512]:
+ if rule.param_1 in tokens and rule.param_2 in tokens:
+ string = re.sub(rf"\b({rule.param_1})\W*({rule.param_2})\b", r"\2 \1", string, flags=re.IGNORECASE)
+ return string
+
+ def apply_regex_replace_automation(self, string, automation_type):
+ # TODO add warning - maybe on SPACE page? when a max of 512 automations of a specific type is exceeded (ALIAS types excluded?)
+ """
+ Replaces strings in a recipe field that are from a matched source
+ field_type are Automation.type that apply regex replacements
+ Automation.DESCRIPTION_REPLACE
+ Automation.INSTRUCTION_REPLACE
+ Automation.FOOD_REPLACE
+ Automation.UNIT_REPLACE
+ Automation.NAME_REPLACE
+
+ regex replacment utilized the following fields from the Automation model
+ :param 1: source that should apply the automation in regex format ('.*' for all)
+ :param 2: regex pattern to match ()
+ :param 3: replacement string (leave blank to delete)
+ return: new string
+ """
+ if self.use_cache and self.regex_replace[automation_type] is None:
+ self.regex_replace[automation_type] = {}
+ REGEX_REPLACE_CACHE_KEY = f'automation_regex_replace_{self.request.space.pk}'
+ if c := caches['default'].get(REGEX_REPLACE_CACHE_KEY, None):
+ self.regex_replace[automation_type] = c[automation_type]
+ caches['default'].touch(REGEX_REPLACE_CACHE_KEY, 30)
+ else:
+ i = 0
+ for a in Automation.objects.filter(space=self.request.space, disabled=False, type=automation_type).only(
+ 'param_1', 'param_2', 'param_3').order_by('order').all()[:512]:
+ self.regex_replace[automation_type][i] = [a.param_1, a.param_2, a.param_3]
+ i += 1
+ caches['default'].set(REGEX_REPLACE_CACHE_KEY, self.regex_replace, 30)
+ else:
+ self.regex_replace[automation_type] = {}
+
+ if self.regex_replace[automation_type]:
+ for rule in self.regex_replace[automation_type].values():
+ if re.match(rule[0], (self.source)[:512]):
+ string = re.sub(rule[1], rule[2], string, flags=re.IGNORECASE)
+ else:
+ for rule in Automation.objects.filter(space=self.request.space, disabled=False, type=automation_type).only(
+ 'param_1', 'param_2', 'param_3').order_by('order').all()[:512]:
+ if re.match(rule.param_1, (self.source)[:512]):
+ string = re.sub(rule.param_2, rule.param_3, string, flags=re.IGNORECASE)
+ return string
diff --git a/cookbook/helper/image_processing.py b/cookbook/helper/image_processing.py
index 9266141b5c..06d022d704 100644
--- a/cookbook/helper/image_processing.py
+++ b/cookbook/helper/image_processing.py
@@ -1,8 +1,7 @@
import os
-import sys
+from io import BytesIO
from PIL import Image
-from io import BytesIO
def rescale_image_jpeg(image_object, base_width=1020):
@@ -11,7 +10,7 @@ def rescale_image_jpeg(image_object, base_width=1020):
width_percent = (base_width / float(img.size[0]))
height = int((float(img.size[1]) * float(width_percent)))
- img = img.resize((base_width, height), Image.ANTIALIAS)
+ img = img.resize((base_width, height), Image.LANCZOS)
img_bytes = BytesIO()
img.save(img_bytes, 'JPEG', quality=90, optimize=True, icc_profile=icc_profile)
@@ -22,7 +21,7 @@ def rescale_image_png(image_object, base_width=1020):
image_object = Image.open(image_object)
wpercent = (base_width / float(image_object.size[0]))
hsize = int((float(image_object.size[1]) * float(wpercent)))
- img = image_object.resize((base_width, hsize), Image.ANTIALIAS)
+ img = image_object.resize((base_width, hsize), Image.LANCZOS)
im_io = BytesIO()
img.save(im_io, 'PNG', quality=90)
diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py
index 57b70f44c3..f944e4164b 100644
--- a/cookbook/helper/ingredient_parser.py
+++ b/cookbook/helper/ingredient_parser.py
@@ -2,22 +2,16 @@
import string
import unicodedata
-from django.core.cache import caches
-from django.db.models import Q
-from django.db.models.functions import Lower
-
-from cookbook.models import Automation, Food, Ingredient, Unit
+from cookbook.helper.automation_helper import AutomationEngine
+from cookbook.models import Food, Ingredient, Unit
class IngredientParser:
request = None
ignore_rules = False
- food_aliases = {}
- unit_aliases = {}
- never_unit = {}
- transpose_words = {}
+ automation = None
- def __init__(self, request, cache_mode, ignore_automations=False):
+ def __init__(self, request, cache_mode=True, ignore_automations=False):
"""
Initialize ingredient parser
:param request: request context (to control caching, rule ownership, etc.)
@@ -26,87 +20,8 @@ def __init__(self, request, cache_mode, ignore_automations=False):
"""
self.request = request
self.ignore_rules = ignore_automations
- if cache_mode:
- FOOD_CACHE_KEY = f'automation_food_alias_{self.request.space.pk}'
- if c := caches['default'].get(FOOD_CACHE_KEY, None):
- self.food_aliases = c
- caches['default'].touch(FOOD_CACHE_KEY, 30)
- else:
- for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').order_by('order').all():
- self.food_aliases[a.param_1.lower()] = a.param_2
- caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
-
- UNIT_CACHE_KEY = f'automation_unit_alias_{self.request.space.pk}'
- if c := caches['default'].get(UNIT_CACHE_KEY, None):
- self.unit_aliases = c
- caches['default'].touch(UNIT_CACHE_KEY, 30)
- else:
- for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all():
- self.unit_aliases[a.param_1.lower()] = a.param_2
- caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
-
- NEVER_UNIT_CACHE_KEY = f'automation_never_unit_{self.request.space.pk}'
- if c := caches['default'].get(NEVER_UNIT_CACHE_KEY, None):
- self.never_unit = c
- caches['default'].touch(NEVER_UNIT_CACHE_KEY, 30)
- else:
- for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.NEVER_UNIT).only('param_1', 'param_2').order_by('order').all():
- self.never_unit[a.param_1.lower()] = a.param_2
- caches['default'].set(NEVER_UNIT_CACHE_KEY, self.never_unit, 30)
-
- TRANSPOSE_WORDS_CACHE_KEY = f'automation_transpose_words_{self.request.space.pk}'
- if c := caches['default'].get(TRANSPOSE_WORDS_CACHE_KEY, None):
- self.transpose_words = c
- caches['default'].touch(TRANSPOSE_WORDS_CACHE_KEY, 30)
- else:
- i = 0
- for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.TRANSPOSE_WORDS).only('param_1', 'param_2').order_by('order').all():
- self.transpose_words[i] = [a.param_1.lower(), a.param_2.lower()]
- i += 1
- caches['default'].set(TRANSPOSE_WORDS_CACHE_KEY, self.transpose_words, 30)
- else:
- self.food_aliases = {}
- self.unit_aliases = {}
- self.never_unit = {}
- self.transpose_words = {}
-
- def apply_food_automation(self, food):
- """
- Apply food alias automations to passed food
- :param food: unit as string
- :return: food as string (possibly changed by automation)
- """
- if self.ignore_rules:
- return food
- else:
- if self.food_aliases:
- try:
- return self.food_aliases[food.lower()]
- except KeyError:
- return food
- else:
- if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1__iexact=food, disabled=False).order_by('order').first():
- return automation.param_2
- return food
-
- def apply_unit_automation(self, unit):
- """
- Apply unit alias automations to passed unit
- :param unit: unit as string
- :return: unit as string (possibly changed by automation)
- """
- if self.ignore_rules:
- return unit
- else:
- if self.transpose_words:
- try:
- return self.unit_aliases[unit.lower()]
- except KeyError:
- return unit
- else:
- if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1__iexact=unit, disabled=False).order_by('order').first():
- return automation.param_2
- return unit
+ if not self.ignore_rules:
+ self.automation = AutomationEngine(self.request, use_cache=cache_mode)
def get_unit(self, unit):
"""
@@ -117,7 +32,10 @@ def get_unit(self, unit):
if not unit:
return None
if len(unit) > 0:
- u, created = Unit.objects.get_or_create(name=self.apply_unit_automation(unit), space=self.request.space)
+ if self.ignore_rules:
+ u, created = Unit.objects.get_or_create(name=unit.strip(), space=self.request.space)
+ else:
+ u, created = Unit.objects.get_or_create(name=self.automation.apply_unit_automation(unit), space=self.request.space)
return u
return None
@@ -130,7 +48,10 @@ def get_food(self, food):
if not food:
return None
if len(food) > 0:
- f, created = Food.objects.get_or_create(name=self.apply_food_automation(food), space=self.request.space)
+ if self.ignore_rules:
+ f, created = Food.objects.get_or_create(name=food.strip(), space=self.request.space)
+ else:
+ f, created = Food.objects.get_or_create(name=self.automation.apply_food_automation(food), space=self.request.space)
return f
return None
@@ -232,67 +153,6 @@ def parse_food(self, tokens):
food, note = self.parse_food_with_comma(tokens)
return food, note
- def apply_never_unit_automations(self, tokens):
- """
- Moves a string that should never be treated as a unit to next token and optionally replaced with default unit
- e.g. NEVER_UNIT: param1: egg, param2: None would modify ['1', 'egg', 'white'] to ['1', '', 'egg', 'white']
- or NEVER_UNIT: param1: egg, param2: pcs would modify ['1', 'egg', 'yolk'] to ['1', 'pcs', 'egg', 'yolk']
- :param1 string: string that should never be considered a unit, will be moved to token[2]
- :param2 (optional) unit as string: will insert unit string into token[1]
- :return: unit as string (possibly changed by automation)
- """
-
- if self.ignore_rules:
- return tokens
-
- new_unit = None
- alt_unit = self.apply_unit_automation(tokens[1])
- never_unit = False
- if self.never_unit:
- try:
- new_unit = self.never_unit[tokens[1].lower()]
- never_unit = True
- except KeyError:
- return tokens
-
- else:
- if automation := Automation.objects.annotate(param_1_lower=Lower('param_1')).filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1_lower__in=[
- tokens[1].lower(), alt_unit.lower()], disabled=False).order_by('order').first():
- new_unit = automation.param_2
- never_unit = True
-
- if never_unit:
- tokens.insert(1, new_unit)
-
- return tokens
-
- def apply_transpose_words_automations(self, ingredient):
- """
- If two words (param_1 & param_2) are detected in sequence, swap their position in the ingredient string
- :param 1: first word to detect
- :param 2: second word to detect
- return: new ingredient string
- """
-
- if self.ignore_rules:
- return ingredient
-
- else:
- tokens = [x.lower() for x in ingredient.replace(',', ' ').split()]
- if self.transpose_words:
- filtered_rules = {}
- for key, value in self.transpose_words.items():
- if value[0] in tokens and value[1] in tokens:
- filtered_rules[key] = value
- for k, v in filtered_rules.items():
- ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient, flags=re.IGNORECASE)
- else:
- for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False) \
- .annotate(param_1_lower=Lower('param_1'), param_2_lower=Lower('param_2')) \
- .filter(Q(Q(param_1_lower__in=tokens) | Q(param_2_lower__in=tokens))).order_by('order'):
- ingredient = re.sub(rf"\b({rule.param_1})\W*({rule.param_1})\b", r"\2 \1", ingredient, flags=re.IGNORECASE)
- return ingredient
-
def parse(self, ingredient):
"""
Main parsing function, takes an ingredient string (e.g. '1 l Water') and extracts amount, unit, food, ...
@@ -333,7 +193,8 @@ def parse(self, ingredient):
if re.match('([0-9])+([A-z])+\\s', ingredient):
ingredient = re.sub(r'(?<=([a-z])|\d)(?=(?(1)\d|[a-z]))', ' ', ingredient)
- ingredient = self.apply_transpose_words_automations(ingredient)
+ if not self.ignore_rules:
+ ingredient = self.automation.apply_transpose_automation(ingredient)
tokens = ingredient.split() # split at each space into tokens
if len(tokens) == 1:
@@ -347,7 +208,8 @@ def parse(self, ingredient):
# three arguments if it already has a unit there can't be
# a fraction for the amount
if len(tokens) > 2:
- tokens = self.apply_never_unit_automations(tokens)
+ if not self.ignore_rules:
+ tokens = self.automation.apply_never_unit_automation(tokens)
try:
if unit is not None:
# a unit is already found, no need to try the second argument for a fraction
@@ -394,10 +256,11 @@ def parse(self, ingredient):
if unit_note not in note:
note += ' ' + unit_note
- if unit:
- unit = self.apply_unit_automation(unit.strip())
+ if unit and not self.ignore_rules:
+ unit = self.automation.apply_unit_automation(unit)
- food = self.apply_food_automation(food.strip())
+ if food and not self.ignore_rules:
+ food = self.automation.apply_food_automation(food)
if len(food) > Food._meta.get_field('name').max_length: # test if food name is to long
# try splitting it at a space and taking only the first arg
if len(food.split()) > 1 and len(food.split()[0]) < Food._meta.get_field('name').max_length:
diff --git a/cookbook/helper/open_data_importer.py b/cookbook/helper/open_data_importer.py
index 2644f1f98c..e709f2f558 100644
--- a/cookbook/helper/open_data_importer.py
+++ b/cookbook/helper/open_data_importer.py
@@ -1,6 +1,5 @@
-from django.db.models import Q
-
-from cookbook.models import Unit, SupermarketCategory, Property, PropertyType, Supermarket, SupermarketCategoryRelation, Food, Automation, UnitConversion, FoodProperty
+from cookbook.models import (Food, FoodProperty, Property, PropertyType, Supermarket,
+ SupermarketCategory, SupermarketCategoryRelation, Unit, UnitConversion)
class OpenDataImporter:
@@ -33,7 +32,8 @@ def import_units(self):
))
if self.update_existing:
- return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('name', 'plural_name', 'base_unit', 'open_data_slug'), unique_fields=('space', 'name',))
+ return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=(
+ 'name', 'plural_name', 'base_unit', 'open_data_slug'), unique_fields=('space', 'name',))
else:
return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('open_data_slug',), unique_fields=('space', 'name',))
@@ -116,27 +116,25 @@ def import_food(self):
self._update_slug_cache(Unit, 'unit')
self._update_slug_cache(PropertyType, 'property')
- # pref_unit_key = 'preferred_unit_metric'
- # pref_shopping_unit_key = 'preferred_packaging_unit_metric'
- # if not self.use_metric:
- # pref_unit_key = 'preferred_unit_imperial'
- # pref_shopping_unit_key = 'preferred_packaging_unit_imperial'
-
insert_list = []
+ insert_list_flat = []
update_list = []
update_field_list = []
for k in list(self.data[datatype].keys()):
if not (self.data[datatype][k]['name'] in existing_objects_flat or self.data[datatype][k]['plural_name'] in existing_objects_flat):
- insert_list.append({'data': {
- 'name': self.data[datatype][k]['name'],
- 'plural_name': self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
- # 'preferred_unit_id': self.slug_id_cache['unit'][self.data[datatype][k][pref_unit_key]],
- # 'preferred_shopping_unit_id': self.slug_id_cache['unit'][self.data[datatype][k][pref_shopping_unit_key]],
- 'supermarket_category_id': self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
- 'fdc_id': self.data[datatype][k]['fdc_id'] if self.data[datatype][k]['fdc_id'] != '' else None,
- 'open_data_slug': k,
- 'space': self.request.space.id,
- }})
+ if not (self.data[datatype][k]['name'] in insert_list_flat or self.data[datatype][k]['plural_name'] in insert_list_flat):
+ insert_list.append({'data': {
+ 'name': self.data[datatype][k]['name'],
+ 'plural_name': self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
+ 'supermarket_category_id': self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
+ 'fdc_id': self.data[datatype][k]['fdc_id'] if self.data[datatype][k]['fdc_id'] != '' else None,
+ 'open_data_slug': k,
+ 'space': self.request.space.id,
+ }})
+ # build a fake second flat array to prevent duplicate foods from being inserted.
+ # trying to insert a duplicate would throw a db error :(
+ insert_list_flat.append(self.data[datatype][k]['name'])
+ insert_list_flat.append(self.data[datatype][k]['plural_name'])
else:
if self.data[datatype][k]['name'] in existing_objects:
existing_food_id = existing_objects[self.data[datatype][k]['name']][0]
@@ -149,8 +147,6 @@ def import_food(self):
id=existing_food_id,
name=self.data[datatype][k]['name'],
plural_name=self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
- # preferred_unit_id=self.slug_id_cache['unit'][self.data[datatype][k][pref_unit_key]],
- # preferred_shopping_unit_id=self.slug_id_cache['unit'][self.data[datatype][k][pref_shopping_unit_key]],
supermarket_category_id=self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
fdc_id=self.data[datatype][k]['fdc_id'] if self.data[datatype][k]['fdc_id'] != '' else None,
open_data_slug=k,
@@ -166,23 +162,20 @@ def import_food(self):
self._update_slug_cache(Food, 'food')
food_property_list = []
- alias_list = []
+ # alias_list = []
+
for k in list(self.data[datatype].keys()):
for fp in self.data[datatype][k]['properties']['type_values']:
- food_property_list.append(Property(
- property_type_id=self.slug_id_cache['property'][fp['property_type']],
- property_amount=fp['property_value'],
- import_food_id=self.slug_id_cache['food'][k],
- space=self.request.space,
- ))
-
- # for a in self.data[datatype][k]['alias']:
- # alias_list.append(Automation(
- # param_1=a,
- # param_2=self.data[datatype][k]['name'],
- # space=self.request.space,
- # created_by=self.request.user,
- # ))
+ # try catch here because somettimes key "k" is not set for he food cache
+ try:
+ food_property_list.append(Property(
+ property_type_id=self.slug_id_cache['property'][fp['property_type']],
+ property_amount=fp['property_value'],
+ import_food_id=self.slug_id_cache['food'][k],
+ space=self.request.space,
+ ))
+ except KeyError:
+ print(str(k) + ' is not in self.slug_id_cache["food"]')
Property.objects.bulk_create(food_property_list, ignore_conflicts=True, unique_fields=('space', 'import_food_id', 'property_type',))
@@ -192,7 +185,6 @@ def import_food(self):
FoodProperty.objects.bulk_create(property_food_relation_list, ignore_conflicts=True, unique_fields=('food_id', 'property_id',))
- # Automation.objects.bulk_create(alias_list, ignore_conflicts=True, unique_fields=('space', 'param_1', 'param_2',))
return insert_list + update_list
def import_conversion(self):
@@ -200,15 +192,19 @@ def import_conversion(self):
insert_list = []
for k in list(self.data[datatype].keys()):
- insert_list.append(UnitConversion(
- base_amount=self.data[datatype][k]['base_amount'],
- base_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['base_unit']],
- converted_amount=self.data[datatype][k]['converted_amount'],
- converted_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['converted_unit']],
- food_id=self.slug_id_cache['food'][self.data[datatype][k]['food']],
- open_data_slug=k,
- space=self.request.space,
- created_by=self.request.user,
- ))
+ # try catch here because sometimes key "k" is not set for he food cache
+ try:
+ insert_list.append(UnitConversion(
+ base_amount=self.data[datatype][k]['base_amount'],
+ base_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['base_unit']],
+ converted_amount=self.data[datatype][k]['converted_amount'],
+ converted_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['converted_unit']],
+ food_id=self.slug_id_cache['food'][self.data[datatype][k]['food']],
+ open_data_slug=k,
+ space=self.request.space,
+ created_by=self.request.user,
+ ))
+ except KeyError:
+ print(str(k) + ' is not in self.slug_id_cache["food"]')
return UnitConversion.objects.bulk_create(insert_list, ignore_conflicts=True, unique_fields=('space', 'base_unit', 'converted_unit', 'food', 'open_data_slug'))
diff --git a/cookbook/helper/permission_helper.py b/cookbook/helper/permission_helper.py
index d45f3e2fd1..889d75055b 100644
--- a/cookbook/helper/permission_helper.py
+++ b/cookbook/helper/permission_helper.py
@@ -4,16 +4,16 @@
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.core.cache import cache
-from django.core.exceptions import ValidationError, ObjectDoesNotExist
+from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.http import HttpResponseRedirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _
-from oauth2_provider.contrib.rest_framework import TokenHasScope, TokenHasReadWriteScope
+from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope
from oauth2_provider.models import AccessToken
from rest_framework import permissions
from rest_framework.permissions import SAFE_METHODS
-from cookbook.models import ShareLink, Recipe, UserSpace
+from cookbook.models import Recipe, ShareLink, UserSpace
def get_allowed_groups(groups_required):
@@ -255,9 +255,6 @@ def has_permission(self, request, view):
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
- # # temporary hack to make old shopping list work with new shopping list
- # if obj.__class__.__name__ in ['ShoppingList', 'ShoppingListEntry']:
- # return is_object_shared(request.user, obj) or obj.created_by in list(request.user.get_shopping_share())
return is_object_shared(request.user, obj)
@@ -322,7 +319,8 @@ class CustomRecipePermission(permissions.BasePermission):
def has_permission(self, request, view): # user is either at least a guest or a share link is given and the request is safe
share = request.query_params.get('share', None)
- return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS) or has_group_permission(request.user, ['user'])) or (share and request.method in SAFE_METHODS and 'pk' in view.kwargs)
+ return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS) or has_group_permission(
+ request.user, ['user'])) or (share and request.method in SAFE_METHODS and 'pk' in view.kwargs)
def has_object_permission(self, request, view, obj):
share = request.query_params.get('share', None)
@@ -332,7 +330,8 @@ def has_object_permission(self, request, view, obj):
if obj.private:
return ((obj.created_by == request.user) or (request.user in obj.shared.all())) and obj.space == request.space
else:
- return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS) or has_group_permission(request.user, ['user'])) and obj.space == request.space
+ return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS)
+ or has_group_permission(request.user, ['user'])) and obj.space == request.space
class CustomUserPermission(permissions.BasePermission):
@@ -361,7 +360,7 @@ class CustomTokenHasScope(TokenHasScope):
"""
def has_permission(self, request, view):
- if type(request.auth) == AccessToken:
+ if isinstance(request.auth, AccessToken):
return super().has_permission(request, view)
else:
return request.user.is_authenticated
@@ -375,7 +374,7 @@ class CustomTokenHasReadWriteScope(TokenHasReadWriteScope):
"""
def has_permission(self, request, view):
- if type(request.auth) == AccessToken:
+ if isinstance(request.auth, AccessToken):
return super().has_permission(request, view)
else:
return True
diff --git a/cookbook/helper/property_helper.py b/cookbook/helper/property_helper.py
index bff69aaab3..04521c6582 100644
--- a/cookbook/helper/property_helper.py
+++ b/cookbook/helper/property_helper.py
@@ -2,7 +2,7 @@
from cookbook.helper.cache_helper import CacheHelper
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
-from cookbook.models import PropertyType, Unit, Food, Property, Recipe, Step
+from cookbook.models import PropertyType
class FoodPropertyHelper:
@@ -31,10 +31,12 @@ def calculate_recipe_properties(self, recipe):
if not property_types:
property_types = PropertyType.objects.filter(space=self.space).all()
- caches['default'].set(CacheHelper(self.space).PROPERTY_TYPE_CACHE_KEY, property_types, 60 * 60) # cache is cleared on property type save signal so long duration is fine
+ # cache is cleared on property type save signal so long duration is fine
+ caches['default'].set(CacheHelper(self.space).PROPERTY_TYPE_CACHE_KEY, property_types, 60 * 60)
for fpt in property_types:
- computed_properties[fpt.id] = {'id': fpt.id, 'name': fpt.name, 'icon': fpt.icon, 'description': fpt.description, 'unit': fpt.unit, 'order': fpt.order, 'food_values': {}, 'total_value': 0, 'missing_value': False}
+ computed_properties[fpt.id] = {'id': fpt.id, 'name': fpt.name, 'description': fpt.description,
+ 'unit': fpt.unit, 'order': fpt.order, 'food_values': {}, 'total_value': 0, 'missing_value': False}
uch = UnitConversionHelper(self.space)
@@ -53,7 +55,8 @@ def calculate_recipe_properties(self, recipe):
if c.unit == i.food.properties_food_unit:
found_property = True
computed_properties[pt.id]['total_value'] += (c.amount / i.food.properties_food_amount) * p.property_amount
- computed_properties[pt.id]['food_values'] = self.add_or_create(computed_properties[p.property_type.id]['food_values'], c.food.id, (c.amount / i.food.properties_food_amount) * p.property_amount, c.food)
+ computed_properties[pt.id]['food_values'] = self.add_or_create(
+ computed_properties[p.property_type.id]['food_values'], c.food.id, (c.amount / i.food.properties_food_amount) * p.property_amount, c.food)
if not found_property:
computed_properties[pt.id]['missing_value'] = True
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': 0}
diff --git a/cookbook/helper/recipe_html_import.py b/cookbook/helper/recipe_html_import.py
deleted file mode 100644
index 95f115b76f..0000000000
--- a/cookbook/helper/recipe_html_import.py
+++ /dev/null
@@ -1,191 +0,0 @@
-# import json
-# import re
-# from json import JSONDecodeError
-# from urllib.parse import unquote
-
-# from bs4 import BeautifulSoup
-# from bs4.element import Tag
-# from recipe_scrapers import scrape_html, scrape_me
-# from recipe_scrapers._exceptions import NoSchemaFoundInWildMode
-# from recipe_scrapers._utils import get_host_name, normalize_string
-
-# from cookbook.helper import recipe_url_import as helper
-# from cookbook.helper.scrapers.scrapers import text_scraper
-
-
-# def get_recipe_from_source(text, url, request):
-# def build_node(k, v):
-# if isinstance(v, dict):
-# node = {
-# 'name': k,
-# 'value': k,
-# 'children': get_children_dict(v)
-# }
-# elif isinstance(v, list):
-# node = {
-# 'name': k,
-# 'value': k,
-# 'children': get_children_list(v)
-# }
-# else:
-# node = {
-# 'name': k + ": " + normalize_string(str(v)),
-# 'value': normalize_string(str(v))
-# }
-# return node
-
-# def get_children_dict(children):
-# kid_list = []
-# for k, v in children.items():
-# kid_list.append(build_node(k, v))
-# return kid_list
-
-# def get_children_list(children):
-# kid_list = []
-# for kid in children:
-# if type(kid) == list:
-# node = {
-# 'name': "unknown list",
-# 'value': "unknown list",
-# 'children': get_children_list(kid)
-# }
-# kid_list.append(node)
-# elif type(kid) == dict:
-# for k, v in kid.items():
-# kid_list.append(build_node(k, v))
-# else:
-# kid_list.append({
-# 'name': normalize_string(str(kid)),
-# 'value': normalize_string(str(kid))
-# })
-# return kid_list
-
-# recipe_tree = []
-# parse_list = []
-# soup = BeautifulSoup(text, "html.parser")
-# html_data = get_from_html(soup)
-# images = get_images_from_source(soup, url)
-# text = unquote(text)
-# scrape = None
-
-# if url and not text:
-# try:
-# scrape = scrape_me(url_path=url, wild_mode=True)
-# except(NoSchemaFoundInWildMode):
-# pass
-
-# if not scrape:
-# try:
-# parse_list.append(remove_graph(json.loads(text)))
-# if not url and 'url' in parse_list[0]:
-# url = parse_list[0]['url']
-# scrape = text_scraper("", url=url)
-
-# except JSONDecodeError:
-# for el in soup.find_all('script', type='application/ld+json'):
-# el = remove_graph(el)
-# if not url and 'url' in el:
-# url = el['url']
-# if type(el) == list:
-# for le in el:
-# parse_list.append(le)
-# elif type(el) == dict:
-# parse_list.append(el)
-# for el in soup.find_all(type='application/json'):
-# el = remove_graph(el)
-# if type(el) == list:
-# for le in el:
-# parse_list.append(le)
-# elif type(el) == dict:
-# parse_list.append(el)
-# scrape = text_scraper(text, url=url)
-
-# recipe_json = helper.get_from_scraper(scrape, request)
-
-# # TODO: DEPRECATE recipe_tree & html_data. first validate it isn't used anywhere
-# for el in parse_list:
-# temp_tree = []
-# if isinstance(el, Tag):
-# try:
-# el = json.loads(el.string)
-# except TypeError:
-# continue
-
-# for k, v in el.items():
-# if isinstance(v, dict):
-# node = {
-# 'name': k,
-# 'value': k,
-# 'children': get_children_dict(v)
-# }
-# elif isinstance(v, list):
-# node = {
-# 'name': k,
-# 'value': k,
-# 'children': get_children_list(v)
-# }
-# else:
-# node = {
-# 'name': k + ": " + normalize_string(str(v)),
-# 'value': normalize_string(str(v))
-# }
-# temp_tree.append(node)
-
-# if '@type' in el and el['@type'] == 'Recipe':
-# recipe_tree += [{'name': 'ld+json', 'children': temp_tree}]
-# else:
-# recipe_tree += [{'name': 'json', 'children': temp_tree}]
-
-# return recipe_json, recipe_tree, html_data, images
-
-
-# def get_from_html(soup):
-# INVISIBLE_ELEMS = ('style', 'script', 'head', 'title')
-# html = []
-# for s in soup.strings:
-# if ((s.parent.name not in INVISIBLE_ELEMS) and (len(s.strip()) > 0)):
-# html.append(s)
-# return html
-
-
-# def get_images_from_source(soup, url):
-# sources = ['src', 'srcset', 'data-src']
-# images = []
-# img_tags = soup.find_all('img')
-# if url:
-# site = get_host_name(url)
-# prot = url.split(':')[0]
-
-# urls = []
-# for img in img_tags:
-# for src in sources:
-# try:
-# urls.append(img[src])
-# except KeyError:
-# pass
-
-# for u in urls:
-# u = u.split('?')[0]
-# filename = re.search(r'/([\w_-]+[.](jpg|jpeg|gif|png))$', u)
-# if filename:
-# if (('http' not in u) and (url)):
-# # sometimes an image source can be relative
-# # if it is provide the base url
-# u = '{}://{}{}'.format(prot, site, u)
-# if 'http' in u:
-# images.append(u)
-# return images
-
-
-# def remove_graph(el):
-# # recipes type might be wrapped in @graph type
-# if isinstance(el, Tag):
-# try:
-# el = json.loads(el.string)
-# if '@graph' in el:
-# for x in el['@graph']:
-# if '@type' in x and x['@type'] == 'Recipe':
-# el = x
-# except (TypeError, JSONDecodeError):
-# pass
-# return el
diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py
index 9481520997..2b70808e31 100644
--- a/cookbook/helper/recipe_search.py
+++ b/cookbook/helper/recipe_search.py
@@ -1,14 +1,11 @@
import json
-from collections import Counter
from datetime import date, timedelta
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
-from django.core.cache import cache, caches
-from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value,
- When)
+from django.core.cache import cache
+from django.db.models import Avg, Case, Count, Exists, F, Max, OuterRef, Q, Subquery, Value, When
from django.db.models.functions import Coalesce, Lower, Substr
from django.utils import timezone, translation
-from django.utils.translation import gettext as _
from cookbook.helper.HelperFunctions import Round, str2bool
from cookbook.managers import DICTIONARY
@@ -17,18 +14,19 @@
from recipes import settings
-# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
class RecipeSearch():
- _postgres = settings.DATABASES['default']['ENGINE'] in [
- 'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
+ _postgres = settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'
def __init__(self, request, **params):
self._request = request
self._queryset = None
if f := params.get('filter', None):
- custom_filter = CustomFilter.objects.filter(id=f, space=self._request.space).filter(Q(created_by=self._request.user) |
- Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first()
+ custom_filter = (
+ CustomFilter.objects.filter(id=f, space=self._request.space)
+ .filter(Q(created_by=self._request.user) | Q(shared=self._request.user) | Q(recipebook__shared=self._request.user))
+ .first()
+ )
if custom_filter:
self._params = {**json.loads(custom_filter.search)}
self._original_params = {**(params or {})}
@@ -101,24 +99,18 @@ def __init__(self, request, **params):
self._search_type = self._search_prefs.search or 'plain'
if self._string:
if self._postgres:
- self._unaccent_include = self._search_prefs.unaccent.values_list(
- 'field', flat=True)
+ self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
else:
self._unaccent_include = []
- self._icontains_include = [
- x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
- self._istartswith_include = [
- x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
+ self._icontains_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
+ self._istartswith_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
self._trigram_include = None
self._fulltext_include = None
self._trigram = False
if self._postgres and self._string:
- self._language = DICTIONARY.get(
- translation.get_language(), 'simple')
- self._trigram_include = [
- x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
- self._fulltext_include = self._search_prefs.fulltext.values_list(
- 'field', flat=True) or None
+ self._language = DICTIONARY.get(translation.get_language(), 'simple')
+ self._trigram_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
+ self._fulltext_include = self._search_prefs.fulltext.values_list('field', flat=True) or None
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
self._trigram = True
@@ -169,7 +161,7 @@ def _build_sort_order(self):
else:
order = []
# TODO add userpreference for default sort order and replace '-favorite'
- default_order = ['-name']
+ default_order = ['name']
# recent and new_recipe are always first; they float a few recipes to the top
if self._num_recent:
order += ['-recent']
@@ -178,7 +170,6 @@ def _build_sort_order(self):
# if a sort order is provided by user - use that order
if self._sort_order:
-
if not isinstance(self._sort_order, list):
order += [self._sort_order]
else:
@@ -218,24 +209,18 @@ def string_filters(self, string=None):
self._queryset = self._queryset.filter(query_filter).distinct()
if self._fulltext_include:
if self._fuzzy_match is None:
- self._queryset = self._queryset.annotate(
- score=Coalesce(Max(self.search_rank), 0.0))
+ self._queryset = self._queryset.annotate(score=Coalesce(Max(self.search_rank), 0.0))
else:
- self._queryset = self._queryset.annotate(
- rank=Coalesce(Max(self.search_rank), 0.0))
+ self._queryset = self._queryset.annotate(rank=Coalesce(Max(self.search_rank), 0.0))
if self._fuzzy_match is not None:
- simularity = self._fuzzy_match.filter(
- pk=OuterRef('pk')).values('simularity')
+ simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity')
if not self._fulltext_include:
- self._queryset = self._queryset.annotate(
- score=Coalesce(Subquery(simularity), 0.0))
+ self._queryset = self._queryset.annotate(score=Coalesce(Subquery(simularity), 0.0))
else:
- self._queryset = self._queryset.annotate(
- simularity=Coalesce(Subquery(simularity), 0.0))
+ self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
- self._queryset = self._queryset.annotate(
- score=F('rank') + F('simularity'))
+ self._queryset = self._queryset.annotate(score=F('rank') + F('simularity'))
else:
query_filter = Q()
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
@@ -244,78 +229,69 @@ def string_filters(self, string=None):
def _cooked_on_filter(self, cooked_date=None):
if self._sort_includes('lastcooked') or cooked_date:
- lessthan = self._sort_includes(
- '-lastcooked') or '-' in (cooked_date or [])[:1]
+ lessthan = self._sort_includes('-lastcooked') or '-' in (cooked_date or [])[:1]
if lessthan:
default = timezone.now() - timedelta(days=100000)
else:
default = timezone.now()
- self._queryset = self._queryset.annotate(lastcooked=Coalesce(
- Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default)))
+ self._queryset = self._queryset.annotate(
+ lastcooked=Coalesce(Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default))
+ )
if cooked_date is None:
return
- cooked_date = date(*[int(x)
- for x in cooked_date.split('-') if x != ''])
+ cooked_date = date(*[int(x)for x in cooked_date.split('-') if x != ''])
if lessthan:
- self._queryset = self._queryset.filter(
- lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
+ self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
else:
- self._queryset = self._queryset.filter(
- lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
+ self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
def _created_on_filter(self, created_date=None):
if created_date is None:
return
lessthan = '-' in created_date[:1]
- created_date = date(*[int(x)
- for x in created_date.split('-') if x != ''])
+ created_date = date(*[int(x) for x in created_date.split('-') if x != ''])
if lessthan:
- self._queryset = self._queryset.filter(
- created_at__date__lte=created_date)
+ self._queryset = self._queryset.filter(created_at__date__lte=created_date)
else:
- self._queryset = self._queryset.filter(
- created_at__date__gte=created_date)
+ self._queryset = self._queryset.filter(created_at__date__gte=created_date)
def _updated_on_filter(self, updated_date=None):
if updated_date is None:
return
lessthan = '-' in updated_date[:1]
- updated_date = date(*[int(x)
- for x in updated_date.split('-') if x != ''])
+ updated_date = date(*[int(x)for x in updated_date.split('-') if x != ''])
if lessthan:
- self._queryset = self._queryset.filter(
- updated_at__date__lte=updated_date)
+ self._queryset = self._queryset.filter(updated_at__date__lte=updated_date)
else:
- self._queryset = self._queryset.filter(
- updated_at__date__gte=updated_date)
+ self._queryset = self._queryset.filter(updated_at__date__gte=updated_date)
def _viewed_on_filter(self, viewed_date=None):
if self._sort_includes('lastviewed') or viewed_date:
longTimeAgo = timezone.now() - timedelta(days=100000)
- self._queryset = self._queryset.annotate(lastviewed=Coalesce(
- Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo)))
+ self._queryset = self._queryset.annotate(
+ lastviewed=Coalesce(Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo))
+ )
if viewed_date is None:
return
lessthan = '-' in viewed_date[:1]
- viewed_date = date(*[int(x)
- for x in viewed_date.split('-') if x != ''])
+ viewed_date = date(*[int(x)for x in viewed_date.split('-') if x != ''])
if lessthan:
- self._queryset = self._queryset.filter(
- lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
+ self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
else:
- self._queryset = self._queryset.filter(
- lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
+ self._queryset = self._queryset.filter(lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
def _new_recipes(self, new_days=7):
# TODO make new days a user-setting
if not self._new:
return
- self._queryset = (
- self._queryset.annotate(new_recipe=Case(
- When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')), default=Value(0), ))
+ self._queryset = self._queryset.annotate(
+ new_recipe=Case(
+ When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')),
+ default=Value(0),
+ )
)
def _recently_viewed(self, num_recent=None):
@@ -325,34 +301,35 @@ def _recently_viewed(self, num_recent=None):
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__pk'))), Value(0)))
return
- num_recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space).values(
- 'recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
- self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(
- pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
+ num_recent_recipes = (
+ ViewLog.objects.filter(created_by=self._request.user, space=self._request.space)
+ .values('recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
+ )
+ self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
def _favorite_recipes(self, times_cooked=None):
if self._sort_includes('favorite') or times_cooked:
- less_than = '-' in (times_cooked or []
- ) and not self._sort_includes('-favorite')
+ less_than = '-' in (times_cooked or []) and not self._sort_includes('-favorite')
if less_than:
default = 1000
else:
default = 0
- favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk')
- ).values('recipe').annotate(count=Count('pk', distinct=True)).values('count')
- self._queryset = self._queryset.annotate(
- favorite=Coalesce(Subquery(favorite_recipes), default))
+ favorite_recipes = (
+ CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk'))
+ .values('recipe')
+ .annotate(count=Count('pk', distinct=True))
+ .values('count')
+ )
+ self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
if times_cooked is None:
return
if times_cooked == '0':
self._queryset = self._queryset.filter(favorite=0)
elif less_than:
- self._queryset = self._queryset.filter(favorite__lte=int(
- times_cooked.replace('-', ''))).exclude(favorite=0)
+ self._queryset = self._queryset.filter(favorite__lte=int(times_cooked.replace('-', ''))).exclude(favorite=0)
else:
- self._queryset = self._queryset.filter(
- favorite__gte=int(times_cooked))
+ self._queryset = self._queryset.filter(favorite__gte=int(times_cooked))
def keyword_filters(self, **kwargs):
if all([kwargs[x] is None for x in kwargs]):
@@ -385,8 +362,7 @@ def keyword_filters(self, **kwargs):
else:
self._queryset = self._queryset.filter(f_and)
if 'not' in kw_filter:
- self._queryset = self._queryset.exclude(
- id__in=recipes.values('id'))
+ self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
def food_filters(self, **kwargs):
if all([kwargs[x] is None for x in kwargs]):
@@ -400,8 +376,7 @@ def food_filters(self, **kwargs):
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
if 'or' in fd_filter:
if self._include_children:
- f_or = Q(
- steps__ingredients__food__in=Food.include_descendants(foods))
+ f_or = Q(steps__ingredients__food__in=Food.include_descendants(foods))
else:
f_or = Q(steps__ingredients__food__in=foods)
@@ -413,8 +388,7 @@ def food_filters(self, **kwargs):
recipes = Recipe.objects.all()
for food in foods:
if self._include_children:
- f_and = Q(
- steps__ingredients__food__in=food.get_descendants_and_self())
+ f_and = Q(steps__ingredients__food__in=food.get_descendants_and_self())
else:
f_and = Q(steps__ingredients__food=food)
if 'not' in fd_filter:
@@ -422,8 +396,7 @@ def food_filters(self, **kwargs):
else:
self._queryset = self._queryset.filter(f_and)
if 'not' in fd_filter:
- self._queryset = self._queryset.exclude(
- id__in=recipes.values('id'))
+ self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
def unit_filters(self, units=None, operator=True):
if operator != True:
@@ -432,8 +405,7 @@ def unit_filters(self, units=None, operator=True):
return
if not isinstance(units, list):
units = [units]
- self._queryset = self._queryset.filter(
- steps__ingredients__unit__in=units)
+ self._queryset = self._queryset.filter(steps__ingredients__unit__in=units)
def rating_filter(self, rating=None):
if rating or self._sort_includes('rating'):
@@ -479,14 +451,11 @@ def book_filters(self, **kwargs):
recipes = Recipe.objects.all()
for book in kwargs[bk_filter]:
if 'not' in bk_filter:
- recipes = recipes.filter(
- recipebookentry__book__id=book)
+ recipes = recipes.filter(recipebookentry__book__id=book)
else:
- self._queryset = self._queryset.filter(
- recipebookentry__book__id=book)
+ self._queryset = self._queryset.filter(recipebookentry__book__id=book)
if 'not' in bk_filter:
- self._queryset = self._queryset.exclude(
- id__in=recipes.values('id'))
+ self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
def step_filters(self, steps=None, operator=True):
if operator != True:
@@ -505,25 +474,20 @@ def build_fulltext_filters(self, string=None):
rank = []
if 'name' in self._fulltext_include:
vectors.append('name_search_vector')
- rank.append(SearchRank('name_search_vector',
- self.search_query, cover_density=True))
+ rank.append(SearchRank('name_search_vector', self.search_query, cover_density=True))
if 'description' in self._fulltext_include:
vectors.append('desc_search_vector')
- rank.append(SearchRank('desc_search_vector',
- self.search_query, cover_density=True))
+ rank.append(SearchRank('desc_search_vector', self.search_query, cover_density=True))
if 'steps__instruction' in self._fulltext_include:
vectors.append('steps__search_vector')
- rank.append(SearchRank('steps__search_vector',
- self.search_query, cover_density=True))
+ rank.append(SearchRank('steps__search_vector', self.search_query, cover_density=True))
if 'keywords__name' in self._fulltext_include:
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
vectors.append('keywords__name__unaccent')
- rank.append(SearchRank('keywords__name__unaccent',
- self.search_query, cover_density=True))
+ rank.append(SearchRank('keywords__name__unaccent', self.search_query, cover_density=True))
if 'steps__ingredients__food__name' in self._fulltext_include:
vectors.append('steps__ingredients__food__name__unaccent')
- rank.append(SearchRank('steps__ingredients__food__name',
- self.search_query, cover_density=True))
+ rank.append(SearchRank('steps__ingredients__food__name', self.search_query, cover_density=True))
for r in rank:
if self.search_rank is None:
@@ -531,8 +495,7 @@ def build_fulltext_filters(self, string=None):
else:
self.search_rank += r
# modifying queryset will annotation creates duplicate results
- self._filters.append(Q(id__in=Recipe.objects.annotate(
- vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
+ self._filters.append(Q(id__in=Recipe.objects.annotate(vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
def build_text_filters(self, string=None):
if not string:
@@ -557,15 +520,19 @@ def build_trigram(self, string=None):
trigram += TrigramSimilarity(f, self._string)
else:
trigram = TrigramSimilarity(f, self._string)
- self._fuzzy_match = Recipe.objects.annotate(trigram=trigram).distinct(
- ).annotate(simularity=Max('trigram')).values('id', 'simularity').filter(simularity__gt=self._search_prefs.trigram_threshold)
+ self._fuzzy_match = (
+ Recipe.objects.annotate(trigram=trigram)
+ .distinct()
+ .annotate(simularity=Max('trigram'))
+ .values('id', 'simularity')
+ .filter(simularity__gt=self._search_prefs.trigram_threshold)
+ )
self._filters += [Q(pk__in=self._fuzzy_match.values('pk'))]
def _makenow_filter(self, missing=None):
if missing is None or (isinstance(missing, bool) and missing == False):
return
- shopping_users = [
- *self._request.user.get_shopping_share(), self._request.user]
+ shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
onhand_filter = (
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
@@ -575,264 +542,40 @@ def _makenow_filter(self, missing=None):
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
)
makenow_recipes = Recipe.objects.annotate(
- count_food=Count('steps__ingredients__food__pk', filter=Q(
- steps__ingredients__food__isnull=False), distinct=True),
- count_onhand=Count('steps__ingredients__food__pk',
- filter=onhand_filter, distinct=True),
- count_ignore_shopping=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True,
- steps__ingredients__food__recipe__isnull=True), distinct=True),
- has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(
- shopping_users), then=Value(1)), default=Value(0)),
- has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(
- shopping_users), then=Value(1)), default=Value(0))
- ).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'))
+ count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
+ count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
+ count_ignore_shopping=Count(
+ 'steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True, steps__ingredients__food__recipe__isnull=True), distinct=True
+ ),
+ has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users), then=Value(1)), default=Value(0)),
+ has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)), default=Value(0))
+ ).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood__lte=missing)
+ self._queryset = self._queryset.distinct().filter(id__in=makenow_recipes.values('id'))
@staticmethod
def __children_substitute_filter(shopping_users=None):
- children_onhand_subquery = Food.objects.filter(
- path__startswith=OuterRef('path'),
- depth__gt=OuterRef('depth'),
- onhand_users__in=shopping_users
+ children_onhand_subquery = Food.objects.filter(path__startswith=OuterRef('path'), depth__gt=OuterRef('depth'), onhand_users__in=shopping_users)
+ return (
+ Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
+ Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
+ )
+ .exclude(depth=1, numchild=0)
+ .filter(substitute_children=True)
+ .annotate(child_onhand_count=Exists(children_onhand_subquery))
+ .filter(child_onhand_count=True)
)
- return Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
- Q(onhand_users__in=shopping_users)
- | Q(ignore_shopping=True, recipe__isnull=True)
- | Q(substitute__onhand_users__in=shopping_users)
- ).exclude(depth=1, numchild=0
- ).filter(substitute_children=True
- ).annotate(child_onhand_count=Exists(children_onhand_subquery)
- ).filter(child_onhand_count=True)
@staticmethod
def __sibling_substitute_filter(shopping_users=None):
sibling_onhand_subquery = Food.objects.filter(
- path__startswith=Substr(
- OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
- depth=OuterRef('depth'),
- onhand_users__in=shopping_users
+ path__startswith=Substr(OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)), depth=OuterRef('depth'), onhand_users__in=shopping_users
)
- return Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
- Q(onhand_users__in=shopping_users)
- | Q(ignore_shopping=True, recipe__isnull=True)
- | Q(substitute__onhand_users__in=shopping_users)
- ).exclude(depth=1, numchild=0
- ).filter(substitute_siblings=True
- ).annotate(sibling_onhand=Exists(sibling_onhand_subquery)
- ).filter(sibling_onhand=True)
-
-
-class RecipeFacet():
- class CacheEmpty(Exception):
- pass
-
- def __init__(self, request, queryset=None, hash_key=None, cache_timeout=3600):
- if hash_key is None and queryset is None:
- raise ValueError(_("One of queryset or hash_key must be provided"))
-
- self._request = request
- self._queryset = queryset
- self.hash_key = hash_key or str(hash(self._queryset.query))
- self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}"
- self._cache_timeout = cache_timeout
- self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, {})
- if self._cache is None and self._queryset is None:
- raise self.CacheEmpty("No queryset provided and cache empty")
-
- self.Keywords = self._cache.get('Keywords', None)
- self.Foods = self._cache.get('Foods', None)
- self.Books = self._cache.get('Books', None)
- self.Ratings = self._cache.get('Ratings', None)
- # TODO Move Recent to recipe annotation/serializer: requrires change in RecipeSearch(), RecipeSearchView.vue and serializer
- self.Recent = self._cache.get('Recent', None)
-
- if self._queryset is not None:
- self._recipe_list = list(
- self._queryset.values_list('id', flat=True))
- self._search_params = {
- 'keyword_list': self._request.query_params.getlist('keywords', []),
- 'food_list': self._request.query_params.getlist('foods', []),
- 'book_list': self._request.query_params.getlist('book', []),
- 'search_keywords_or': str2bool(self._request.query_params.get('keywords_or', True)),
- 'search_foods_or': str2bool(self._request.query_params.get('foods_or', True)),
- 'search_books_or': str2bool(self._request.query_params.get('books_or', True)),
- 'space': self._request.space,
- }
- elif self.hash_key is not None:
- self._recipe_list = self._cache.get('recipe_list', [])
- self._search_params = {
- 'keyword_list': self._cache.get('keyword_list', None),
- 'food_list': self._cache.get('food_list', None),
- 'book_list': self._cache.get('book_list', None),
- 'search_keywords_or': self._cache.get('search_keywords_or', None),
- 'search_foods_or': self._cache.get('search_foods_or', None),
- 'search_books_or': self._cache.get('search_books_or', None),
- 'space': self._cache.get('space', None),
- }
-
- self._cache = {
- **self._search_params,
- 'recipe_list': self._recipe_list,
- 'Ratings': self.Ratings,
- 'Recent': self.Recent,
- 'Keywords': self.Keywords,
- 'Foods': self.Foods,
- 'Books': self.Books
-
- }
- caches['default'].set(self._SEARCH_CACHE_KEY,
- self._cache, self._cache_timeout)
-
- def get_facets(self, from_cache=False):
- if from_cache:
- return {
- 'cache_key': self.hash_key or '',
- 'Ratings': self.Ratings or {},
- 'Recent': self.Recent or [],
- 'Keywords': self.Keywords or [],
- 'Foods': self.Foods or [],
- 'Books': self.Books or []
- }
- return {
- 'cache_key': self.hash_key,
- 'Ratings': self.get_ratings(),
- 'Recent': self.get_recent(),
- 'Keywords': self.get_keywords(),
- 'Foods': self.get_foods(),
- 'Books': self.get_books()
- }
-
- def set_cache(self, key, value):
- self._cache = {**self._cache, key: value}
- caches['default'].set(
- self._SEARCH_CACHE_KEY,
- self._cache,
- self._cache_timeout
+ return (
+ Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
+ Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
+ )
+ .exclude(depth=1, numchild=0)
+ .filter(substitute_siblings=True)
+ .annotate(sibling_onhand=Exists(sibling_onhand_subquery))
+ .filter(sibling_onhand=True)
)
-
- def get_books(self):
- if self.Books is None:
- self.Books = []
- return self.Books
-
- def get_keywords(self):
- if self.Keywords is None:
- if self._search_params['search_keywords_or']:
- keywords = Keyword.objects.filter(
- space=self._request.space).distinct()
- else:
- keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(
- depth=1)).filter(space=self._request.space).distinct()
-
- # set keywords to root objects only
- keywords = self._keyword_queryset(keywords)
- self.Keywords = [{**x, 'children': None}
- if x['numchild'] > 0 else x for x in list(keywords)]
- self.set_cache('Keywords', self.Keywords)
- return self.Keywords
-
- def get_foods(self):
- if self.Foods is None:
- # # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
- if self._search_params['search_foods_or']:
- foods = Food.objects.filter(
- space=self._request.space).distinct()
- else:
- foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(
- depth=1)).filter(space=self._request.space).distinct()
-
- # set keywords to root objects only
- foods = self._food_queryset(foods)
-
- self.Foods = [{**x, 'children': None}
- if x['numchild'] > 0 else x for x in list(foods)]
- self.set_cache('Foods', self.Foods)
- return self.Foods
-
- def get_ratings(self):
- if self.Ratings is None:
- if not self._request.space.demo and self._request.space.show_facet_count:
- if self._queryset is None:
- self._queryset = Recipe.objects.filter(
- id__in=self._recipe_list)
- rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(
- cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
- self.Ratings = dict(Counter(r.rating for r in rating_qs))
- else:
- self.Rating = {}
- self.set_cache('Ratings', self.Ratings)
- return self.Ratings
-
- def get_recent(self):
- if self.Recent is None:
- # TODO make days of recent recipe a setting
- recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space, created_at__gte=timezone.now() - timedelta(days=14)
- ).values_list('recipe__pk', flat=True)
- self.Recent = list(recent_recipes)
- self.set_cache('Recent', self.Recent)
- return self.Recent
-
- def add_food_children(self, id):
- try:
- food = Food.objects.get(id=id)
- nodes = food.get_ancestors()
- except Food.DoesNotExist:
- return self.get_facets()
- foods = self._food_queryset(food.get_children(), food)
- deep_search = self.Foods
- for node in nodes:
- index = next((i for i, x in enumerate(
- deep_search) if x["id"] == node.id), None)
- deep_search = deep_search[index]['children']
- index = next((i for i, x in enumerate(
- deep_search) if x["id"] == food.id), None)
- deep_search[index]['children'] = [
- {**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
- self.set_cache('Foods', self.Foods)
- return self.get_facets()
-
- def add_keyword_children(self, id):
- try:
- keyword = Keyword.objects.get(id=id)
- nodes = keyword.get_ancestors()
- except Keyword.DoesNotExist:
- return self.get_facets()
- keywords = self._keyword_queryset(keyword.get_children(), keyword)
- deep_search = self.Keywords
- for node in nodes:
- index = next((i for i, x in enumerate(
- deep_search) if x["id"] == node.id), None)
- deep_search = deep_search[index]['children']
- index = next((i for i, x in enumerate(deep_search)
- if x["id"] == keyword.id), None)
- deep_search[index]['children'] = [
- {**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
- self.set_cache('Keywords', self.Keywords)
- return self.get_facets()
-
- def _recipe_count_queryset(self, field, depth=1, steplen=4):
- return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path'), f'{field}__depth__gte': depth}, id__in=self._recipe_list, space=self._request.space
- ).annotate(count=Coalesce(Func('pk', function='Count'), 0)).values('count')
-
- def _keyword_queryset(self, queryset, keyword=None):
- depth = getattr(keyword, 'depth', 0) + 1
- steplen = depth * Keyword.steplen
-
- if not self._request.space.demo and self._request.space.show_facet_count:
- return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0)
- ).filter(depth=depth, count__gt=0
- ).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
- else:
- return queryset.filter(depth=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
-
- def _food_queryset(self, queryset, food=None):
- depth = getattr(food, 'depth', 0) + 1
- steplen = depth * Food.steplen
-
- if not self._request.space.demo and self._request.space.show_facet_count:
- return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0)
- ).filter(depth__lte=depth, count__gt=0
- ).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
- else:
- return queryset.filter(depth__lte=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
diff --git a/cookbook/helper/recipe_url_import.py b/cookbook/helper/recipe_url_import.py
index a73655737c..b863e8bdbf 100644
--- a/cookbook/helper/recipe_url_import.py
+++ b/cookbook/helper/recipe_url_import.py
@@ -1,9 +1,7 @@
-# import random
import re
import traceback
from html import unescape
-from django.core.cache import caches
from django.utils.dateparse import parse_duration
from django.utils.translation import gettext as _
from isodate import parse_duration as iso_parse_duration
@@ -11,19 +9,37 @@
from pytube import YouTube
from recipe_scrapers._utils import get_host_name, get_minutes
-# from cookbook.helper import recipe_url_import as helper
+from cookbook.helper.automation_helper import AutomationEngine
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.models import Automation, Keyword, PropertyType
-# from unicodedata import decomposition
+def get_from_scraper(scrape, request):
+ # converting the scrape_me object to the existing json format based on ld+json
-# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
+ recipe_json = {
+ 'steps': [],
+ 'internal': True
+ }
+ keywords = []
+ # assign source URL
+ try:
+ source_url = scrape.canonical_url()
+ except Exception:
+ try:
+ source_url = scrape.url
+ except Exception:
+ pass
+ if source_url:
+ recipe_json['source_url'] = source_url
+ try:
+ keywords.append(source_url.replace('http://', '').replace('https://', '').split('/')[0])
+ except Exception:
+ recipe_json['source_url'] = ''
-def get_from_scraper(scrape, request):
- # converting the scrape_me object to the existing json format based on ld+json
- recipe_json = {}
+ automation_engine = AutomationEngine(request, source=recipe_json.get('source_url'))
+ # assign recipe name
try:
recipe_json['name'] = parse_name(scrape.title()[:128] or None)
except Exception:
@@ -37,6 +53,10 @@ def get_from_scraper(scrape, request):
if isinstance(recipe_json['name'], list) and len(recipe_json['name']) > 0:
recipe_json['name'] = recipe_json['name'][0]
+ recipe_json['name'] = automation_engine.apply_regex_replace_automation(recipe_json['name'], Automation.NAME_REPLACE)
+
+ # assign recipe description
+ # TODO notify user about limit if reached - >256 description will be truncated
try:
description = scrape.description() or None
except Exception:
@@ -47,8 +67,10 @@ def get_from_scraper(scrape, request):
except Exception:
description = ''
- recipe_json['internal'] = True
+ recipe_json['description'] = parse_description(description)
+ recipe_json['description'] = automation_engine.apply_regex_replace_automation(recipe_json['description'], Automation.DESCRIPTION_REPLACE)
+ # assign servings attributes
try:
# dont use scrape.yields() as this will always return "x servings" or "x items", should be improved in scrapers directly
servings = scrape.schema.data.get('recipeYield') or 1
@@ -58,6 +80,7 @@ def get_from_scraper(scrape, request):
recipe_json['servings'] = parse_servings(servings)
recipe_json['servings_text'] = parse_servings_text(servings)
+ # assign time attributes
try:
recipe_json['working_time'] = get_minutes(scrape.prep_time()) or 0
except Exception:
@@ -82,6 +105,7 @@ def get_from_scraper(scrape, request):
except Exception:
pass
+ # assign image
try:
recipe_json['image'] = parse_image(scrape.image()) or None
except Exception:
@@ -92,7 +116,7 @@ def get_from_scraper(scrape, request):
except Exception:
recipe_json['image'] = ''
- keywords = []
+ # assign keywords
try:
if scrape.schema.data.get("keywords"):
keywords += listify_keywords(scrape.schema.data.get("keywords"))
@@ -117,20 +141,6 @@ def get_from_scraper(scrape, request):
except Exception:
pass
- try:
- source_url = scrape.canonical_url()
- except Exception:
- try:
- source_url = scrape.url
- except Exception:
- pass
- if source_url:
- recipe_json['source_url'] = source_url
- try:
- keywords.append(source_url.replace('http://', '').replace('https://', '').split('/')[0])
- except Exception:
- recipe_json['source_url'] = ''
-
try:
if scrape.author():
keywords.append(scrape.author())
@@ -138,40 +148,25 @@ def get_from_scraper(scrape, request):
pass
try:
- recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space)
+ recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request)
except AttributeError:
recipe_json['keywords'] = keywords
ingredient_parser = IngredientParser(request, True)
- recipe_json['steps'] = []
+ # assign steps
try:
for i in parse_instructions(scrape.instructions()):
- recipe_json['steps'].append({'instruction': i, 'ingredients': [], 'show_ingredients_table': request.user.userpreference.show_step_ingredients,})
+ recipe_json['steps'].append({'instruction': i, 'ingredients': [], 'show_ingredients_table': request.user.userpreference.show_step_ingredients, })
except Exception:
pass
if len(recipe_json['steps']) == 0:
recipe_json['steps'].append({'instruction': '', 'ingredients': [], })
- parsed_description = parse_description(description)
- # TODO notify user about limit if reached
- # limits exist to limit the attack surface for dos style attacks
- automations = Automation.objects.filter(
- type=Automation.DESCRIPTION_REPLACE,
- space=request.space,
- disabled=False).only(
- 'param_1',
- 'param_2',
- 'param_3').all().order_by('order')[
- :512]
- for a in automations:
- if re.match(a.param_1, (recipe_json['source_url'])[:512]):
- parsed_description = re.sub(a.param_2, a.param_3, parsed_description, count=1)
-
- if len(parsed_description) > 256: # split at 256 as long descriptions don't look good on recipe cards
- recipe_json['steps'][0]['instruction'] = f'*{parsed_description}* \n\n' + recipe_json['steps'][0]['instruction']
+ if len(recipe_json['description']) > 256: # split at 256 as long descriptions don't look good on recipe cards
+ recipe_json['steps'][0]['instruction'] = f"*{recipe_json['description']}* \n\n" + recipe_json['steps'][0]['instruction']
else:
- recipe_json['description'] = parsed_description[:512]
+ recipe_json['description'] = recipe_json['description'][:512]
try:
for x in scrape.ingredients():
@@ -212,19 +207,9 @@ def get_from_scraper(scrape, request):
traceback.print_exc()
pass
- if 'source_url' in recipe_json and recipe_json['source_url']:
- automations = Automation.objects.filter(
- type=Automation.INSTRUCTION_REPLACE,
- space=request.space,
- disabled=False).only(
- 'param_1',
- 'param_2',
- 'param_3').order_by('order').all()[
- :512]
- for a in automations:
- if re.match(a.param_1, (recipe_json['source_url'])[:512]):
- for s in recipe_json['steps']:
- s['instruction'] = re.sub(a.param_2, a.param_3, s['instruction'])
+ for s in recipe_json['steps']:
+ s['instruction'] = automation_engine.apply_regex_replace_automation(s['instruction'], Automation.INSTRUCTION_REPLACE)
+ # re.sub(a.param_2, a.param_3, s['instruction'])
return recipe_json
@@ -274,11 +259,14 @@ def get_from_youtube_scraper(url, request):
]
}
+ # TODO add automation here
try:
+ automation_engine = AutomationEngine(request, source=url)
video = YouTube(url=url)
- default_recipe_json['name'] = video.title
+ default_recipe_json['name'] = automation_engine.apply_regex_replace_automation(video.title, Automation.NAME_REPLACE)
default_recipe_json['image'] = video.thumbnail_url
- default_recipe_json['steps'][0]['instruction'] = video.description
+ default_recipe_json['steps'][0]['instruction'] = automation_engine.apply_regex_replace_automation(video.description, Automation.INSTRUCTION_REPLACE)
+
except Exception:
pass
@@ -417,18 +405,9 @@ def parse_time(recipe_time):
return recipe_time
-def parse_keywords(keyword_json, space):
+def parse_keywords(keyword_json, request):
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):
- keyword_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.lower()] = a.param_2
- caches['default'].set(KEYWORD_CACHE_KEY, keyword_aliases, 30)
+ automation_engine = AutomationEngine(request)
# keywords as list
for kw in keyword_json:
@@ -436,12 +415,8 @@ def parse_keywords(keyword_json, space):
# if alias exists use that instead
if len(kw) != 0:
- if keyword_aliases:
- try:
- kw = keyword_aliases[kw.lower()]
- except KeyError:
- pass
- if k := Keyword.objects.filter(name=kw, space=space).first():
+ automation_engine.apply_keyword_automation(kw)
+ if k := Keyword.objects.filter(name=kw, space=request.space).first():
keywords.append({'label': str(k), 'name': k.name, 'id': k.id})
else:
keywords.append({'label': kw, 'name': kw})
diff --git a/cookbook/helper/scope_middleware.py b/cookbook/helper/scope_middleware.py
index a0218f0892..8c2613c8f0 100644
--- a/cookbook/helper/scope_middleware.py
+++ b/cookbook/helper/scope_middleware.py
@@ -1,8 +1,6 @@
from django.urls import reverse
from django_scopes import scope, scopes_disabled
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
-from rest_framework.authentication import TokenAuthentication
-from rest_framework.authtoken.models import Token
from rest_framework.exceptions import AuthenticationFailed
from cookbook.views import views
@@ -50,7 +48,6 @@ def __call__(self, request):
return views.no_groups(request)
request.space = user_space.space
- # with scopes_disabled():
with scope(space=request.space):
return self.get_response(request)
else:
diff --git a/cookbook/helper/shopping_helper.py b/cookbook/helper/shopping_helper.py
index c4a8b07964..ffcf5beab1 100644
--- a/cookbook/helper/shopping_helper.py
+++ b/cookbook/helper/shopping_helper.py
@@ -1,16 +1,13 @@
from datetime import timedelta
from decimal import Decimal
-from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import F, OuterRef, Q, Subquery, Value
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.utils.translation import gettext as _
-from cookbook.helper.HelperFunctions import Round, str2bool
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
SupermarketCategoryRelation)
-from recipes import settings
def shopping_helper(qs, request):
@@ -47,7 +44,7 @@ def __init__(self, user, space, **kwargs):
self.mealplan = self._kwargs.get('mealplan', None)
if type(self.mealplan) in [int, float]:
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
- if type(self.mealplan) == dict:
+ if isinstance(self.mealplan, dict):
self.mealplan = MealPlan.objects.filter(id=self.mealplan['id'], space=self.space).first()
self.id = self._kwargs.get('id', None)
@@ -69,11 +66,12 @@ def __init__(self, user, space, **kwargs):
@property
def _recipe_servings(self):
- return getattr(self.recipe, 'servings', None) or getattr(getattr(self.mealplan, 'recipe', None), 'servings', None) or getattr(getattr(self._shopping_list_recipe, 'recipe', None), 'servings', None)
+ return getattr(self.recipe, 'servings', None) or getattr(getattr(self.mealplan, 'recipe', None), 'servings',
+ None) or getattr(getattr(self._shopping_list_recipe, 'recipe', None), 'servings', None)
@property
def _servings_factor(self):
- return Decimal(self.servings)/Decimal(self._recipe_servings)
+ return Decimal(self.servings) / Decimal(self._recipe_servings)
@property
def _shared_users(self):
@@ -90,9 +88,10 @@ def get_shopping_list_recipe(id, user, space):
def get_recipe_ingredients(self, id, exclude_onhand=False):
if exclude_onhand:
- return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(food__onhand_users__id__in=[x.id for x in self._shared_users])
+ return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(
+ food__onhand_users__id__in=[x.id for x in self._shared_users])
else:
- return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
+ return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
@property
def _include_related(self):
@@ -109,7 +108,7 @@ def create(self, **kwargs):
self.servings = float(servings)
if mealplan := kwargs.get('mealplan', None):
- if type(mealplan) == dict:
+ if isinstance(mealplan, dict):
self.mealplan = MealPlan.objects.filter(id=mealplan['id'], space=self.space).first()
else:
self.mealplan = mealplan
@@ -170,14 +169,14 @@ def delete(self, **kwargs):
try:
self._shopping_list_recipe.delete()
return True
- except:
+ except BaseException:
return False
def _add_ingredients(self, ingredients=None):
if not ingredients:
return
- elif type(ingredients) == list:
- ingredients = Ingredient.objects.filter(id__in=ingredients)
+ elif isinstance(ingredients, list):
+ ingredients = Ingredient.objects.filter(id__in=ingredients, food__ignore_shopping=False)
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
add_ingredients = ingredients.exclude(id__in=existing)
@@ -199,120 +198,3 @@ def _delete_ingredients(self, ingredients=None):
to_delete = self._shopping_list_recipe.entries.exclude(ingredient__in=ingredients)
ShoppingListEntry.objects.filter(id__in=to_delete).delete()
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
-
-
-# # TODO refactor as class
-# def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None, append=False):
-# """
-# Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
-# :param list_recipe: Modify an existing ShoppingListRecipe
-# :param recipe: Recipe to use as list of ingredients. One of [recipe, mealplan] are required
-# :param mealplan: alternatively use a mealplan recipe as source of ingredients
-# :param servings: Optional: Number of servings to use to scale shoppinglist. If servings = 0 an existing recipe list will be deleted
-# :param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
-# :param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list
-# """
-# r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
-# if not r:
-# raise ValueError(_("You must supply a recipe or mealplan"))
-
-# created_by = created_by or getattr(ShoppingListEntry.objects.filter(list_recipe=list_recipe).first(), 'created_by', None)
-# if not created_by:
-# raise ValueError(_("You must supply a created_by"))
-
-# try:
-# servings = float(servings)
-# except (ValueError, TypeError):
-# servings = getattr(mealplan, 'servings', 1.0)
-
-# servings_factor = servings / r.servings
-
-# shared_users = list(created_by.get_shopping_share())
-# shared_users.append(created_by)
-# if list_recipe:
-# created = False
-# else:
-# list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings)
-# created = True
-
-# related_step_ing = []
-# if servings == 0 and not created:
-# list_recipe.delete()
-# return []
-# elif ingredients:
-# ingredients = Ingredient.objects.filter(pk__in=ingredients, space=space)
-# else:
-# ingredients = Ingredient.objects.filter(step__recipe=r, food__ignore_shopping=False, space=space)
-
-# if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
-# ingredients = ingredients.exclude(food__onhand_users__id__in=[x.id for x in shared_users])
-
-# if related := created_by.userpreference.mealplan_autoinclude_related:
-# # TODO: add levels of related recipes (related recipes of related recipes) to use when auto-adding mealplans
-# related_recipes = r.get_related_recipes()
-
-# for x in related_recipes:
-# # related recipe is a Step serving size is driven by recipe serving size
-# # TODO once/if Steps can have a serving size this needs to be refactored
-# if exclude_onhand:
-# # if steps are used more than once in a recipe or subrecipe - I don' think this results in the desired behavior
-# related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users]).values_list('id', flat=True)
-# else:
-# related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True)
-
-# x_ing = []
-# if ingredients.filter(food__recipe=x).exists():
-# for ing in ingredients.filter(food__recipe=x):
-# if exclude_onhand:
-# x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
-# else:
-# x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__ignore_shopping=True)
-# for i in [x for x in x_ing]:
-# ShoppingListEntry.objects.create(
-# list_recipe=list_recipe,
-# food=i.food,
-# unit=i.unit,
-# ingredient=i,
-# amount=i.amount * Decimal(servings_factor),
-# created_by=created_by,
-# space=space,
-# )
-# # dont' add food to the shopping list that are actually recipes that will be added as ingredients
-# ingredients = ingredients.exclude(food__recipe=x)
-
-# add_ingredients = list(ingredients.values_list('id', flat=True)) + related_step_ing
-# if not append:
-# existing_list = ShoppingListEntry.objects.filter(list_recipe=list_recipe)
-# # delete shopping list entries not included in ingredients
-# existing_list.exclude(ingredient__in=ingredients).delete()
-# # add shopping list entries that did not previously exist
-# add_ingredients = set(add_ingredients) - set(existing_list.values_list('ingredient__id', flat=True))
-# add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
-
-# # if servings have changed, update the ShoppingListRecipe and existing Entries
-# if servings <= 0:
-# servings = 1
-
-# if not created and list_recipe.servings != servings:
-# update_ingredients = set(ingredients.values_list('id', flat=True)) - set(add_ingredients.values_list('id', flat=True))
-# list_recipe.servings = servings
-# list_recipe.save()
-# for sle in ShoppingListEntry.objects.filter(list_recipe=list_recipe, ingredient__id__in=update_ingredients):
-# sle.amount = sle.ingredient.amount * Decimal(servings_factor)
-# sle.save()
-
-# # add any missing Entries
-# for i in [x for x in add_ingredients if x.food]:
-
-# ShoppingListEntry.objects.create(
-# list_recipe=list_recipe,
-# food=i.food,
-# unit=i.unit,
-# ingredient=i,
-# amount=i.amount * Decimal(servings_factor),
-# created_by=created_by,
-# space=space,
-# )
-
-# # return all shopping list items
-# return list_recipe
\ No newline at end of file
diff --git a/cookbook/integration/chowdown.py b/cookbook/integration/chowdown.py
index 3d41cf481b..722ce2880f 100644
--- a/cookbook/integration/chowdown.py
+++ b/cookbook/integration/chowdown.py
@@ -4,6 +4,7 @@
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser
+from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step
@@ -19,6 +20,10 @@ def get_recipe_from_file(self, file):
direction_mode = False
description_mode = False
+ description = None
+ prep_time = None
+ serving = None
+
ingredients = []
directions = []
descriptions = []
@@ -26,6 +31,12 @@ def get_recipe_from_file(self, file):
line = fl.decode("utf-8")
if 'title:' in line:
title = line.replace('title:', '').replace('"', '').strip()
+ if 'description:' in line:
+ description = line.replace('description:', '').replace('"', '').strip()
+ if 'prep_time:' in line:
+ prep_time = line.replace('prep_time:', '').replace('"', '').strip()
+ if 'yield:' in line:
+ serving = line.replace('yield:', '').replace('"', '').strip()
if 'image:' in line:
image = line.replace('image:', '').strip()
if 'tags:' in line:
@@ -48,15 +59,43 @@ def get_recipe_from_file(self, file):
descriptions.append(line)
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space)
+ if description:
+ recipe.description = description
for k in tags.split(','):
- print(f'adding keyword {k.strip()}')
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
recipe.keywords.add(keyword)
- step = Step.objects.create(
- instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions), space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
- )
+ ingredients_added = False
+ for direction in directions:
+ if len(direction.strip()) > 0:
+ step = Step.objects.create(
+ instruction=direction, name='', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
+ )
+ else:
+ step = Step.objects.create(
+ instruction=direction, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
+ )
+ if not ingredients_added:
+ ingredients_added = True
+
+ ingredient_parser = IngredientParser(self.request, True)
+ for ingredient in ingredients:
+ if len(ingredient.strip()) > 0:
+ amount, unit, food, note = ingredient_parser.parse(ingredient)
+ f = ingredient_parser.get_food(food)
+ u = ingredient_parser.get_unit(unit)
+ step.ingredients.add(Ingredient.objects.create(
+ food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
+ ))
+ recipe.steps.add(step)
+
+ if serving:
+ recipe.servings = parse_servings(serving)
+ recipe.servings_text = 'servings'
+
+ if prep_time:
+ recipe.working_time = parse_time(prep_time)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
@@ -76,6 +115,7 @@ def get_recipe_from_file(self, file):
if re.match(f'^images/{image}$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename))
+ recipe.save()
return recipe
def get_file_from_recipe(self, recipe):
diff --git a/cookbook/integration/cookbookapp.py b/cookbook/integration/cookbookapp.py
index 99cccea46b..21d9d7f30f 100644
--- a/cookbook/integration/cookbookapp.py
+++ b/cookbook/integration/cookbookapp.py
@@ -1,20 +1,15 @@
-import base64
-import gzip
-import json
import re
-from gettext import gettext as _
from io import BytesIO
import requests
import validators
-import yaml
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import (get_from_scraper, get_images_from_soup,
iso_duration_to_minutes)
from cookbook.helper.scrapers.scrapers import text_scraper
from cookbook.integration.integration import Integration
-from cookbook.models import Ingredient, Keyword, Recipe, Step
+from cookbook.models import Ingredient, Recipe, Step
class CookBookApp(Integration):
@@ -25,7 +20,6 @@ def import_file_name_filter(self, zip_info_object):
def get_recipe_from_file(self, file):
recipe_html = file.getvalue().decode("utf-8")
- # recipe_json, recipe_tree, html_data, images = get_recipe_from_source(recipe_html, 'CookBookApp', self.request)
scrape = text_scraper(text=recipe_html)
recipe_json = get_from_scraper(scrape, self.request)
images = list(dict.fromkeys(get_images_from_soup(scrape.soup, None)))
@@ -37,7 +31,7 @@ def get_recipe_from_file(self, file):
try:
recipe.servings = re.findall('([0-9])+', recipe_json['recipeYield'])[0]
- except Exception as e:
+ except Exception:
pass
try:
@@ -47,7 +41,8 @@ def get_recipe_from_file(self, file):
pass
# assuming import files only contain single step
- step = Step.objects.create(instruction=recipe_json['steps'][0]['instruction'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
+ step = Step.objects.create(instruction=recipe_json['steps'][0]['instruction'], space=self.request.space,
+ show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
if 'nutrition' in recipe_json:
step.instruction = step.instruction + '\n\n' + recipe_json['nutrition']
@@ -62,7 +57,7 @@ def get_recipe_from_file(self, file):
if unit := ingredient.get('unit', None):
u = ingredient_parser.get_unit(unit.get('name', None))
step.ingredients.add(Ingredient.objects.create(
- food=f, unit=u, amount=ingredient.get('amount', None), note=ingredient.get('note', None), original_text=ingredient.get('original_text', None), space=self.request.space,
+ food=f, unit=u, amount=ingredient.get('amount', None), note=ingredient.get('note', None), original_text=ingredient.get('original_text', None), space=self.request.space,
))
if len(images) > 0:
diff --git a/cookbook/integration/cookmate.py b/cookbook/integration/cookmate.py
index 58fd082442..a51ca45b9e 100644
--- a/cookbook/integration/cookmate.py
+++ b/cookbook/integration/cookmate.py
@@ -1,17 +1,12 @@
-import base64
-import json
from io import BytesIO
-from gettext import gettext as _
-
import requests
import validators
-from lxml import etree
from cookbook.helper.ingredient_parser import IngredientParser
-from cookbook.helper.recipe_url_import import parse_servings, parse_time, parse_servings_text
+from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
from cookbook.integration.integration import Integration
-from cookbook.models import Ingredient, Keyword, Recipe, Step
+from cookbook.models import Ingredient, Recipe, Step
class Cookmate(Integration):
diff --git a/cookbook/integration/copymethat.py b/cookbook/integration/copymethat.py
index 21ea665212..01d51c844c 100644
--- a/cookbook/integration/copymethat.py
+++ b/cookbook/integration/copymethat.py
@@ -1,4 +1,3 @@
-import re
from io import BytesIO
from zipfile import ZipFile
@@ -26,12 +25,13 @@ def get_recipe_from_file(self, file):
except AttributeError:
source = None
- recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip()[:128], source_url=source, created_by=self.request.user, internal=True, space=self.request.space, )
+ recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip(
+ )[:128], source_url=source, created_by=self.request.user, internal=True, space=self.request.space, )
for category in file.find_all("span", {"class": "recipeCategory"}):
keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space)
recipe.keywords.add(keyword)
-
+
try:
recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip())
recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
@@ -61,7 +61,14 @@ def get_recipe_from_file(self, file):
if not isinstance(ingredient, Tag) or not ingredient.text.strip() or "recipeIngredient_spacer" in ingredient['class']:
continue
if any(x in ingredient['class'] for x in ["recipeIngredient_subheader", "recipeIngredient_note"]):
- step.ingredients.add(Ingredient.objects.create(is_header=True, note=ingredient.text.strip()[:256], original_text=ingredient.text.strip(), space=self.request.space, ))
+ step.ingredients.add(
+ Ingredient.objects.create(
+ is_header=True,
+ note=ingredient.text.strip()[
+ :256],
+ original_text=ingredient.text.strip(),
+ space=self.request.space,
+ ))
else:
amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(food)
@@ -78,7 +85,7 @@ def get_recipe_from_file(self, file):
step.save()
recipe.steps.add(step)
step = Step.objects.create(instruction='', space=self.request.space, )
-
+
step.name = instruction.text.strip()[:128]
else:
step.instruction += instruction.text.strip() + ' \n\n'
diff --git a/cookbook/integration/default.py b/cookbook/integration/default.py
index 5939b5f47e..00a1579668 100644
--- a/cookbook/integration/default.py
+++ b/cookbook/integration/default.py
@@ -22,7 +22,7 @@ def get_recipe_from_file(self, file):
if images:
try:
self.import_recipe_image(recipe, BytesIO(recipe_zip.read(images[0])), filetype=get_filetype(images[0]))
- except AttributeError as e:
+ except AttributeError:
traceback.print_exc()
return recipe
@@ -58,7 +58,7 @@ def get_files_from_recipes(self, recipes, el, cookie):
try:
recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read())
- except ValueError:
+ except (ValueError, FileNotFoundError):
pass
recipe_zip_obj.close()
@@ -71,4 +71,4 @@ def get_files_from_recipes(self, recipes, el, cookie):
export_zip_obj.close()
- return [[ self.get_export_file_name(), export_zip_stream.getvalue() ]]
\ No newline at end of file
+ return [[self.get_export_file_name(), export_zip_stream.getvalue()]]
diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py
index dbe3d6678d..7386b50d92 100644
--- a/cookbook/integration/integration.py
+++ b/cookbook/integration/integration.py
@@ -1,4 +1,3 @@
-import traceback
import datetime
import traceback
import uuid
@@ -18,8 +17,7 @@
from cookbook.helper.image_processing import handle_image
from cookbook.models import Keyword, Recipe
-from recipes.settings import DEBUG
-from recipes.settings import EXPORT_FILE_CACHE_DURATION
+from recipes.settings import DEBUG, EXPORT_FILE_CACHE_DURATION
class Integration:
@@ -39,7 +37,6 @@ def __init__(self, request, export_type):
self.ignored_recipes = []
description = f'Imported by {request.user.get_user_display_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}. Type: {export_type}'
- icon = '📥'
try:
last_kw = Keyword.objects.filter(name__regex=r'^(Import [0-9]+)', space=request.space).latest('created_at')
@@ -52,23 +49,19 @@ def __init__(self, request, export_type):
self.keyword = parent.add_child(
name=name,
description=description,
- icon=icon,
space=request.space
)
except (IntegrityError, ValueError): # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
self.keyword = parent.add_child(
name=f'{name} {str(uuid.uuid4())[0:8]}',
description=description,
- icon=icon,
space=request.space
)
-
-
def do_export(self, recipes, el):
with scope(space=self.request.space):
- el.total_recipes = len(recipes)
+ el.total_recipes = len(recipes)
el.cache_duration = EXPORT_FILE_CACHE_DURATION
el.save()
@@ -80,7 +73,7 @@ def do_export(self, recipes, el):
export_file = file
else:
- #zip the files if there is more then one file
+ # zip the files if there is more then one file
export_filename = self.get_export_file_name()
export_stream = BytesIO()
export_obj = ZipFile(export_stream, 'w')
@@ -91,8 +84,7 @@ def do_export(self, recipes, el):
export_obj.close()
export_file = export_stream.getvalue()
-
- cache.set('export_file_'+str(el.pk), {'filename': export_filename, 'file': export_file}, EXPORT_FILE_CACHE_DURATION)
+ cache.set('export_file_' + str(el.pk), {'filename': export_filename, 'file': export_file}, EXPORT_FILE_CACHE_DURATION)
el.running = False
el.save()
@@ -100,7 +92,6 @@ def do_export(self, recipes, el):
response['Content-Disposition'] = 'attachment; filename="' + export_filename + '"'
return response
-
def import_file_name_filter(self, zip_info_object):
"""
Since zipfile.namelist() returns all files in all subdirectories this function allows filtering of files
@@ -164,7 +155,7 @@ def do_import(self, files, il, import_duplicates):
for z in file_list:
try:
- if not hasattr(z, 'filename') or type(z) == Tag:
+ if not hasattr(z, 'filename') or isinstance(z, Tag):
recipe = self.get_recipe_from_file(z)
else:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
@@ -298,7 +289,6 @@ def handle_exception(exception, log=None, message=''):
if DEBUG:
traceback.print_exc()
-
def get_export_file_name(self, format='zip'):
return "export_{}.{}".format(datetime.datetime.now().strftime("%Y-%m-%d"), format)
diff --git a/cookbook/integration/mealie.py b/cookbook/integration/mealie.py
index 5e4e1578da..21ff633c50 100644
--- a/cookbook/integration/mealie.py
+++ b/cookbook/integration/mealie.py
@@ -7,7 +7,7 @@
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
from cookbook.integration.integration import Integration
-from cookbook.models import Ingredient, Recipe, Step
+from cookbook.models import Ingredient, Keyword, Recipe, Step
class Mealie(Integration):
@@ -56,6 +56,12 @@ def get_recipe_from_file(self, file):
except Exception:
pass
+ if 'tags' in recipe_json and len(recipe_json['tags']) > 0:
+ for k in recipe_json['tags']:
+ if 'name' in k:
+ keyword, created = Keyword.objects.get_or_create(name=k['name'].strip(), space=self.request.space)
+ recipe.keywords.add(keyword)
+
if 'notes' in recipe_json and len(recipe_json['notes']) > 0:
notes_text = "#### Notes \n\n"
for n in recipe_json['notes']:
diff --git a/cookbook/integration/melarecipes.py b/cookbook/integration/melarecipes.py
index 9679a90f1a..fe95460bf9 100644
--- a/cookbook/integration/melarecipes.py
+++ b/cookbook/integration/melarecipes.py
@@ -57,7 +57,7 @@ def get_recipe_from_file(self, file):
recipe.source_url = recipe_json['link']
step = Step.objects.create(
- instruction=instruction, space=self.request.space,
+ instruction=instruction, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients
)
ingredient_parser = IngredientParser(self.request, True)
@@ -67,7 +67,7 @@ def get_recipe_from_file(self, file):
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
- food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
+ food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)
diff --git a/cookbook/integration/nextcloud_cookbook.py b/cookbook/integration/nextcloud_cookbook.py
index f43501f8c9..4357c509b7 100644
--- a/cookbook/integration/nextcloud_cookbook.py
+++ b/cookbook/integration/nextcloud_cookbook.py
@@ -2,13 +2,14 @@
import re
from io import BytesIO, StringIO
from zipfile import ZipFile
+
from PIL import Image
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import iso_duration_to_minutes
from cookbook.integration.integration import Integration
-from cookbook.models import Ingredient, Keyword, Recipe, Step, NutritionInformation
+from cookbook.models import Ingredient, Keyword, NutritionInformation, Recipe, Step
class NextcloudCookbook(Integration):
@@ -51,7 +52,6 @@ def get_recipe_from_file(self, file):
ingredients_added = False
for s in recipe_json['recipeInstructions']:
- instruction_text = ''
if 'text' in s:
step = Step.objects.create(
instruction=s['text'], name=s['name'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
@@ -91,7 +91,7 @@ def get_recipe_from_file(self, file):
if nutrition != {}:
recipe.nutrition = NutritionInformation.objects.create(**nutrition, space=self.request.space)
recipe.save()
- except Exception as e:
+ except Exception:
pass
for f in self.files:
diff --git a/cookbook/integration/openeats.py b/cookbook/integration/openeats.py
index 406c997819..2a30b73c3d 100644
--- a/cookbook/integration/openeats.py
+++ b/cookbook/integration/openeats.py
@@ -1,9 +1,11 @@
import json
+from django.utils.translation import gettext as _
+
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
-from cookbook.models import Ingredient, Recipe, Step, Keyword, Comment, CookLog
-from django.utils.translation import gettext as _
+from cookbook.models import Comment, CookLog, Ingredient, Keyword, Recipe, Step
+
class OpenEats(Integration):
@@ -25,16 +27,16 @@ def get_recipe_from_file(self, file):
if file["source"] != '':
instructions += '\n' + _('Recipe source:') + f'[{file["source"]}]({file["source"]})'
- cuisine_keyword, created = Keyword.objects.get_or_create(name="Cuisine", space=self.request.space)
+ cuisine_keyword, created = Keyword.objects.get_or_create(name="Cuisine", space=self.request.space)
if file["cuisine"] != '':
- keyword, created = Keyword.objects.get_or_create(name=file["cuisine"].strip(), space=self.request.space)
+ keyword, created = Keyword.objects.get_or_create(name=file["cuisine"].strip(), space=self.request.space)
if created:
keyword.move(cuisine_keyword, pos="last-child")
recipe.keywords.add(keyword)
- course_keyword, created = Keyword.objects.get_or_create(name="Course", space=self.request.space)
+ course_keyword, created = Keyword.objects.get_or_create(name="Course", space=self.request.space)
if file["course"] != '':
- keyword, created = Keyword.objects.get_or_create(name=file["course"].strip(), space=self.request.space)
+ keyword, created = Keyword.objects.get_or_create(name=file["course"].strip(), space=self.request.space)
if created:
keyword.move(course_keyword, pos="last-child")
recipe.keywords.add(keyword)
diff --git a/cookbook/integration/paprika.py b/cookbook/integration/paprika.py
index 9904f330a0..b830d85fd6 100644
--- a/cookbook/integration/paprika.py
+++ b/cookbook/integration/paprika.py
@@ -90,7 +90,7 @@ def get_recipe_from_file(self, file):
if validators.url(url, public=True):
response = requests.get(url)
self.import_recipe_image(recipe, BytesIO(response.content))
- except:
+ except Exception:
if recipe_json.get("photo_data", None):
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])), filetype='.jpeg')
diff --git a/cookbook/integration/pdfexport.py b/cookbook/integration/pdfexport.py
index fca7824737..4fea857810 100644
--- a/cookbook/integration/pdfexport.py
+++ b/cookbook/integration/pdfexport.py
@@ -1,21 +1,11 @@
-import json
-from io import BytesIO
-from re import match
-from zipfile import ZipFile
import asyncio
-from pyppeteer import launch
-from rest_framework.renderers import JSONRenderer
+import django.core.management.commands.runserver as runserver
+from asgiref.sync import sync_to_async
+from pyppeteer import launch
-from cookbook.helper.image_processing import get_filetype
from cookbook.integration.integration import Integration
-from cookbook.serializer import RecipeExportSerializer
-
-from cookbook.models import ExportLog
-from asgiref.sync import sync_to_async
-import django.core.management.commands.runserver as runserver
-import logging
class PDFexport(Integration):
@@ -42,7 +32,6 @@ async def get_files_from_recipes_async(self, recipes, el, cookie):
}
}
-
files = []
for recipe in recipes:
@@ -50,20 +39,18 @@ async def get_files_from_recipes_async(self, recipes, el, cookie):
await page.emulateMedia('print')
await page.setCookie(cookies)
- await page.goto('http://'+cmd.default_addr+':'+cmd.default_port+'/view/recipe/'+str(recipe.id), {'waitUntil': 'domcontentloaded'})
- await page.waitForSelector('#printReady');
+ await page.goto('http://' + cmd.default_addr + ':' + cmd.default_port + '/view/recipe/' + str(recipe.id), {'waitUntil': 'domcontentloaded'})
+ await page.waitForSelector('#printReady')
files.append([recipe.name + '.pdf', await page.pdf(options)])
- await page.close();
+ await page.close()
el.exported_recipes += 1
el.msg += self.get_recipe_processed_msg(recipe)
await sync_to_async(el.save, thread_sensitive=True)()
-
await browser.close()
return files
-
def get_files_from_recipes(self, recipes, el, cookie):
return asyncio.run(self.get_files_from_recipes_async(recipes, el, cookie))
diff --git a/cookbook/integration/recipesage.py b/cookbook/integration/recipesage.py
index 70b8838ce2..ab681c2521 100644
--- a/cookbook/integration/recipesage.py
+++ b/cookbook/integration/recipesage.py
@@ -39,7 +39,7 @@ def get_recipe_from_file(self, file):
ingredients_added = False
for s in file['recipeInstructions']:
step = Step.objects.create(
- instruction=s['text'], space=self.request.space,
+ instruction=s['text'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
if not ingredients_added:
ingredients_added = True
@@ -49,7 +49,7 @@ def get_recipe_from_file(self, file):
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
- food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
+ food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)
diff --git a/cookbook/integration/rezeptsuitede.py b/cookbook/integration/rezeptsuitede.py
index a3334d8404..afe3e543cb 100644
--- a/cookbook/integration/rezeptsuitede.py
+++ b/cookbook/integration/rezeptsuitede.py
@@ -2,12 +2,10 @@
from io import BytesIO
from xml import etree
-from lxml import etree
-
from cookbook.helper.ingredient_parser import IngredientParser
-from cookbook.helper.recipe_url_import import parse_time, parse_servings, parse_servings_text
+from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text
from cookbook.integration.integration import Integration
-from cookbook.models import Ingredient, Recipe, Step, Keyword
+from cookbook.models import Ingredient, Keyword, Recipe, Step
class Rezeptsuitede(Integration):
@@ -61,14 +59,14 @@ def get_recipe_from_file(self, file):
try:
k, created = Keyword.objects.get_or_create(name=recipe_xml.find('head').find('cat').text.strip(), space=self.request.space)
recipe.keywords.add(k)
- except Exception as e:
+ except Exception:
pass
recipe.save()
try:
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_xml.find('head').find('picbin').text)), filetype='.jpeg')
- except:
+ except BaseException:
pass
return recipe
diff --git a/cookbook/integration/rezkonv.py b/cookbook/integration/rezkonv.py
index 4f5f7fc92c..d8417ded4d 100644
--- a/cookbook/integration/rezkonv.py
+++ b/cookbook/integration/rezkonv.py
@@ -60,8 +60,8 @@ def get_file_from_recipe(self, recipe):
def split_recipe_file(self, file):
recipe_list = []
current_recipe = ''
- encoding_list = ['windows-1250',
- 'latin-1'] # TODO build algorithm to try trough encodings and fail if none work, use for all importers
+ # TODO build algorithm to try trough encodings and fail if none work, use for all importers
+ # encoding_list = ['windows-1250', 'latin-1']
encoding = 'windows-1250'
for fl in file.readlines():
try:
diff --git a/cookbook/integration/saffron.py b/cookbook/integration/saffron.py
index 4c75d1a049..ab0b3155c2 100644
--- a/cookbook/integration/saffron.py
+++ b/cookbook/integration/saffron.py
@@ -59,11 +59,11 @@ def get_recipe_from_file(self, file):
def get_file_from_recipe(self, recipe):
- data = "Title: "+recipe.name if recipe.name else ""+"\n"
- data += "Description: "+recipe.description if recipe.description else ""+"\n"
+ data = "Title: " + recipe.name if recipe.name else "" + "\n"
+ data += "Description: " + recipe.description if recipe.description else "" + "\n"
data += "Source: \n"
data += "Original URL: \n"
- data += "Yield: "+str(recipe.servings)+"\n"
+ data += "Yield: " + str(recipe.servings) + "\n"
data += "Cookbook: \n"
data += "Section: \n"
data += "Image: \n"
@@ -78,13 +78,13 @@ def get_file_from_recipe(self, recipe):
data += "Ingredients: \n"
for ingredient in recipeIngredient:
- data += ingredient+"\n"
+ data += ingredient + "\n"
data += "Instructions: \n"
for instruction in recipeInstructions:
- data += instruction+"\n"
+ data += instruction + "\n"
- return recipe.name+'.txt', data
+ return recipe.name + '.txt', data
def get_files_from_recipes(self, recipes, el, cookie):
files = []
diff --git a/cookbook/locale/de/LC_MESSAGES/django.po b/cookbook/locale/de/LC_MESSAGES/django.po
index 9043f3b7df..ae2a907541 100644
--- a/cookbook/locale/de/LC_MESSAGES/django.po
+++ b/cookbook/locale/de/LC_MESSAGES/django.po
@@ -15,8 +15,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
-"PO-Revision-Date: 2023-08-13 08:19+0000\n"
-"Last-Translator: Fabian Flodman \n"
+"PO-Revision-Date: 2023-11-22 18:19+0000\n"
+"Last-Translator: Spreez \n"
"Language-Team: German \n"
"Language: de\n"
@@ -161,7 +161,7 @@ msgstr "Name"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
-msgstr "Stichwörter"
+msgstr "Schlüsselwörter"
#: .\cookbook\forms.py:125
msgid "Preparation time in minutes"
diff --git a/cookbook/locale/es/LC_MESSAGES/django.po b/cookbook/locale/es/LC_MESSAGES/django.po
index a7af39ac76..14802d8601 100644
--- a/cookbook/locale/es/LC_MESSAGES/django.po
+++ b/cookbook/locale/es/LC_MESSAGES/django.po
@@ -14,7 +14,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
-"PO-Revision-Date: 2023-08-27 11:19+0000\n"
+"PO-Revision-Date: 2023-09-25 09:59+0000\n"
"Last-Translator: Matias Laporte \n"
"Language-Team: Spanish \n"
diff --git a/cookbook/locale/fr/LC_MESSAGES/django.po b/cookbook/locale/fr/LC_MESSAGES/django.po
index 395410671e..6ee174e0d0 100644
--- a/cookbook/locale/fr/LC_MESSAGES/django.po
+++ b/cookbook/locale/fr/LC_MESSAGES/django.po
@@ -14,8 +14,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
-"PO-Revision-Date: 2023-08-16 21:19+0000\n"
-"Last-Translator: Alexandre Braure \n"
+"PO-Revision-Date: 2023-10-12 20:19+0000\n"
+"Last-Translator: pharok \n"
"Language-Team: French \n"
"Language: fr\n"
@@ -310,7 +310,7 @@ msgid ""
msgstr ""
"Champs à rechercher en ignorant les accents. La sélection de cette option "
"peut améliorer ou dégrader la qualité de la recherche en fonction de la "
-"langue."
+"langue"
#: .\cookbook\forms.py:466
msgid ""
@@ -326,8 +326,8 @@ msgid ""
"will return 'salad' and 'sandwich')"
msgstr ""
"Champs permettant de rechercher les correspondances de début de mot (par "
-"exemple, si vous recherchez « sa », vous obtiendrez « salade » et "
-"« sandwich»)."
+"exemple, si vous recherchez « sa », vous obtiendrez « salade » et « "
+"sandwich»)"
#: .\cookbook\forms.py:470
msgid ""
@@ -546,8 +546,6 @@ msgid "One of queryset or hash_key must be provided"
msgstr "Il est nécessaire de fournir soit le queryset, soit la clé de hachage"
#: .\cookbook\helper\recipe_url_import.py:266
-#, fuzzy
-#| msgid "Use fractions"
msgid "reverse rotation"
msgstr "sens inverse"
@@ -557,27 +555,27 @@ msgstr ""
#: .\cookbook\helper\recipe_url_import.py:268
msgid "knead"
-msgstr ""
+msgstr "pétrir"
#: .\cookbook\helper\recipe_url_import.py:269
msgid "thicken"
-msgstr ""
+msgstr "épaissir"
#: .\cookbook\helper\recipe_url_import.py:270
msgid "warm up"
-msgstr ""
+msgstr "réchauffer"
#: .\cookbook\helper\recipe_url_import.py:271
msgid "ferment"
-msgstr ""
+msgstr "fermenter"
#: .\cookbook\helper\recipe_url_import.py:272
msgid "sous-vide"
-msgstr ""
+msgstr "sous-vide"
#: .\cookbook\helper\shopping_helper.py:157
msgid "You must supply a servings size"
-msgstr "Vous devez fournir une information de portion"
+msgstr "Vous devez fournir un nombre de portions"
#: .\cookbook\helper\template_helper.py:79
#: .\cookbook\helper\template_helper.py:81
@@ -590,7 +588,6 @@ msgid "Favorite"
msgstr "Favori"
#: .\cookbook\integration\copymethat.py:50
-#, fuzzy
msgid "I made this"
msgstr "J'ai fait ça"
@@ -646,7 +643,7 @@ msgstr "Portions"
#: .\cookbook\integration\saffron.py:25
msgid "Waiting time"
-msgstr "temps d’attente"
+msgstr "Temps d’attente"
#: .\cookbook\integration\saffron.py:27
msgid "Preparation Time"
@@ -849,7 +846,6 @@ msgid "ID of unit to use for the shopping list"
msgstr "ID de l’unité à utiliser pour la liste de courses"
#: .\cookbook\serializer.py:1259
-#, fuzzy
msgid "When set to true will delete all food from active shopping lists."
msgstr ""
"Lorsqu'il est défini sur \"true\", tous les aliments des listes de courses "
@@ -965,8 +961,9 @@ msgid ""
" ."
msgstr ""
"Confirmez SVP que\n"
-" est une adresse mail de "
-"l’utilisateur %(user_display)s."
+" est une adresse mail de l’"
+"utilisateur %(user_display)s\n"
+" ."
#: .\cookbook\templates\account\email_confirm.html:22
#: .\cookbook\templates\generic\delete_template.html:72
@@ -1369,9 +1366,8 @@ msgid "Are you sure you want to delete the %(title)s: %(object)s "
msgstr "Êtes-vous sûr(e) de vouloir supprimer %(title)s : %(object)s "
#: .\cookbook\templates\generic\delete_template.html:22
-#, fuzzy
msgid "This cannot be undone!"
-msgstr "Cela ne peut pas être annulé !"
+msgstr "L'opération ne peut pas être annulée !"
#: .\cookbook\templates\generic\delete_template.html:27
msgid "Protected"
@@ -1454,12 +1450,12 @@ msgid ""
" "
msgstr ""
"\n"
-" Les champs Mot de passe et Token sont stockés en texte "
-"brutdans la base de données.\n"
+" Les champs Mot de passe et Token sont stockés en clair"
+"b>dans la base de données.\n"
" C'est nécessaire car ils sont utilisés pour faire des requêtes API, "
"mais cela accroît le risque que quelqu'un les vole.
\n"
-" Pour limiter les risques, des tokens ou comptes avec un accès limité "
-"devraient être utilisés.\n"
+" Pour limiter les risques, il est possible d'utiliser des tokens ou "
+"des comptes avec un accès limité.\n"
" "
#: .\cookbook\templates\index.html:29
@@ -1769,15 +1765,6 @@ msgstr ""
" "
#: .\cookbook\templates\search_info.html:29
-#, fuzzy
-#| msgid ""
-#| " \n"
-#| " Simple searches ignore punctuation and common words such as "
-#| "'the', 'a', 'and'. And will treat seperate words as required.\n"
-#| " Searching for 'apple or flour' will return any recipe that "
-#| "includes both 'apple' and 'flour' anywhere in the fields that have been "
-#| "selected for a full text search.\n"
-#| " "
msgid ""
" \n"
" Simple searches ignore punctuation and common words such as "
@@ -1789,7 +1776,7 @@ msgid ""
msgstr ""
" \n"
" Les recherches simples ignorent la ponctuation et les mots "
-"courants tels que \"le\", \"a\", \"et\", et traiteront les mots séparés "
+"courants tels que \"le\", \"et\", \"a\", et traiteront les mots séparés "
"comme il se doit.\n"
" Si vous recherchez \"pomme ou farine\", vous obtiendrez toutes "
"les recettes qui contiennent à la fois \"pomme\" et \"farine\" dans les "
@@ -2217,7 +2204,7 @@ msgstr "Gérer l’abonnement"
#: .\cookbook\templates\space_overview.html:13 .\cookbook\views\delete.py:216
msgid "Space"
-msgstr "Groupe :"
+msgstr "Groupe"
#: .\cookbook\templates\space_overview.html:17
msgid ""
@@ -2657,7 +2644,7 @@ msgstr ""
#: .\cookbook\views\api.py:1394
msgid "Sync successful!"
-msgstr "Synchro réussie !"
+msgstr "Synchronisation réussie !"
#: .\cookbook\views\api.py:1399
msgid "Error synchronizing with Storage"
@@ -2730,6 +2717,8 @@ msgid ""
"The PDF Exporter is not enabled on this instance as it is still in an "
"experimental state."
msgstr ""
+"L'export PDF n'est pas activé sur cette instance car il est toujours au "
+"statut expérimental."
#: .\cookbook\views\lists.py:24
msgid "Import Log"
diff --git a/cookbook/locale/he/LC_MESSAGES/django.po b/cookbook/locale/he/LC_MESSAGES/django.po
new file mode 100644
index 0000000000..6e12418d8b
--- /dev/null
+++ b/cookbook/locale/he/LC_MESSAGES/django.po
@@ -0,0 +1,2529 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-08-29 13:09+0200\n"
+"PO-Revision-Date: 2023-11-15 08:20+0000\n"
+"Last-Translator: avi meyer \n"
+"Language-Team: Hebrew \n"
+"Language: he\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && "
+"n % 10 == 0) ? 2 : 3));\n"
+"X-Generator: Weblate 4.15\n"
+
+#: .\cookbook\forms.py:53
+msgid "Default unit"
+msgstr "יחידות ברירת מחדל"
+
+#: .\cookbook\forms.py:54
+msgid "Use fractions"
+msgstr "שימוש בשברים"
+
+#: .\cookbook\forms.py:55
+msgid "Use KJ"
+msgstr "שימוש ב KJ"
+
+#: .\cookbook\forms.py:56
+msgid "Theme"
+msgstr "נושא"
+
+#: .\cookbook\forms.py:57
+msgid "Navbar color"
+msgstr ""
+
+#: .\cookbook\forms.py:58
+msgid "Sticky navbar"
+msgstr ""
+
+#: .\cookbook\forms.py:59
+msgid "Default page"
+msgstr ""
+
+#: .\cookbook\forms.py:60
+msgid "Plan sharing"
+msgstr ""
+
+#: .\cookbook\forms.py:61
+msgid "Ingredient decimal places"
+msgstr ""
+
+#: .\cookbook\forms.py:62
+msgid "Shopping list auto sync period"
+msgstr ""
+
+#: .\cookbook\forms.py:63 .\cookbook\templates\recipe_view.html:36
+msgid "Comments"
+msgstr ""
+
+#: .\cookbook\forms.py:64
+msgid "Left-handed mode"
+msgstr ""
+
+#: .\cookbook\forms.py:65
+msgid "Show step ingredients table"
+msgstr ""
+
+#: .\cookbook\forms.py:69
+msgid ""
+"Color of the top navigation bar. Not all colors work with all themes, just "
+"try them out!"
+msgstr ""
+
+#: .\cookbook\forms.py:71
+msgid "Default Unit to be used when inserting a new ingredient into a recipe."
+msgstr ""
+
+#: .\cookbook\forms.py:73
+msgid ""
+"Enables support for fractions in ingredient amounts (e.g. convert decimals "
+"to fractions automatically)"
+msgstr ""
+
+#: .\cookbook\forms.py:75
+msgid "Display nutritional energy amounts in joules instead of calories"
+msgstr ""
+
+#: .\cookbook\forms.py:76
+msgid "Users with whom newly created meal plans should be shared by default."
+msgstr ""
+
+#: .\cookbook\forms.py:77
+msgid "Users with whom to share shopping lists."
+msgstr ""
+
+#: .\cookbook\forms.py:78
+msgid "Number of decimals to round ingredients."
+msgstr ""
+
+#: .\cookbook\forms.py:79
+msgid "If you want to be able to create and see comments underneath recipes."
+msgstr ""
+
+#: .\cookbook\forms.py:81 .\cookbook\forms.py:512
+msgid ""
+"Setting to 0 will disable auto sync. When viewing a shopping list the list "
+"is updated every set seconds to sync changes someone else might have made. "
+"Useful when shopping with multiple people but might use a little bit of "
+"mobile data. If lower than instance limit it is reset when saving."
+msgstr ""
+
+#: .\cookbook\forms.py:84
+msgid "Makes the navbar stick to the top of the page."
+msgstr ""
+
+#: .\cookbook\forms.py:85 .\cookbook\forms.py:515
+msgid "Automatically add meal plan ingredients to shopping list."
+msgstr ""
+
+#: .\cookbook\forms.py:86
+msgid "Exclude ingredients that are on hand."
+msgstr ""
+
+#: .\cookbook\forms.py:87
+msgid "Will optimize the UI for use with your left hand."
+msgstr ""
+
+#: .\cookbook\forms.py:88
+msgid ""
+"Add ingredients table next to recipe steps. Applies at creation time for "
+"manually created and URL imported recipes. Individual steps can be "
+"overridden in the edit recipe view."
+msgstr ""
+
+#: .\cookbook\forms.py:105
+msgid ""
+"Both fields are optional. If none are given the username will be displayed "
+"instead"
+msgstr ""
+
+#: .\cookbook\forms.py:126 .\cookbook\forms.py:317
+msgid "Name"
+msgstr ""
+
+#: .\cookbook\forms.py:127 .\cookbook\forms.py:318 .\cookbook\views\lists.py:88
+msgid "Keywords"
+msgstr "מילות מפתח"
+
+#: .\cookbook\forms.py:128
+msgid "Preparation time in minutes"
+msgstr ""
+
+#: .\cookbook\forms.py:129
+msgid "Waiting time (cooking/baking) in minutes"
+msgstr ""
+
+#: .\cookbook\forms.py:130 .\cookbook\forms.py:286 .\cookbook\forms.py:319
+msgid "Path"
+msgstr ""
+
+#: .\cookbook\forms.py:131
+msgid "Storage UID"
+msgstr ""
+
+#: .\cookbook\forms.py:164
+msgid "Default"
+msgstr ""
+
+#: .\cookbook\forms.py:193
+msgid ""
+"To prevent duplicates recipes with the same name as existing ones are "
+"ignored. Check this box to import everything."
+msgstr ""
+
+#: .\cookbook\forms.py:216
+msgid "Add your comment: "
+msgstr ""
+
+#: .\cookbook\forms.py:231
+msgid "Leave empty for dropbox and enter app password for nextcloud."
+msgstr ""
+
+#: .\cookbook\forms.py:238
+msgid "Leave empty for nextcloud and enter api token for dropbox."
+msgstr ""
+
+#: .\cookbook\forms.py:247
+msgid ""
+"Leave empty for dropbox and enter only base url for nextcloud (/remote."
+"php/webdav/
is added automatically)"
+msgstr ""
+
+#: .\cookbook\forms.py:285 .\cookbook\views\edit.py:157
+msgid "Storage"
+msgstr ""
+
+#: .\cookbook\forms.py:287
+msgid "Active"
+msgstr ""
+
+#: .\cookbook\forms.py:293
+msgid "Search String"
+msgstr ""
+
+#: .\cookbook\forms.py:320
+msgid "File ID"
+msgstr ""
+
+#: .\cookbook\forms.py:342
+msgid "You must provide at least a recipe or a title."
+msgstr ""
+
+#: .\cookbook\forms.py:355
+msgid "You can list default users to share recipes with in the settings."
+msgstr ""
+
+#: .\cookbook\forms.py:356
+msgid ""
+"You can use markdown to format this field. See the docs here"
+msgstr ""
+
+#: .\cookbook\forms.py:382
+msgid "Maximum number of users for this space reached."
+msgstr ""
+
+#: .\cookbook\forms.py:388
+msgid "Email address already taken!"
+msgstr ""
+
+#: .\cookbook\forms.py:396
+msgid ""
+"An email address is not required but if present the invite link will be sent "
+"to the user."
+msgstr ""
+
+#: .\cookbook\forms.py:411
+msgid "Name already taken."
+msgstr ""
+
+#: .\cookbook\forms.py:422
+msgid "Accept Terms and Privacy"
+msgstr ""
+
+#: .\cookbook\forms.py:454
+msgid ""
+"Determines how fuzzy a search is if it uses trigram similarity matching (e."
+"g. low values mean more typos are ignored)."
+msgstr ""
+
+#: .\cookbook\forms.py:464
+msgid ""
+"Select type method of search. Click here for "
+"full description of choices."
+msgstr ""
+
+#: .\cookbook\forms.py:465
+msgid ""
+"Use fuzzy matching on units, keywords and ingredients when editing and "
+"importing recipes."
+msgstr ""
+
+#: .\cookbook\forms.py:467
+msgid ""
+"Fields to search ignoring accents. Selecting this option can improve or "
+"degrade search quality depending on language"
+msgstr ""
+
+#: .\cookbook\forms.py:469
+msgid ""
+"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
+"'pie' and 'piece' and 'soapie')"
+msgstr ""
+
+#: .\cookbook\forms.py:471
+msgid ""
+"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
+"will return 'salad' and 'sandwich')"
+msgstr ""
+
+#: .\cookbook\forms.py:473
+msgid ""
+"Fields to 'fuzzy' search. (e.g. searching for 'recpie' will find 'recipe'.) "
+"Note: this option will conflict with 'web' and 'raw' methods of search."
+msgstr ""
+
+#: .\cookbook\forms.py:475
+msgid ""
+"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
+"only function with fulltext fields."
+msgstr ""
+
+#: .\cookbook\forms.py:479
+msgid "Search Method"
+msgstr ""
+
+#: .\cookbook\forms.py:480
+msgid "Fuzzy Lookups"
+msgstr ""
+
+#: .\cookbook\forms.py:481
+msgid "Ignore Accent"
+msgstr ""
+
+#: .\cookbook\forms.py:482
+msgid "Partial Match"
+msgstr ""
+
+#: .\cookbook\forms.py:483
+msgid "Starts With"
+msgstr ""
+
+#: .\cookbook\forms.py:484
+msgid "Fuzzy Search"
+msgstr ""
+
+#: .\cookbook\forms.py:485
+msgid "Full Text"
+msgstr ""
+
+#: .\cookbook\forms.py:510
+msgid ""
+"Users will see all items you add to your shopping list. They must add you "
+"to see items on their list."
+msgstr ""
+
+#: .\cookbook\forms.py:516
+msgid ""
+"When adding a meal plan to the shopping list (manually or automatically), "
+"include all related recipes."
+msgstr ""
+
+#: .\cookbook\forms.py:517
+msgid ""
+"When adding a meal plan to the shopping list (manually or automatically), "
+"exclude ingredients that are on hand."
+msgstr ""
+
+#: .\cookbook\forms.py:518
+msgid "Default number of hours to delay a shopping list entry."
+msgstr ""
+
+#: .\cookbook\forms.py:519
+msgid "Filter shopping list to only include supermarket categories."
+msgstr ""
+
+#: .\cookbook\forms.py:520
+msgid "Days of recent shopping list entries to display."
+msgstr ""
+
+#: .\cookbook\forms.py:521
+msgid "Mark food 'On Hand' when checked off shopping list."
+msgstr ""
+
+#: .\cookbook\forms.py:522
+msgid "Delimiter to use for CSV exports."
+msgstr ""
+
+#: .\cookbook\forms.py:523
+msgid "Prefix to add when copying list to the clipboard."
+msgstr ""
+
+#: .\cookbook\forms.py:527
+msgid "Share Shopping List"
+msgstr ""
+
+#: .\cookbook\forms.py:528
+msgid "Autosync"
+msgstr ""
+
+#: .\cookbook\forms.py:529
+msgid "Auto Add Meal Plan"
+msgstr ""
+
+#: .\cookbook\forms.py:530
+msgid "Exclude On Hand"
+msgstr ""
+
+#: .\cookbook\forms.py:531
+msgid "Include Related"
+msgstr ""
+
+#: .\cookbook\forms.py:532
+msgid "Default Delay Hours"
+msgstr ""
+
+#: .\cookbook\forms.py:533
+msgid "Filter to Supermarket"
+msgstr ""
+
+#: .\cookbook\forms.py:534
+msgid "Recent Days"
+msgstr ""
+
+#: .\cookbook\forms.py:535
+msgid "CSV Delimiter"
+msgstr ""
+
+#: .\cookbook\forms.py:536
+msgid "List Prefix"
+msgstr ""
+
+#: .\cookbook\forms.py:537
+msgid "Auto On Hand"
+msgstr ""
+
+#: .\cookbook\forms.py:547
+msgid "Reset Food Inheritance"
+msgstr ""
+
+#: .\cookbook\forms.py:548
+msgid "Reset all food to inherit the fields configured."
+msgstr ""
+
+#: .\cookbook\forms.py:560
+msgid "Fields on food that should be inherited by default."
+msgstr ""
+
+#: .\cookbook\forms.py:561
+msgid "Show recipe counts on search filters"
+msgstr ""
+
+#: .\cookbook\forms.py:562
+msgid "Use the plural form for units and food inside this space."
+msgstr ""
+
+#: .\cookbook\helper\AllAuthCustomAdapter.py:39
+msgid ""
+"In order to prevent spam, the requested email was not send. Please wait a "
+"few minutes and try again."
+msgstr ""
+
+#: .\cookbook\helper\permission_helper.py:164
+#: .\cookbook\helper\permission_helper.py:187 .\cookbook\views\views.py:113
+msgid "You are not logged in and therefore cannot view this page!"
+msgstr ""
+
+#: .\cookbook\helper\permission_helper.py:168
+#: .\cookbook\helper\permission_helper.py:174
+#: .\cookbook\helper\permission_helper.py:199
+#: .\cookbook\helper\permission_helper.py:269
+#: .\cookbook\helper\permission_helper.py:283
+#: .\cookbook\helper\permission_helper.py:294
+#: .\cookbook\helper\permission_helper.py:305
+#: .\cookbook\helper\permission_helper.py:321
+#: .\cookbook\helper\permission_helper.py:342 .\cookbook\views\data.py:36
+#: .\cookbook\views\views.py:124 .\cookbook\views\views.py:131
+msgid "You do not have the required permissions to view this page!"
+msgstr ""
+
+#: .\cookbook\helper\permission_helper.py:192
+#: .\cookbook\helper\permission_helper.py:215
+#: .\cookbook\helper\permission_helper.py:237
+#: .\cookbook\helper\permission_helper.py:252
+msgid "You cannot interact with this object as it is not owned by you!"
+msgstr ""
+
+#: .\cookbook\helper\permission_helper.py:403
+msgid "You have reached the maximum number of recipes for your space."
+msgstr ""
+
+#: .\cookbook\helper\permission_helper.py:415
+msgid "You have more users than allowed in your space."
+msgstr ""
+
+#: .\cookbook\helper\recipe_search.py:632
+msgid "One of queryset or hash_key must be provided"
+msgstr ""
+
+#: .\cookbook\helper\recipe_url_import.py:316
+msgid "reverse rotation"
+msgstr ""
+
+#: .\cookbook\helper\recipe_url_import.py:317
+msgid "careful rotation"
+msgstr ""
+
+#: .\cookbook\helper\recipe_url_import.py:318
+msgid "knead"
+msgstr ""
+
+#: .\cookbook\helper\recipe_url_import.py:319
+msgid "thicken"
+msgstr ""
+
+#: .\cookbook\helper\recipe_url_import.py:320
+msgid "warm up"
+msgstr ""
+
+#: .\cookbook\helper\recipe_url_import.py:321
+msgid "ferment"
+msgstr ""
+
+#: .\cookbook\helper\recipe_url_import.py:322
+msgid "sous-vide"
+msgstr ""
+
+#: .\cookbook\helper\shopping_helper.py:157
+msgid "You must supply a servings size"
+msgstr ""
+
+#: .\cookbook\helper\template_helper.py:90
+#: .\cookbook\helper\template_helper.py:92
+msgid "Could not parse template code."
+msgstr ""
+
+#: .\cookbook\integration\copymethat.py:44
+#: .\cookbook\integration\melarecipes.py:37
+msgid "Favorite"
+msgstr ""
+
+#: .\cookbook\integration\copymethat.py:50
+msgid "I made this"
+msgstr ""
+
+#: .\cookbook\integration\integration.py:218
+msgid ""
+"Importer expected a .zip file. Did you choose the correct importer type for "
+"your data ?"
+msgstr ""
+
+#: .\cookbook\integration\integration.py:221
+msgid ""
+"An unexpected error occurred during the import. Please make sure you have "
+"uploaded a valid file."
+msgstr ""
+
+#: .\cookbook\integration\integration.py:226
+msgid "The following recipes were ignored because they already existed:"
+msgstr ""
+
+#: .\cookbook\integration\integration.py:230
+#, python-format
+msgid "Imported %s recipes."
+msgstr ""
+
+#: .\cookbook\integration\openeats.py:26
+msgid "Recipe source:"
+msgstr ""
+
+#: .\cookbook\integration\paprika.py:49
+msgid "Notes"
+msgstr ""
+
+#: .\cookbook\integration\paprika.py:52
+msgid "Nutritional Information"
+msgstr ""
+
+#: .\cookbook\integration\paprika.py:56
+msgid "Source"
+msgstr ""
+
+#: .\cookbook\integration\recettetek.py:54
+#: .\cookbook\integration\recipekeeper.py:70
+msgid "Imported from"
+msgstr ""
+
+#: .\cookbook\integration\saffron.py:23
+msgid "Servings"
+msgstr ""
+
+#: .\cookbook\integration\saffron.py:25
+msgid "Waiting time"
+msgstr ""
+
+#: .\cookbook\integration\saffron.py:27
+msgid "Preparation Time"
+msgstr ""
+
+#: .\cookbook\integration\saffron.py:29 .\cookbook\templates\index.html:7
+msgid "Cookbook"
+msgstr ""
+
+#: .\cookbook\integration\saffron.py:31
+msgid "Section"
+msgstr ""
+
+#: .\cookbook\management\commands\rebuildindex.py:14
+msgid "Rebuilds full text search index on Recipe"
+msgstr ""
+
+#: .\cookbook\management\commands\rebuildindex.py:18
+msgid "Only Postgresql databases use full text search, no index to rebuild"
+msgstr ""
+
+#: .\cookbook\management\commands\rebuildindex.py:29
+msgid "Recipe index rebuild complete."
+msgstr ""
+
+#: .\cookbook\management\commands\rebuildindex.py:31
+msgid "Recipe index rebuild failed."
+msgstr ""
+
+#: .\cookbook\migrations\0047_auto_20200602_1133.py:14
+msgid "Breakfast"
+msgstr ""
+
+#: .\cookbook\migrations\0047_auto_20200602_1133.py:19
+msgid "Lunch"
+msgstr ""
+
+#: .\cookbook\migrations\0047_auto_20200602_1133.py:24
+msgid "Dinner"
+msgstr ""
+
+#: .\cookbook\migrations\0047_auto_20200602_1133.py:29 .\cookbook\models.py:774
+msgid "Other"
+msgstr ""
+
+#: .\cookbook\migrations\0190_auto_20230525_1506.py:17
+msgid "Fat"
+msgstr ""
+
+#: .\cookbook\migrations\0190_auto_20230525_1506.py:17
+#: .\cookbook\migrations\0190_auto_20230525_1506.py:18
+#: .\cookbook\migrations\0190_auto_20230525_1506.py:19
+msgid "g"
+msgstr ""
+
+#: .\cookbook\migrations\0190_auto_20230525_1506.py:18
+msgid "Carbohydrates"
+msgstr ""
+
+#: .\cookbook\migrations\0190_auto_20230525_1506.py:19
+msgid "Proteins"
+msgstr ""
+
+#: .\cookbook\migrations\0190_auto_20230525_1506.py:20
+msgid "Calories"
+msgstr ""
+
+#: .\cookbook\migrations\0190_auto_20230525_1506.py:20
+msgid "kcal"
+msgstr ""
+
+#: .\cookbook\models.py:264
+msgid ""
+"Maximum file storage for space in MB. 0 for unlimited, -1 to disable file "
+"upload."
+msgstr ""
+
+#: .\cookbook\models.py:372 .\cookbook\templates\search.html:7
+#: .\cookbook\templates\settings.html:18
+#: .\cookbook\templates\space_manage.html:7
+msgid "Search"
+msgstr ""
+
+#: .\cookbook\models.py:373 .\cookbook\templates\base.html:112
+#: .\cookbook\templates\meal_plan.html:7 .\cookbook\views\delete.py:178
+#: .\cookbook\views\edit.py:211 .\cookbook\views\new.py:179
+msgid "Meal-Plan"
+msgstr ""
+
+#: .\cookbook\models.py:374 .\cookbook\templates\base.html:120
+msgid "Books"
+msgstr ""
+
+#: .\cookbook\models.py:611
+msgid " is part of a recipe step and cannot be deleted"
+msgstr ""
+
+#: .\cookbook\models.py:773
+msgid "Nutrition"
+msgstr ""
+
+#: .\cookbook\models.py:773
+msgid "Allergen"
+msgstr ""
+
+#: .\cookbook\models.py:774
+msgid "Price"
+msgstr ""
+
+#: .\cookbook\models.py:774
+msgid "Goal"
+msgstr ""
+
+#: .\cookbook\models.py:1275 .\cookbook\templates\search_info.html:28
+msgid "Simple"
+msgstr ""
+
+#: .\cookbook\models.py:1276 .\cookbook\templates\search_info.html:33
+msgid "Phrase"
+msgstr ""
+
+#: .\cookbook\models.py:1277 .\cookbook\templates\search_info.html:38
+msgid "Web"
+msgstr ""
+
+#: .\cookbook\models.py:1278 .\cookbook\templates\search_info.html:47
+msgid "Raw"
+msgstr ""
+
+#: .\cookbook\models.py:1327
+msgid "Food Alias"
+msgstr ""
+
+#: .\cookbook\models.py:1327
+msgid "Unit Alias"
+msgstr ""
+
+#: .\cookbook\models.py:1327
+msgid "Keyword Alias"
+msgstr ""
+
+#: .\cookbook\models.py:1328
+msgid "Description Replace"
+msgstr ""
+
+#: .\cookbook\models.py:1328
+msgid "Instruction Replace"
+msgstr ""
+
+#: .\cookbook\models.py:1329
+msgid "Never Unit"
+msgstr ""
+
+#: .\cookbook\models.py:1329
+msgid "Transpose Words"
+msgstr ""
+
+#: .\cookbook\models.py:1355 .\cookbook\views\delete.py:36
+#: .\cookbook\views\edit.py:251 .\cookbook\views\new.py:48
+msgid "Recipe"
+msgstr ""
+
+#: .\cookbook\models.py:1356
+msgid "Food"
+msgstr ""
+
+#: .\cookbook\models.py:1357 .\cookbook\templates\base.html:147
+msgid "Keyword"
+msgstr ""
+
+#: .\cookbook\serializer.py:222
+msgid "File uploads are not enabled for this Space."
+msgstr ""
+
+#: .\cookbook\serializer.py:233
+msgid "You have reached your file upload limit."
+msgstr ""
+
+#: .\cookbook\serializer.py:318
+msgid "Cannot modify Space owner permission."
+msgstr ""
+
+#: .\cookbook\serializer.py:1233
+msgid "Hello"
+msgstr ""
+
+#: .\cookbook\serializer.py:1233
+msgid "You have been invited by "
+msgstr ""
+
+#: .\cookbook\serializer.py:1235
+msgid " to join their Tandoor Recipes space "
+msgstr ""
+
+#: .\cookbook\serializer.py:1237
+msgid "Click the following link to activate your account: "
+msgstr ""
+
+#: .\cookbook\serializer.py:1239
+msgid ""
+"If the link does not work use the following code to manually join the space: "
+msgstr ""
+
+#: .\cookbook\serializer.py:1241
+msgid "The invitation is valid until "
+msgstr ""
+
+#: .\cookbook\serializer.py:1243
+msgid ""
+"Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub "
+msgstr ""
+
+#: .\cookbook\serializer.py:1246
+msgid "Tandoor Recipes Invite"
+msgstr ""
+
+#: .\cookbook\serializer.py:1388
+msgid "Existing shopping list to update"
+msgstr ""
+
+#: .\cookbook\serializer.py:1390
+msgid ""
+"List of ingredient IDs from the recipe to add, if not provided all "
+"ingredients will be added."
+msgstr ""
+
+#: .\cookbook\serializer.py:1392
+msgid ""
+"Providing a list_recipe ID and servings of 0 will delete that shopping list."
+msgstr ""
+
+#: .\cookbook\serializer.py:1401
+msgid "Amount of food to add to the shopping list"
+msgstr ""
+
+#: .\cookbook\serializer.py:1403
+msgid "ID of unit to use for the shopping list"
+msgstr ""
+
+#: .\cookbook\serializer.py:1405
+msgid "When set to true will delete all food from active shopping lists."
+msgstr ""
+
+#: .\cookbook\tables.py:61 .\cookbook\tables.py:75
+#: .\cookbook\templates\generic\delete_template.html:7
+#: .\cookbook\templates\generic\delete_template.html:15
+#: .\cookbook\templates\generic\edit_template.html:28
+msgid "Delete"
+msgstr ""
+
+#: .\cookbook\templates\404.html:5
+msgid "404 Error"
+msgstr ""
+
+#: .\cookbook\templates\404.html:18
+msgid "The page you are looking for could not be found."
+msgstr ""
+
+#: .\cookbook\templates\404.html:33
+msgid "Take me Home"
+msgstr ""
+
+#: .\cookbook\templates\404.html:35
+msgid "Report a Bug"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:6
+#: .\cookbook\templates\account\email.html:17
+msgid "E-mail Addresses"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:12
+#: .\cookbook\templates\account\password_change.html:11
+#: .\cookbook\templates\account\password_set.html:11
+#: .\cookbook\templates\base.html:329 .\cookbook\templates\settings.html:6
+#: .\cookbook\templates\settings.html:17
+#: .\cookbook\templates\socialaccount\connections.html:10
+#: .\cookbook\templates\user_settings.html:8
+msgid "Settings"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:13
+msgid "Email"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:19
+msgid "The following e-mail addresses are associated with your account:"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:36
+msgid "Verified"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:38
+msgid "Unverified"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:40
+msgid "Primary"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:47
+msgid "Make Primary"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:49
+msgid "Re-send Verification"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:50
+#: .\cookbook\templates\generic\delete_template.html:57
+#: .\cookbook\templates\socialaccount\connections.html:44
+msgid "Remove"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:58
+msgid "Warning:"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:58
+msgid ""
+"You currently do not have any e-mail address set up. You should really add "
+"an e-mail address so you can receive notifications, reset your password, etc."
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:64
+msgid "Add E-mail Address"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:69
+msgid "Add E-mail"
+msgstr ""
+
+#: .\cookbook\templates\account\email.html:79
+msgid "Do you really want to remove the selected e-mail address?"
+msgstr ""
+
+#: .\cookbook\templates\account\email_confirm.html:6
+#: .\cookbook\templates\account\email_confirm.html:10
+msgid "Confirm E-mail Address"
+msgstr ""
+
+#: .\cookbook\templates\account\email_confirm.html:16
+#, python-format
+msgid ""
+"Please confirm that\n"
+" %(email)s is an e-mail address "
+"for user %(user_display)s\n"
+" ."
+msgstr ""
+
+#: .\cookbook\templates\account\email_confirm.html:22
+#: .\cookbook\templates\generic\delete_template.html:72
+msgid "Confirm"
+msgstr ""
+
+#: .\cookbook\templates\account\email_confirm.html:29
+#, python-format
+msgid ""
+"This e-mail confirmation link expired or is invalid. Please\n"
+" issue a new e-mail confirmation "
+"request."
+msgstr ""
+
+#: .\cookbook\templates\account\login.html:8 .\cookbook\templates\base.html:382
+#: .\cookbook\templates\openid\login.html:8
+msgid "Login"
+msgstr ""
+
+#: .\cookbook\templates\account\login.html:15
+#: .\cookbook\templates\account\login.html:31
+#: .\cookbook\templates\account\signup.html:69
+#: .\cookbook\templates\account\signup_closed.html:15
+#: .\cookbook\templates\openid\login.html:15
+#: .\cookbook\templates\openid\login.html:26
+#: .\cookbook\templates\socialaccount\authentication_error.html:15
+msgid "Sign In"
+msgstr ""
+
+#: .\cookbook\templates\account\login.html:34
+#: .\cookbook\templates\socialaccount\signup.html:8
+#: .\cookbook\templates\socialaccount\signup.html:57
+msgid "Sign Up"
+msgstr ""
+
+#: .\cookbook\templates\account\login.html:38
+msgid "Lost your password?"
+msgstr ""
+
+#: .\cookbook\templates\account\login.html:39
+#: .\cookbook\templates\account\password_reset.html:29
+msgid "Reset My Password"
+msgstr ""
+
+#: .\cookbook\templates\account\login.html:50
+msgid "Social Login"
+msgstr ""
+
+#: .\cookbook\templates\account\login.html:51
+msgid "You can use any of the following providers to sign in."
+msgstr ""
+
+#: .\cookbook\templates\account\logout.html:5
+#: .\cookbook\templates\account\logout.html:9
+#: .\cookbook\templates\account\logout.html:18
+msgid "Sign Out"
+msgstr ""
+
+#: .\cookbook\templates\account\logout.html:11
+msgid "Are you sure you want to sign out?"
+msgstr ""
+
+#: .\cookbook\templates\account\password_change.html:6
+#: .\cookbook\templates\account\password_change.html:16
+#: .\cookbook\templates\account\password_change.html:21
+#: .\cookbook\templates\account\password_reset_from_key.html:7
+#: .\cookbook\templates\account\password_reset_from_key.html:13
+#: .\cookbook\templates\account\password_reset_from_key_done.html:7
+#: .\cookbook\templates\account\password_reset_from_key_done.html:13
+msgid "Change Password"
+msgstr ""
+
+#: .\cookbook\templates\account\password_change.html:12
+#: .\cookbook\templates\account\password_set.html:12
+msgid "Password"
+msgstr ""
+
+#: .\cookbook\templates\account\password_change.html:22
+msgid "Forgot Password?"
+msgstr ""
+
+#: .\cookbook\templates\account\password_reset.html:7
+#: .\cookbook\templates\account\password_reset.html:13
+#: .\cookbook\templates\account\password_reset_done.html:7
+#: .\cookbook\templates\account\password_reset_done.html:10
+msgid "Password Reset"
+msgstr ""
+
+#: .\cookbook\templates\account\password_reset.html:24
+msgid ""
+"Forgotten your password? Enter your e-mail address below, and we'll send you "
+"an e-mail allowing you to reset it."
+msgstr ""
+
+#: .\cookbook\templates\account\password_reset.html:32
+msgid "Password reset is disabled on this instance."
+msgstr ""
+
+#: .\cookbook\templates\account\password_reset_done.html:16
+msgid ""
+"We have sent you an e-mail. Please contact us if you do not receive it "
+"within a few minutes."
+msgstr ""
+
+#: .\cookbook\templates\account\password_reset_from_key.html:13
+msgid "Bad Token"
+msgstr ""
+
+#: .\cookbook\templates\account\password_reset_from_key.html:25
+#, python-format
+msgid ""
+"The password reset link was invalid, possibly because it has already been "
+"used.\n"
+" Please request a new "
+"password reset."
+msgstr ""
+
+#: .\cookbook\templates\account\password_reset_from_key.html:33
+msgid "change password"
+msgstr ""
+
+#: .\cookbook\templates\account\password_reset_from_key.html:36
+#: .\cookbook\templates\account\password_reset_from_key_done.html:19
+msgid "Your password is now changed."
+msgstr ""
+
+#: .\cookbook\templates\account\password_set.html:6
+#: .\cookbook\templates\account\password_set.html:16
+#: .\cookbook\templates\account\password_set.html:21
+msgid "Set Password"
+msgstr ""
+
+#: .\cookbook\templates\account\signup.html:6
+msgid "Register"
+msgstr ""
+
+#: .\cookbook\templates\account\signup.html:12
+msgid "Create an Account"
+msgstr ""
+
+#: .\cookbook\templates\account\signup.html:42
+#: .\cookbook\templates\socialaccount\signup.html:33
+msgid "I accept the follwoing"
+msgstr ""
+
+#: .\cookbook\templates\account\signup.html:45
+#: .\cookbook\templates\socialaccount\signup.html:36
+msgid "Terms and Conditions"
+msgstr ""
+
+#: .\cookbook\templates\account\signup.html:48
+#: .\cookbook\templates\socialaccount\signup.html:39
+msgid "and"
+msgstr ""
+
+#: .\cookbook\templates\account\signup.html:52
+#: .\cookbook\templates\socialaccount\signup.html:43
+msgid "Privacy Policy"
+msgstr ""
+
+#: .\cookbook\templates\account\signup.html:65
+msgid "Create User"
+msgstr ""
+
+#: .\cookbook\templates\account\signup.html:69
+msgid "Already have an account?"
+msgstr ""
+
+#: .\cookbook\templates\account\signup_closed.html:5
+#: .\cookbook\templates\account\signup_closed.html:11
+msgid "Sign Up Closed"
+msgstr ""
+
+#: .\cookbook\templates\account\signup_closed.html:13
+msgid "We are sorry, but the sign up is currently closed."
+msgstr ""
+
+#: .\cookbook\templates\api_info.html:5 .\cookbook\templates\base.html:372
+#: .\cookbook\templates\rest_framework\api.html:11
+msgid "API Documentation"
+msgstr ""
+
+#: .\cookbook\templates\base.html:108 .\cookbook\templates\index.html:87
+msgid "Recipes"
+msgstr ""
+
+#: .\cookbook\templates\base.html:116
+msgid "Shopping"
+msgstr ""
+
+#: .\cookbook\templates\base.html:159 .\cookbook\views\lists.py:105
+msgid "Foods"
+msgstr ""
+
+#: .\cookbook\templates\base.html:171 .\cookbook\views\lists.py:122
+msgid "Units"
+msgstr ""
+
+#: .\cookbook\templates\base.html:185 .\cookbook\templates\supermarket.html:7
+msgid "Supermarket"
+msgstr ""
+
+#: .\cookbook\templates\base.html:197
+msgid "Supermarket Category"
+msgstr ""
+
+#: .\cookbook\templates\base.html:209 .\cookbook\views\lists.py:171
+msgid "Automations"
+msgstr ""
+
+#: .\cookbook\templates\base.html:223 .\cookbook\views\lists.py:207
+msgid "Files"
+msgstr ""
+
+#: .\cookbook\templates\base.html:235
+msgid "Batch Edit"
+msgstr ""
+
+#: .\cookbook\templates\base.html:247 .\cookbook\templates\history.html:6
+#: .\cookbook\templates\history.html:14
+msgid "History"
+msgstr ""
+
+#: .\cookbook\templates\base.html:261
+#: .\cookbook\templates\ingredient_editor.html:7
+#: .\cookbook\templates\ingredient_editor.html:13
+msgid "Ingredient Editor"
+msgstr ""
+
+#: .\cookbook\templates\base.html:273
+#: .\cookbook\templates\export_response.html:7
+#: .\cookbook\templates\test2.html:14 .\cookbook\templates\test2.html:20
+msgid "Export"
+msgstr ""
+
+#: .\cookbook\templates\base.html:285
+msgid "Properties"
+msgstr ""
+
+#: .\cookbook\templates\base.html:299 .\cookbook\views\lists.py:240
+msgid "Unit Conversions"
+msgstr ""
+
+#: .\cookbook\templates\base.html:316 .\cookbook\templates\index.html:47
+msgid "Import Recipe"
+msgstr ""
+
+#: .\cookbook\templates\base.html:318
+msgid "Create"
+msgstr ""
+
+#: .\cookbook\templates\base.html:331
+#: .\cookbook\templates\generic\list_template.html:14
+msgid "External Recipes"
+msgstr ""
+
+#: .\cookbook\templates\base.html:334 .\cookbook\templates\space_manage.html:15
+msgid "Space Settings"
+msgstr ""
+
+#: .\cookbook\templates\base.html:339 .\cookbook\templates\system.html:13
+msgid "System"
+msgstr ""
+
+#: .\cookbook\templates\base.html:341
+msgid "Admin"
+msgstr ""
+
+#: .\cookbook\templates\base.html:345
+#: .\cookbook\templates\space_overview.html:25
+msgid "Your Spaces"
+msgstr ""
+
+#: .\cookbook\templates\base.html:356
+#: .\cookbook\templates\space_overview.html:6
+msgid "Overview"
+msgstr ""
+
+#: .\cookbook\templates\base.html:366
+msgid "Markdown Guide"
+msgstr ""
+
+#: .\cookbook\templates\base.html:368
+msgid "GitHub"
+msgstr ""
+
+#: .\cookbook\templates\base.html:370
+msgid "Translate Tandoor"
+msgstr ""
+
+#: .\cookbook\templates\base.html:374
+msgid "API Browser"
+msgstr ""
+
+#: .\cookbook\templates\base.html:377
+msgid "Log out"
+msgstr ""
+
+#: .\cookbook\templates\base.html:400
+msgid "You are using the free version of Tandor"
+msgstr ""
+
+#: .\cookbook\templates\base.html:401
+msgid "Upgrade Now"
+msgstr ""
+
+#: .\cookbook\templates\batch\edit.html:6
+msgid "Batch edit Category"
+msgstr ""
+
+#: .\cookbook\templates\batch\edit.html:15
+msgid "Batch edit Recipes"
+msgstr ""
+
+#: .\cookbook\templates\batch\edit.html:20
+msgid "Add the specified keywords to all recipes containing a word"
+msgstr ""
+
+#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:73
+msgid "Sync"
+msgstr ""
+
+#: .\cookbook\templates\batch\monitor.html:10
+msgid "Manage watched Folders"
+msgstr ""
+
+#: .\cookbook\templates\batch\monitor.html:14
+msgid ""
+"On this Page you can manage all storage folder locations that should be "
+"monitored and synced."
+msgstr ""
+
+#: .\cookbook\templates\batch\monitor.html:16
+msgid "The path must be in the following format"
+msgstr ""
+
+#: .\cookbook\templates\batch\monitor.html:20
+#: .\cookbook\templates\forms\edit_import_recipe.html:14
+#: .\cookbook\templates\generic\edit_template.html:23
+#: .\cookbook\templates\generic\new_template.html:23
+#: .\cookbook\templates\settings.html:57
+msgid "Save"
+msgstr ""
+
+#: .\cookbook\templates\batch\monitor.html:21
+msgid "Manage External Storage"
+msgstr ""
+
+#: .\cookbook\templates\batch\monitor.html:28
+msgid "Sync Now!"
+msgstr ""
+
+#: .\cookbook\templates\batch\monitor.html:29
+msgid "Show Recipes"
+msgstr ""
+
+#: .\cookbook\templates\batch\monitor.html:30
+msgid "Show Log"
+msgstr ""
+
+#: .\cookbook\templates\batch\waiting.html:4
+#: .\cookbook\templates\batch\waiting.html:10
+msgid "Importing Recipes"
+msgstr ""
+
+#: .\cookbook\templates\batch\waiting.html:28
+msgid ""
+"This can take a few minutes, depending on the number of recipes in sync, "
+"please wait."
+msgstr ""
+
+#: .\cookbook\templates\books.html:7
+msgid "Recipe Books"
+msgstr ""
+
+#: .\cookbook\templates\export.html:8 .\cookbook\templates\test2.html:6
+msgid "Export Recipes"
+msgstr ""
+
+#: .\cookbook\templates\forms\edit_import_recipe.html:5
+#: .\cookbook\templates\forms\edit_import_recipe.html:9
+msgid "Import new Recipe"
+msgstr ""
+
+#: .\cookbook\templates\forms\edit_internal_recipe.html:7
+msgid "Edit Recipe"
+msgstr ""
+
+#: .\cookbook\templates\generic\delete_template.html:21
+#, python-format
+msgid "Are you sure you want to delete the %(title)s: %(object)s "
+msgstr ""
+
+#: .\cookbook\templates\generic\delete_template.html:22
+msgid "This cannot be undone!"
+msgstr ""
+
+#: .\cookbook\templates\generic\delete_template.html:27
+msgid "Protected"
+msgstr ""
+
+#: .\cookbook\templates\generic\delete_template.html:42
+msgid "Cascade"
+msgstr ""
+
+#: .\cookbook\templates\generic\delete_template.html:73
+msgid "Cancel"
+msgstr ""
+
+#: .\cookbook\templates\generic\edit_template.html:6
+#: .\cookbook\templates\generic\edit_template.html:14
+msgid "Edit"
+msgstr ""
+
+#: .\cookbook\templates\generic\edit_template.html:32
+msgid "View"
+msgstr ""
+
+#: .\cookbook\templates\generic\edit_template.html:36
+msgid "Delete original file"
+msgstr ""
+
+#: .\cookbook\templates\generic\list_template.html:6
+#: .\cookbook\templates\generic\list_template.html:22
+msgid "List"
+msgstr ""
+
+#: .\cookbook\templates\generic\list_template.html:36
+msgid "Filter"
+msgstr ""
+
+#: .\cookbook\templates\generic\list_template.html:41
+msgid "Import all"
+msgstr ""
+
+#: .\cookbook\templates\generic\new_template.html:6
+#: .\cookbook\templates\generic\new_template.html:14
+msgid "New"
+msgstr ""
+
+#: .\cookbook\templates\generic\table_template.html:76
+msgid "previous"
+msgstr ""
+
+#: .\cookbook\templates\generic\table_template.html:98
+msgid "next"
+msgstr ""
+
+#: .\cookbook\templates\history.html:20
+msgid "View Log"
+msgstr ""
+
+#: .\cookbook\templates\history.html:24
+msgid "Cook Log"
+msgstr ""
+
+#: .\cookbook\templates\import_response.html:7 .\cookbook\views\delete.py:86
+#: .\cookbook\views\edit.py:191
+msgid "Import"
+msgstr ""
+
+#: .\cookbook\templates\include\storage_backend_warning.html:4
+msgid "Security Warning"
+msgstr ""
+
+#: .\cookbook\templates\include\storage_backend_warning.html:5
+msgid ""
+"\n"
+" The Password and Token field are stored as plain text "
+"inside the database.\n"
+" This is necessary because they are needed to make API requests, but "
+"it also increases the risk of\n"
+" someone stealing it.
\n"
+" To limit the possible damage tokens or accounts with limited access "
+"can be used.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\index.html:29
+msgid "Search recipe ..."
+msgstr ""
+
+#: .\cookbook\templates\index.html:44
+msgid "New Recipe"
+msgstr ""
+
+#: .\cookbook\templates\index.html:53
+msgid "Advanced Search"
+msgstr ""
+
+#: .\cookbook\templates\index.html:57
+msgid "Reset Search"
+msgstr ""
+
+#: .\cookbook\templates\index.html:85
+msgid "Last viewed"
+msgstr ""
+
+#: .\cookbook\templates\index.html:94
+msgid "Log in to view recipes"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:5
+#: .\cookbook\templates\markdown_info.html:13
+msgid "Markdown Info"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:14
+msgid ""
+"\n"
+" Markdown is lightweight markup language that can be used to format "
+"plain text easily.\n"
+" This site uses the Python Markdown library to\n"
+" convert your text into nice looking HTML. Its full markdown "
+"documentation can be found\n"
+" here.\n"
+" An incomplete but most likely sufficient documentation can be found "
+"below.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:25
+msgid "Headers"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:54
+msgid "Formatting"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:56
+#: .\cookbook\templates\markdown_info.html:72
+msgid "Line breaks are inserted by adding two spaces after the end of a line"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:57
+#: .\cookbook\templates\markdown_info.html:73
+msgid "or by leaving a blank line in between."
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:59
+#: .\cookbook\templates\markdown_info.html:74
+msgid "This text is bold"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:60
+#: .\cookbook\templates\markdown_info.html:75
+msgid "This text is italic"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:61
+#: .\cookbook\templates\markdown_info.html:77
+msgid "Blockquotes are also possible"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:84
+msgid "Lists"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:85
+msgid ""
+"Lists can ordered or unordered. It is important to leave a blank line "
+"before the list!"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:87
+#: .\cookbook\templates\markdown_info.html:108
+msgid "Ordered List"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:89
+#: .\cookbook\templates\markdown_info.html:90
+#: .\cookbook\templates\markdown_info.html:91
+#: .\cookbook\templates\markdown_info.html:110
+#: .\cookbook\templates\markdown_info.html:111
+#: .\cookbook\templates\markdown_info.html:112
+msgid "unordered list item"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:93
+#: .\cookbook\templates\markdown_info.html:114
+msgid "Unordered List"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:95
+#: .\cookbook\templates\markdown_info.html:96
+#: .\cookbook\templates\markdown_info.html:97
+#: .\cookbook\templates\markdown_info.html:116
+#: .\cookbook\templates\markdown_info.html:117
+#: .\cookbook\templates\markdown_info.html:118
+msgid "ordered list item"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:125
+msgid "Images & Links"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:126
+msgid ""
+"Links can be formatted with Markdown. This application also allows to paste "
+"links directly into markdown fields without any formatting."
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:132
+#: .\cookbook\templates\markdown_info.html:145
+msgid "This will become an image"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:152
+msgid "Tables"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:153
+msgid ""
+"Markdown tables are hard to create by hand. It is recommended to use a table "
+"editor like this one."
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:155
+#: .\cookbook\templates\markdown_info.html:157
+#: .\cookbook\templates\markdown_info.html:171
+#: .\cookbook\templates\markdown_info.html:177
+msgid "Table"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:155
+#: .\cookbook\templates\markdown_info.html:172
+msgid "Header"
+msgstr ""
+
+#: .\cookbook\templates\markdown_info.html:157
+#: .\cookbook\templates\markdown_info.html:178
+msgid "Cell"
+msgstr ""
+
+#: .\cookbook\templates\no_groups_info.html:5
+#: .\cookbook\templates\no_groups_info.html:12
+msgid "No Permissions"
+msgstr ""
+
+#: .\cookbook\templates\no_groups_info.html:17
+msgid "You do not have any groups and therefor cannot use this application."
+msgstr ""
+
+#: .\cookbook\templates\no_groups_info.html:18
+#: .\cookbook\templates\no_perm_info.html:15
+msgid "Please contact your administrator."
+msgstr ""
+
+#: .\cookbook\templates\no_perm_info.html:5
+#: .\cookbook\templates\no_perm_info.html:12
+msgid "No Permission"
+msgstr ""
+
+#: .\cookbook\templates\no_perm_info.html:15
+msgid ""
+"You do not have the required permissions to view this page or perform this "
+"action."
+msgstr ""
+
+#: .\cookbook\templates\offline.html:6
+msgid "Offline"
+msgstr ""
+
+#: .\cookbook\templates\offline.html:19
+msgid "You are currently offline!"
+msgstr ""
+
+#: .\cookbook\templates\offline.html:20
+msgid ""
+"The recipes listed below are available for offline viewing because you have "
+"recently viewed them. Keep in mind that data might be outdated."
+msgstr ""
+
+#: .\cookbook\templates\openid\login.html:27
+#: .\cookbook\templates\socialaccount\authentication_error.html:27
+msgid "Back"
+msgstr ""
+
+#: .\cookbook\templates\profile.html:7
+msgid "Profile"
+msgstr ""
+
+#: .\cookbook\templates\recipe_view.html:41
+msgid "by"
+msgstr ""
+
+#: .\cookbook\templates\recipe_view.html:59 .\cookbook\views\delete.py:144
+#: .\cookbook\views\edit.py:171
+msgid "Comment"
+msgstr ""
+
+#: .\cookbook\templates\rest_framework\api.html:5
+msgid "Recipe Home"
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:5
+#: .\cookbook\templates\search_info.html:9
+#: .\cookbook\templates\settings.html:24
+msgid "Search Settings"
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:10
+msgid ""
+"\n"
+" Creating the best search experience is complicated and weighs "
+"heavily on your personal configuration. \n"
+" Changing any of the search settings can have significant impact on "
+"the speed and quality of the results.\n"
+" Search Methods, Trigrams and Full Text Search configurations are "
+"only available if you are using Postgres for your database.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:19
+msgid "Search Methods"
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:23
+msgid ""
+" \n"
+" Full text searches attempt to normalize the words provided to "
+"match common variants. For example: 'forked', 'forking', 'forks' will all "
+"normalize to 'fork'.\n"
+" There are several methods available, described below, that will "
+"control how the search behavior should react when multiple words are "
+"searched.\n"
+" Full technical details on how these operate can be viewed on Postgresql's website.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:29
+msgid ""
+" \n"
+" Simple searches ignore punctuation and common words such as "
+"'the', 'a', 'and'. And will treat separate words as required.\n"
+" Searching for 'apple or flour' will return any recipe that "
+"includes both 'apple' and 'flour' anywhere in the fields that have been "
+"selected for a full text search.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:34
+msgid ""
+" \n"
+" Phrase searches ignore punctuation, but will search for all of "
+"the words in the exact order provided.\n"
+" Searching for 'apple or flour' will only return a recipe that "
+"includes the exact phrase 'apple or flour' in any of the fields that have "
+"been selected for a full text search.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:39
+msgid ""
+" \n"
+" Web searches simulate functionality found on many web search "
+"sites supporting special syntax.\n"
+" Placing quotes around several words will convert those words "
+"into a phrase.\n"
+" 'or' is recognized as searching for the word (or phrase) "
+"immediately before 'or' OR the word (or phrase) directly after.\n"
+" '-' is recognized as searching for recipes that do not include "
+"the word (or phrase) that comes immediately after. \n"
+" For example searching for 'apple pie' or cherry -butter will "
+"return any recipe that includes the phrase 'apple pie' or the word "
+"'cherry' \n"
+" in any field included in the full text search but exclude any "
+"recipe that has the word 'butter' in any field included.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:48
+msgid ""
+" \n"
+" Raw search is similar to Web except will take puncuation "
+"operators such as '|', '&' and '()'\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:59
+msgid ""
+" \n"
+" Another approach to searching that also requires Postgresql is "
+"fuzzy search or trigram similarity. A trigram is a group of three "
+"consecutive characters.\n"
+" For example searching for 'apple' will create x trigrams 'app', "
+"'ppl', 'ple' and will create a score of how closely words match the "
+"generated trigrams.\n"
+" One benefit of searching trigams is that a search for 'sandwich' "
+"will find misspelled words such as 'sandwhich' that would be missed by other "
+"methods.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:69
+msgid "Search Fields"
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:73
+msgid ""
+" \n"
+" Unaccent is a special case in that it enables searching a field "
+"'unaccented' for each search style attempting to ignore accented values. \n"
+" For example when you enable unaccent for 'Name' any search "
+"(starts with, contains, trigram) will attempt the search ignoring accented "
+"characters.\n"
+" \n"
+" For the other options, you can enable search on any or all "
+"fields and they will be combined together with an assumed 'OR'.\n"
+" For example enabling 'Name' for Starts With, 'Name' and "
+"'Description' for Partial Match and 'Ingredients' and 'Keywords' for Full "
+"Search\n"
+" and searching for 'apple' will generate a search that will "
+"return recipes that have:\n"
+" - A recipe name that starts with 'apple'\n"
+" - OR a recipe name that contains 'apple'\n"
+" - OR a recipe description that contains 'apple'\n"
+" - OR a recipe that will have a full text search match ('apple' "
+"or 'apples') in ingredients\n"
+" - OR a recipe that will have a full text search match in "
+"Keywords\n"
+"\n"
+" Combining too many fields in too many types of search can have a "
+"negative impact on performance, create duplicate results or return "
+"unexpected results.\n"
+" For example, enabling fuzzy search or partial matches will "
+"interfere with web search methods. \n"
+" Searching for 'apple -pie' with fuzzy search and full text "
+"search will return the recipe Apple Pie. Though it is not included in the "
+"full text results, it does match the trigram results.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:95
+msgid "Search Index"
+msgstr ""
+
+#: .\cookbook\templates\search_info.html:99
+msgid ""
+" \n"
+" Trigram search and Full Text Search both rely on database "
+"indexes to perform effectively. \n"
+" You can rebuild the indexes on all fields in the Admin page for "
+"Recipes and selecting all recipes and running 'rebuild index for selected "
+"recipes'\n"
+" You can also rebuild indexes at the command line by executing "
+"the management command 'python manage.py rebuildindex'\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\settings.html:25
+msgid ""
+"There are many options to configure the search depending on your personal "
+"preferences."
+msgstr ""
+
+#: .\cookbook\templates\settings.html:26
+msgid ""
+"Usually you do not need to configure any of them and can just stick "
+"with either the default or one of the following presets."
+msgstr ""
+
+#: .\cookbook\templates\settings.html:27
+msgid ""
+"If you do want to configure the search you can read about the different "
+"options here."
+msgstr ""
+
+#: .\cookbook\templates\settings.html:32
+msgid "Fuzzy"
+msgstr ""
+
+#: .\cookbook\templates\settings.html:33
+msgid ""
+"Find what you need even if your search or the recipe contains typos. Might "
+"return more results than needed to make sure you find what you are looking "
+"for."
+msgstr ""
+
+#: .\cookbook\templates\settings.html:34
+msgid "This is the default behavior"
+msgstr ""
+
+#: .\cookbook\templates\settings.html:37 .\cookbook\templates\settings.html:46
+msgid "Apply"
+msgstr ""
+
+#: .\cookbook\templates\settings.html:42
+msgid "Precise"
+msgstr ""
+
+#: .\cookbook\templates\settings.html:43
+msgid ""
+"Allows fine control over search results but might not return results if too "
+"many spelling mistakes are made."
+msgstr ""
+
+#: .\cookbook\templates\settings.html:44
+msgid "Perfect for large Databases"
+msgstr ""
+
+#: .\cookbook\templates\setup.html:6 .\cookbook\templates\system.html:5
+msgid "Cookbook Setup"
+msgstr ""
+
+#: .\cookbook\templates\setup.html:14
+msgid "Setup"
+msgstr ""
+
+#: .\cookbook\templates\setup.html:15
+msgid ""
+"To start using this application you must first create a superuser account."
+msgstr ""
+
+#: .\cookbook\templates\setup.html:20
+msgid "Create Superuser account"
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\authentication_error.html:7
+#: .\cookbook\templates\socialaccount\authentication_error.html:23
+msgid "Social Network Login Failure"
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\authentication_error.html:25
+msgid ""
+"An error occurred while attempting to login via your social network account."
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\connections.html:4
+#: .\cookbook\templates\socialaccount\connections.html:15
+msgid "Account Connections"
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\connections.html:11
+msgid "Social"
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\connections.html:18
+msgid ""
+"You can sign in to your account using any of the following third party\n"
+" accounts:"
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\connections.html:52
+msgid ""
+"You currently have no social network accounts connected to this account."
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\connections.html:55
+msgid "Add a 3rd Party Account"
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\login.html:5
+#: .\cookbook\templates\socialaccount\signup.html:5
+msgid "Signup"
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\login.html:9
+#, python-format
+msgid "Connect %(provider)s"
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\login.html:11
+#, python-format
+msgid "You are about to connect a new third party account from %(provider)s."
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\login.html:13
+#, python-format
+msgid "Sign In Via %(provider)s"
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\login.html:15
+#, python-format
+msgid "You are about to sign in using a third party account from %(provider)s."
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\login.html:20
+msgid "Continue"
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\signup.html:10
+#, python-format
+msgid ""
+"You are about to use your\n"
+" %(provider_name)s account to login to\n"
+" %(site_name)s. As a final step, please complete the following form:"
+msgstr ""
+
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:23
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:31
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:39
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:47
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:55
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:63
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:71
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:79
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:87
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:95
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:103
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:111
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:119
+#: .\cookbook\templates\socialaccount\snippets\provider_list.html:127
+msgid "Sign in using"
+msgstr ""
+
+#: .\cookbook\templates\space_manage.html:26
+msgid "Space:"
+msgstr ""
+
+#: .\cookbook\templates\space_manage.html:27
+msgid "Manage Subscription"
+msgstr ""
+
+#: .\cookbook\templates\space_overview.html:13 .\cookbook\views\delete.py:216
+msgid "Space"
+msgstr ""
+
+#: .\cookbook\templates\space_overview.html:17
+msgid ""
+"Recipes, foods, shopping lists and more are organized in spaces of one or "
+"more people."
+msgstr ""
+
+#: .\cookbook\templates\space_overview.html:18
+msgid ""
+"You can either be invited into an existing space or create your own one."
+msgstr ""
+
+#: .\cookbook\templates\space_overview.html:53
+msgid "Owner"
+msgstr ""
+
+#: .\cookbook\templates\space_overview.html:57
+msgid "Leave Space"
+msgstr ""
+
+#: .\cookbook\templates\space_overview.html:78
+#: .\cookbook\templates\space_overview.html:88
+msgid "Join Space"
+msgstr ""
+
+#: .\cookbook\templates\space_overview.html:81
+msgid "Join an existing space."
+msgstr ""
+
+#: .\cookbook\templates\space_overview.html:83
+msgid ""
+"To join an existing space either enter your invite token or click on the "
+"invite link the space owner send you."
+msgstr ""
+
+#: .\cookbook\templates\space_overview.html:96
+#: .\cookbook\templates\space_overview.html:105
+msgid "Create Space"
+msgstr ""
+
+#: .\cookbook\templates\space_overview.html:99
+msgid "Create your own recipe space."
+msgstr ""
+
+#: .\cookbook\templates\space_overview.html:101
+msgid "Start your own recipe space and invite other users to it."
+msgstr ""
+
+#: .\cookbook\templates\system.html:14
+msgid ""
+"\n"
+" Django Recipes is an open source free software application. It can "
+"be found on\n"
+" GitHub.\n"
+" Changelogs can be found here.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\system.html:20
+msgid "System Information"
+msgstr ""
+
+#: .\cookbook\templates\system.html:41
+msgid ""
+"\n"
+" You need to execute version.py
in your update "
+"script to generate version information (done automatically in docker).\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\system.html:46
+msgid "Media Serving"
+msgstr ""
+
+#: .\cookbook\templates\system.html:47 .\cookbook\templates\system.html:61
+#: .\cookbook\templates\system.html:75
+msgid "Warning"
+msgstr ""
+
+#: .\cookbook\templates\system.html:47 .\cookbook\templates\system.html:61
+#: .\cookbook\templates\system.html:75 .\cookbook\templates\system.html:88
+msgid "Ok"
+msgstr ""
+
+#: .\cookbook\templates\system.html:49
+msgid ""
+"Serving media files directly using gunicorn/python is not recommend!\n"
+" Please follow the steps described\n"
+" here to update\n"
+" your installation.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\system.html:55 .\cookbook\templates\system.html:70
+#: .\cookbook\templates\system.html:83 .\cookbook\templates\system.html:95
+msgid "Everything is fine!"
+msgstr ""
+
+#: .\cookbook\templates\system.html:59
+msgid "Secret Key"
+msgstr ""
+
+#: .\cookbook\templates\system.html:63
+msgid ""
+"\n"
+" You do not have a SECRET_KEY
configured in your "
+".env
file. Django defaulted to the\n"
+" standard key\n"
+" provided with the installation which is publicly know and "
+"insecure! Please set\n"
+" SECRET_KEY
int the .env
configuration "
+"file.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\system.html:73
+msgid "Debug Mode"
+msgstr ""
+
+#: .\cookbook\templates\system.html:77
+msgid ""
+"\n"
+" This application is still running in debug mode. This is most "
+"likely not needed. Turn of debug mode by\n"
+" setting\n"
+" DEBUG=0
int the .env
configuration "
+"file.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\system.html:86
+msgid "Database"
+msgstr ""
+
+#: .\cookbook\templates\system.html:88
+msgid "Info"
+msgstr ""
+
+#: .\cookbook\templates\system.html:90
+msgid ""
+"\n"
+" This application is not running with a Postgres database "
+"backend. This is ok but not recommended as some\n"
+" features only work with postgres databases.\n"
+" "
+msgstr ""
+
+#: .\cookbook\templates\url_import.html:8
+msgid "URL Import"
+msgstr ""
+
+#: .\cookbook\views\api.py:120 .\cookbook\views\api.py:214
+msgid "Parameter updated_at incorrectly formatted"
+msgstr ""
+
+#: .\cookbook\views\api.py:234 .\cookbook\views\api.py:341
+#, python-brace-format
+msgid "No {self.basename} with id {pk} exists"
+msgstr ""
+
+#: .\cookbook\views\api.py:238
+msgid "Cannot merge with the same object!"
+msgstr ""
+
+#: .\cookbook\views\api.py:245
+#, python-brace-format
+msgid "No {self.basename} with id {target} exists"
+msgstr ""
+
+#: .\cookbook\views\api.py:250
+msgid "Cannot merge with child object!"
+msgstr ""
+
+#: .\cookbook\views\api.py:286
+#, python-brace-format
+msgid "{source.name} was merged successfully with {target.name}"
+msgstr ""
+
+#: .\cookbook\views\api.py:292
+#, python-brace-format
+msgid "An error occurred attempting to merge {source.name} with {target.name}"
+msgstr ""
+
+#: .\cookbook\views\api.py:350
+#, python-brace-format
+msgid "{child.name} was moved successfully to the root."
+msgstr ""
+
+#: .\cookbook\views\api.py:353 .\cookbook\views\api.py:371
+msgid "An error occurred attempting to move "
+msgstr ""
+
+#: .\cookbook\views\api.py:356
+msgid "Cannot move an object to itself!"
+msgstr ""
+
+#: .\cookbook\views\api.py:362
+#, python-brace-format
+msgid "No {self.basename} with id {parent} exists"
+msgstr ""
+
+#: .\cookbook\views\api.py:368
+#, python-brace-format
+msgid "{child.name} was moved successfully to parent {parent.name}"
+msgstr ""
+
+#: .\cookbook\views\api.py:586
+#, python-brace-format
+msgid "{obj.name} was removed from the shopping list."
+msgstr ""
+
+#: .\cookbook\views\api.py:591 .\cookbook\views\api.py:1006
+#: .\cookbook\views\api.py:1019
+#, python-brace-format
+msgid "{obj.name} was added to the shopping list."
+msgstr ""
+
+#: .\cookbook\views\api.py:778
+msgid "ID of recipe a step is part of. For multiple repeat parameter."
+msgstr ""
+
+#: .\cookbook\views\api.py:780
+msgid "Query string matched (fuzzy) against object name."
+msgstr ""
+
+#: .\cookbook\views\api.py:824
+msgid ""
+"Query string matched (fuzzy) against recipe name. In the future also "
+"fulltext search."
+msgstr ""
+
+#: .\cookbook\views\api.py:826
+msgid ""
+"ID of keyword a recipe should have. For multiple repeat parameter. "
+"Equivalent to keywords_or"
+msgstr ""
+
+#: .\cookbook\views\api.py:829
+msgid ""
+"Keyword IDs, repeat for multiple. Return recipes with any of the keywords"
+msgstr ""
+
+#: .\cookbook\views\api.py:832
+msgid ""
+"Keyword IDs, repeat for multiple. Return recipes with all of the keywords."
+msgstr ""
+
+#: .\cookbook\views\api.py:835
+msgid ""
+"Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords."
+msgstr ""
+
+#: .\cookbook\views\api.py:838
+msgid ""
+"Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords."
+msgstr ""
+
+#: .\cookbook\views\api.py:840
+msgid "ID of food a recipe should have. For multiple repeat parameter."
+msgstr ""
+
+#: .\cookbook\views\api.py:843
+msgid "Food IDs, repeat for multiple. Return recipes with any of the foods"
+msgstr ""
+
+#: .\cookbook\views\api.py:845
+msgid "Food IDs, repeat for multiple. Return recipes with all of the foods."
+msgstr ""
+
+#: .\cookbook\views\api.py:847
+msgid "Food IDs, repeat for multiple. Exclude recipes with any of the foods."
+msgstr ""
+
+#: .\cookbook\views\api.py:849
+msgid "Food IDs, repeat for multiple. Exclude recipes with all of the foods."
+msgstr ""
+
+#: .\cookbook\views\api.py:850
+msgid "ID of unit a recipe should have."
+msgstr ""
+
+#: .\cookbook\views\api.py:852
+msgid ""
+"Rating a recipe should have or greater. [0 - 5] Negative value filters "
+"rating less than."
+msgstr ""
+
+#: .\cookbook\views\api.py:853
+msgid "ID of book a recipe should be in. For multiple repeat parameter."
+msgstr ""
+
+#: .\cookbook\views\api.py:855
+msgid "Book IDs, repeat for multiple. Return recipes with any of the books"
+msgstr ""
+
+#: .\cookbook\views\api.py:857
+msgid "Book IDs, repeat for multiple. Return recipes with all of the books."
+msgstr ""
+
+#: .\cookbook\views\api.py:859
+msgid "Book IDs, repeat for multiple. Exclude recipes with any of the books."
+msgstr ""
+
+#: .\cookbook\views\api.py:861
+msgid "Book IDs, repeat for multiple. Exclude recipes with all of the books."
+msgstr ""
+
+#: .\cookbook\views\api.py:863
+msgid "If only internal recipes should be returned. [true/false]"
+msgstr ""
+
+#: .\cookbook\views\api.py:865
+msgid "Returns the results in randomized order. [true/false]"
+msgstr ""
+
+#: .\cookbook\views\api.py:867
+msgid "Returns new results first in search results. [true/false]"
+msgstr ""
+
+#: .\cookbook\views\api.py:869
+msgid ""
+"Filter recipes cooked X times or more. Negative values returns cooked less "
+"than X times"
+msgstr ""
+
+#: .\cookbook\views\api.py:871
+msgid ""
+"Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on "
+"or before date."
+msgstr ""
+
+#: .\cookbook\views\api.py:873
+msgid ""
+"Filter recipes created on or after YYYY-MM-DD. Prepending - filters on or "
+"before date."
+msgstr ""
+
+#: .\cookbook\views\api.py:875
+msgid ""
+"Filter recipes updated on or after YYYY-MM-DD. Prepending - filters on or "
+"before date."
+msgstr ""
+
+#: .\cookbook\views\api.py:877
+msgid ""
+"Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending - filters on "
+"or before date."
+msgstr ""
+
+#: .\cookbook\views\api.py:879
+msgid "Filter recipes that can be made with OnHand food. [true/false]"
+msgstr ""
+
+#: .\cookbook\views\api.py:1099
+msgid ""
+"Returns the shopping list entry with a primary key of id. Multiple values "
+"allowed."
+msgstr ""
+
+#: .\cookbook\views\api.py:1104
+msgid ""
+"Filter shopping list entries on checked. [true, false, both, recent"
+"b>]
- recent includes unchecked items and recently completed items."
+msgstr ""
+
+#: .\cookbook\views\api.py:1107
+msgid "Returns the shopping list entries sorted by supermarket category order."
+msgstr ""
+
+#: .\cookbook\views\api.py:1343
+msgid "Nothing to do."
+msgstr ""
+
+#: .\cookbook\views\api.py:1384
+msgid "Invalid Url"
+msgstr ""
+
+#: .\cookbook\views\api.py:1391
+msgid "Connection Refused."
+msgstr ""
+
+#: .\cookbook\views\api.py:1396
+msgid "Bad URL Schema."
+msgstr ""
+
+#: .\cookbook\views\api.py:1423
+msgid "No usable data could be found."
+msgstr ""
+
+#: .\cookbook\views\api.py:1516 .\cookbook\views\import_export.py:117
+msgid "Importing is not implemented for this provider"
+msgstr ""
+
+#: .\cookbook\views\api.py:1595 .\cookbook\views\data.py:31
+#: .\cookbook\views\edit.py:120 .\cookbook\views\new.py:90
+msgid "This feature is not yet available in the hosted version of tandoor!"
+msgstr ""
+
+#: .\cookbook\views\api.py:1617
+msgid "Sync successful!"
+msgstr ""
+
+#: .\cookbook\views\api.py:1622
+msgid "Error synchronizing with Storage"
+msgstr ""
+
+#: .\cookbook\views\data.py:100
+#, python-format
+msgid "Batch edit done. %(count)d recipe was updated."
+msgid_plural "Batch edit done. %(count)d Recipes where updated."
+msgstr[0] ""
+msgstr[1] ""
+
+#: .\cookbook\views\delete.py:98
+msgid "Monitor"
+msgstr ""
+
+#: .\cookbook\views\delete.py:122 .\cookbook\views\lists.py:62
+#: .\cookbook\views\new.py:96
+msgid "Storage Backend"
+msgstr ""
+
+#: .\cookbook\views\delete.py:132
+msgid ""
+"Could not delete this storage backend as it is used in at least one monitor."
+msgstr ""
+
+#: .\cookbook\views\delete.py:155
+msgid "Recipe Book"
+msgstr ""
+
+#: .\cookbook\views\delete.py:167
+msgid "Bookmarks"
+msgstr ""
+
+#: .\cookbook\views\delete.py:189
+msgid "Invite Link"
+msgstr ""
+
+#: .\cookbook\views\delete.py:200
+msgid "Space Membership"
+msgstr ""
+
+#: .\cookbook\views\edit.py:116
+msgid "You cannot edit this storage!"
+msgstr ""
+
+#: .\cookbook\views\edit.py:140
+msgid "Storage saved!"
+msgstr ""
+
+#: .\cookbook\views\edit.py:146
+msgid "There was an error updating this storage backend!"
+msgstr ""
+
+#: .\cookbook\views\edit.py:239
+msgid "Changes saved!"
+msgstr ""
+
+#: .\cookbook\views\edit.py:243
+msgid "Error saving changes!"
+msgstr ""
+
+#: .\cookbook\views\import_export.py:104
+msgid ""
+"The PDF Exporter is not enabled on this instance as it is still in an "
+"experimental state."
+msgstr ""
+
+#: .\cookbook\views\lists.py:24
+msgid "Import Log"
+msgstr ""
+
+#: .\cookbook\views\lists.py:37
+msgid "Discovery"
+msgstr ""
+
+#: .\cookbook\views\lists.py:47
+msgid "Shopping List"
+msgstr ""
+
+#: .\cookbook\views\lists.py:76
+msgid "Invite Links"
+msgstr ""
+
+#: .\cookbook\views\lists.py:139
+msgid "Supermarkets"
+msgstr ""
+
+#: .\cookbook\views\lists.py:155
+msgid "Shopping Categories"
+msgstr ""
+
+#: .\cookbook\views\lists.py:187
+msgid "Custom Filters"
+msgstr ""
+
+#: .\cookbook\views\lists.py:224
+msgid "Steps"
+msgstr ""
+
+#: .\cookbook\views\lists.py:255
+msgid "Property Types"
+msgstr ""
+
+#: .\cookbook\views\new.py:121
+msgid "Imported new recipe!"
+msgstr ""
+
+#: .\cookbook\views\new.py:124
+msgid "There was an error importing this recipe!"
+msgstr ""
+
+#: .\cookbook\views\views.py:69 .\cookbook\views\views.py:187
+#: .\cookbook\views\views.py:209 .\cookbook\views\views.py:395
+msgid "This feature is not available in the demo version!"
+msgstr ""
+
+#: .\cookbook\views\views.py:85
+msgid ""
+"You have successfully created your own recipe space. Start by adding some "
+"recipes or invite other people to join you."
+msgstr ""
+
+#: .\cookbook\views\views.py:139
+msgid "You do not have the required permissions to perform this action!"
+msgstr ""
+
+#: .\cookbook\views\views.py:150
+msgid "Comment saved!"
+msgstr ""
+
+#: .\cookbook\views\views.py:249
+msgid "You must select at least one field to search!"
+msgstr ""
+
+#: .\cookbook\views\views.py:254
+msgid ""
+"To use this search method you must select at least one full text search "
+"field!"
+msgstr ""
+
+#: .\cookbook\views\views.py:258
+msgid "Fuzzy search is not compatible with this search method!"
+msgstr ""
+
+#: .\cookbook\views\views.py:334
+msgid ""
+"The setup page can only be used to create the first user! If you have "
+"forgotten your superuser credentials please consult the django documentation "
+"on how to reset passwords."
+msgstr ""
+
+#: .\cookbook\views\views.py:341
+msgid "Passwords dont match!"
+msgstr ""
+
+#: .\cookbook\views\views.py:349
+msgid "User has been created, please login!"
+msgstr ""
+
+#: .\cookbook\views\views.py:365
+msgid "Malformed Invite Link supplied!"
+msgstr ""
+
+#: .\cookbook\views\views.py:382
+msgid "Successfully joined space."
+msgstr ""
+
+#: .\cookbook\views\views.py:388
+msgid "Invite Link not valid or already used!"
+msgstr ""
+
+#: .\cookbook\views\views.py:405
+msgid ""
+"Reporting share links is not enabled for this instance. Please notify the "
+"page administrator to report problems."
+msgstr ""
+
+#: .\cookbook\views\views.py:411
+msgid ""
+"Recipe sharing link has been disabled! For additional information please "
+"contact the page administrator."
+msgstr ""
diff --git a/cookbook/locale/hu_HU/LC_MESSAGES/django.po b/cookbook/locale/hu_HU/LC_MESSAGES/django.po
index 8193bf1a82..85b1deeb66 100644
--- a/cookbook/locale/hu_HU/LC_MESSAGES/django.po
+++ b/cookbook/locale/hu_HU/LC_MESSAGES/django.po
@@ -11,8 +11,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
-"PO-Revision-Date: 2023-04-12 11:55+0000\n"
-"Last-Translator: noxonad \n"
+"PO-Revision-Date: 2023-10-20 14:05+0000\n"
+"Last-Translator: Ferenc \n"
"Language-Team: Hungarian \n"
"Language: hu_HU\n"
@@ -99,7 +99,7 @@ msgstr ""
#: .\cookbook\forms.py:74
msgid "Users with whom newly created meal plans should be shared by default."
msgstr ""
-"Azok a felhasználók, akikkel az újonnan létrehozott étkezési terveket "
+"Azok a felhasználók, akikkel az újonnan létrehozott menüterveket "
"alapértelmezés szerint meg kell osztani."
#: .\cookbook\forms.py:75
@@ -135,8 +135,7 @@ msgstr "A navigációs sávot az oldal tetejére rögzíti."
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
-msgstr ""
-"Automatikusan hozzáadja az étkezési terv hozzávalóit a bevásárlólistához."
+msgstr "Automatikusan hozzáadja a menüterv hozzávalóit a bevásárlólistához."
#: .\cookbook\forms.py:84
msgid "Exclude ingredients that are on hand."
@@ -360,10 +359,8 @@ msgid "Partial Match"
msgstr "Részleges találat"
#: .\cookbook\forms.py:480
-#, fuzzy
-#| msgid "Starts Wtih"
msgid "Starts With"
-msgstr "Kezdődik a következővel"
+msgstr "Ezzel kezdődik"
#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
@@ -387,16 +384,16 @@ msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
msgstr ""
-"Amikor étkezési tervet ad hozzá a bevásárlólistához (kézzel vagy "
-"automatikusan), vegye fel az összes kapcsolódó receptet."
+"Amikor menütervet ad hozzá a bevásárlólistához (kézzel vagy automatikusan), "
+"vegye fel az összes kapcsolódó receptet."
#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
msgstr ""
-"Amikor étkezési tervet ad hozzá a bevásárlólistához (kézzel vagy "
-"automatikusan), zárja ki a kéznél lévő összetevőket."
+"Amikor menütervet ad hozzá a bevásárlólistához (kézzel vagy automatikusan), "
+"zárja ki a kéznél lévő összetevőket."
#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
@@ -436,7 +433,7 @@ msgstr "Automatikus szinkronizálás"
#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
-msgstr "Automatikus étkezési terv hozzáadása"
+msgstr "Menüterv automatikus hozzáadása"
#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
@@ -490,6 +487,7 @@ msgstr "A receptek számának megjelenítése a keresési szűrőkön"
#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""
+"Használja a többes számot az egységek és az ételek esetében ezen a helyen."
#: .\cookbook\helper\AllAuthCustomAdapter.py:39
msgid ""
@@ -549,29 +547,27 @@ msgstr ""
#: .\cookbook\helper\recipe_url_import.py:268
msgid "knead"
-msgstr ""
+msgstr "dagasztás"
#: .\cookbook\helper\recipe_url_import.py:269
msgid "thicken"
-msgstr ""
+msgstr "sűrítés"
#: .\cookbook\helper\recipe_url_import.py:270
msgid "warm up"
-msgstr ""
+msgstr "melegítés"
#: .\cookbook\helper\recipe_url_import.py:271
msgid "ferment"
-msgstr ""
+msgstr "fermentálás"
#: .\cookbook\helper\recipe_url_import.py:272
msgid "sous-vide"
-msgstr ""
+msgstr "sous-vide"
#: .\cookbook\helper\shopping_helper.py:157
-#, fuzzy
-#| msgid "You must supply a created_by"
msgid "You must supply a servings size"
-msgstr "Meg kell adnia egy created_by"
+msgstr "Meg kell adnia az adagok nagyságát"
#: .\cookbook\helper\template_helper.py:79
#: .\cookbook\helper\template_helper.py:81
@@ -581,11 +577,11 @@ msgstr "Nem sikerült elemezni a sablon kódját."
#: .\cookbook\integration\copymethat.py:44
#: .\cookbook\integration\melarecipes.py:37
msgid "Favorite"
-msgstr ""
+msgstr "Kedvenc"
#: .\cookbook\integration\copymethat.py:50
msgid "I made this"
-msgstr ""
+msgstr "Elkészítettem"
#: .\cookbook\integration\integration.py:218
msgid ""
@@ -613,10 +609,8 @@ msgid "Imported %s recipes."
msgstr "Importálva %s recept."
#: .\cookbook\integration\openeats.py:26
-#, fuzzy
-#| msgid "Recipe Home"
msgid "Recipe source:"
-msgstr "Recipe Home"
+msgstr "Recept forrása:"
#: .\cookbook\integration\paprika.py:49
msgid "Notes"
@@ -632,10 +626,8 @@ msgstr "Forrás"
#: .\cookbook\integration\recettetek.py:54
#: .\cookbook\integration\recipekeeper.py:70
-#, fuzzy
-#| msgid "Import Log"
msgid "Imported from"
-msgstr "Import napló"
+msgstr "Importálva a"
#: .\cookbook\integration\saffron.py:23
msgid "Servings"
@@ -662,12 +654,10 @@ msgid "Rebuilds full text search index on Recipe"
msgstr "Újraépíti a teljes szöveges keresési indexet a Recept oldalon"
#: .\cookbook\management\commands\rebuildindex.py:18
-#, fuzzy
-#| msgid "Only Postgress databases use full text search, no index to rebuild"
msgid "Only Postgresql databases use full text search, no index to rebuild"
msgstr ""
-"Csak a Postgress adatbázisok használnak teljes szöveges keresést, nincs "
-"újjáépítendő index"
+"Csak a Postgresql adatbázisok használják a teljes szöveges keresést, nem "
+"kell indexet újjáépíteni"
#: .\cookbook\management\commands\rebuildindex.py:29
msgid "Recipe index rebuild complete."
@@ -711,7 +701,7 @@ msgstr "Keresés"
#: .\cookbook\templates\meal_plan.html:7 .\cookbook\views\delete.py:178
#: .\cookbook\views\edit.py:211 .\cookbook\views\new.py:179
msgid "Meal-Plan"
-msgstr "Étkezési terv"
+msgstr "Menüterv"
#: .\cookbook\models.py:367 .\cookbook\templates\base.html:118
msgid "Books"
@@ -750,16 +740,12 @@ msgid "Keyword Alias"
msgstr "Kulcsszó álneve"
#: .\cookbook\models.py:1232
-#, fuzzy
-#| msgid "Description"
msgid "Description Replace"
-msgstr "Leírás"
+msgstr "Leírás csere"
#: .\cookbook\models.py:1232
-#, fuzzy
-#| msgid "Instructions"
msgid "Instruction Replace"
-msgstr "Elkészítés"
+msgstr "Leírás cseréje"
#: .\cookbook\models.py:1258 .\cookbook\views\delete.py:36
#: .\cookbook\views\edit.py:251 .\cookbook\views\new.py:48
@@ -767,10 +753,8 @@ msgid "Recipe"
msgstr "Recept"
#: .\cookbook\models.py:1259
-#, fuzzy
-#| msgid "Foods"
msgid "Food"
-msgstr "Ételek"
+msgstr "Étel"
#: .\cookbook\models.py:1260 .\cookbook\templates\base.html:141
msgid "Keyword"
@@ -1176,7 +1160,7 @@ msgstr "Ételek"
#: .\cookbook\templates\base.html:165 .\cookbook\views\lists.py:122
msgid "Units"
-msgstr "Egységek"
+msgstr "Mértékegységek"
#: .\cookbook\templates\base.html:179 .\cookbook\templates\supermarket.html:7
msgid "Supermarket"
@@ -1206,10 +1190,8 @@ msgstr "Előzmények"
#: .\cookbook\templates\base.html:255
#: .\cookbook\templates\ingredient_editor.html:7
#: .\cookbook\templates\ingredient_editor.html:13
-#, fuzzy
-#| msgid "Ingredients"
msgid "Ingredient Editor"
-msgstr "Hozzávalók"
+msgstr "Hozzávaló szerkesztő"
#: .\cookbook\templates\base.html:267
#: .\cookbook\templates\export_response.html:7
@@ -1252,7 +1234,7 @@ msgstr "Nincs hely"
#: .\cookbook\templates\base.html:323
#: .\cookbook\templates\space_overview.html:6
msgid "Overview"
-msgstr ""
+msgstr "Áttekintés"
#: .\cookbook\templates\base.html:327
msgid "Markdown Guide"
@@ -1276,11 +1258,11 @@ msgstr "Kijelentkezés"
#: .\cookbook\templates\base.html:360
msgid "You are using the free version of Tandor"
-msgstr ""
+msgstr "Ön a Tandoor ingyenes verzióját használja"
#: .\cookbook\templates\base.html:361
msgid "Upgrade Now"
-msgstr ""
+msgstr "Frissítés most"
#: .\cookbook\templates\batch\edit.html:6
msgid "Batch edit Category"
@@ -1377,7 +1359,7 @@ msgstr "Biztos, hogy törölni akarod a %(title)s: %(object)s "
#: .\cookbook\templates\generic\delete_template.html:22
msgid "This cannot be undone!"
-msgstr ""
+msgstr "Ezt nem lehet visszafordítani!"
#: .\cookbook\templates\generic\delete_template.html:27
msgid "Protected"
@@ -1541,8 +1523,6 @@ msgstr "A sortörés a sor vége után két szóköz hozzáadásával történik
#: .\cookbook\templates\markdown_info.html:57
#: .\cookbook\templates\markdown_info.html:73
-#, fuzzy
-#| msgid "or by leaving a blank line inbetween."
msgid "or by leaving a blank line in between."
msgstr "vagy egy üres sort hagyva közöttük."
@@ -1566,10 +1546,6 @@ msgid "Lists"
msgstr "Listák"
#: .\cookbook\templates\markdown_info.html:85
-#, fuzzy
-#| msgid ""
-#| "Lists can ordered or unorderd. It is important to leave a blank line "
-#| "before the list!"
msgid ""
"Lists can ordered or unordered. It is important to leave a blank line "
"before the list!"
@@ -1701,11 +1677,11 @@ msgstr ""
#: .\cookbook\templates\openid\login.html:27
#: .\cookbook\templates\socialaccount\authentication_error.html:27
msgid "Back"
-msgstr ""
+msgstr "Vissza"
#: .\cookbook\templates\profile.html:7
msgid "Profile"
-msgstr ""
+msgstr "Profil"
#: .\cookbook\templates\recipe_view.html:41
msgid "by"
@@ -1718,7 +1694,7 @@ msgstr "Megjegyzés"
#: .\cookbook\templates\rest_framework\api.html:5
msgid "Recipe Home"
-msgstr "Recipe Home"
+msgstr "Recept főoldal"
#: .\cookbook\templates\search_info.html:5
#: .\cookbook\templates\search_info.html:9
@@ -2104,17 +2080,15 @@ msgstr "Szuperfelhasználói fiók létrehozása"
#: .\cookbook\templates\socialaccount\authentication_error.html:7
#: .\cookbook\templates\socialaccount\authentication_error.html:23
-#, fuzzy
-#| msgid "Social Login"
msgid "Social Network Login Failure"
-msgstr "Közösségi bejelentkezés"
+msgstr "Közösségi hálózat bejelentkezési hiba"
#: .\cookbook\templates\socialaccount\authentication_error.html:25
-#, fuzzy
-#| msgid "An error occurred attempting to move "
msgid ""
"An error occurred while attempting to login via your social network account."
-msgstr "Hiba történt az áthelyezés közben "
+msgstr ""
+"Hiba történt, miközben megpróbált bejelentkezni a közösségi hálózati fiókján "
+"keresztül."
#: .\cookbook\templates\socialaccount\connections.html:4
#: .\cookbook\templates\socialaccount\connections.html:15
@@ -2152,7 +2126,7 @@ msgstr "Regisztráció"
#: .\cookbook\templates\socialaccount\login.html:9
#, python-format
msgid "Connect %(provider)s"
-msgstr ""
+msgstr "Csatlakozás %(provider)s"
#: .\cookbook\templates\socialaccount\login.html:11
#, python-format
@@ -2162,7 +2136,7 @@ msgstr ""
#: .\cookbook\templates\socialaccount\login.html:13
#, python-format
msgid "Sign In Via %(provider)s"
-msgstr ""
+msgstr "Bejelentkezve %(provider)s keresztül"
#: .\cookbook\templates\socialaccount\login.html:15
#, python-format
@@ -2171,7 +2145,7 @@ msgstr ""
#: .\cookbook\templates\socialaccount\login.html:20
msgid "Continue"
-msgstr ""
+msgstr "Folytatás"
#: .\cookbook\templates\socialaccount\signup.html:10
#, python-format
@@ -2210,10 +2184,8 @@ msgid "Manage Subscription"
msgstr "Feliratkozás kezelése"
#: .\cookbook\templates\space_overview.html:13 .\cookbook\views\delete.py:216
-#, fuzzy
-#| msgid "Space:"
msgid "Space"
-msgstr "Tér:"
+msgstr "Tér"
#: .\cookbook\templates\space_overview.html:17
msgid ""
@@ -2230,13 +2202,11 @@ msgstr "Meghívást kaphatsz egy meglévő térbe, vagy létrehozhatod a sajáto
#: .\cookbook\templates\space_overview.html:53
msgid "Owner"
-msgstr ""
+msgstr "Tulajdonos"
#: .\cookbook\templates\space_overview.html:57
-#, fuzzy
-#| msgid "Create Space"
msgid "Leave Space"
-msgstr "Tér létrehozása"
+msgstr "Kilépés a Térből"
#: .\cookbook\templates\space_overview.html:78
#: .\cookbook\templates\space_overview.html:88
@@ -2485,87 +2455,111 @@ msgstr ""
"teljes szöveges keresés is."
#: .\cookbook\views\api.py:733
-#, fuzzy
-#| msgid "ID of keyword a recipe should have. For multiple repeat parameter."
msgid ""
"ID of keyword a recipe should have. For multiple repeat parameter. "
"Equivalent to keywords_or"
msgstr ""
-"A recept kulcsszavának azonosítója. Többszörös ismétlődő paraméter esetén."
+"A recept kulcsszavának azonosítója. Többszörös ismétlődő paraméter esetén. "
+"Egyenértékű a keywords_or kulcsszavakkal"
#: .\cookbook\views\api.py:736
msgid ""
"Keyword IDs, repeat for multiple. Return recipes with any of the keywords"
msgstr ""
+"Kulcsszó azonosítók. Többször is megadható. A megadott kulcsszavak "
+"mindegyikéhez tartozó receptek listázza"
#: .\cookbook\views\api.py:739
msgid ""
"Keyword IDs, repeat for multiple. Return recipes with all of the keywords."
msgstr ""
+"Kulcsszó azonosítók. Többször is megadható. Az összes megadott kulcsszót "
+"tartalmazó receptek listázása."
#: .\cookbook\views\api.py:742
msgid ""
"Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords."
msgstr ""
+"Kulcsszó azonosító. Többször is megadható. Kizárja a recepteket a megadott "
+"kulcsszavak egyikéből."
#: .\cookbook\views\api.py:745
msgid ""
"Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords."
msgstr ""
+"Kulcsszó azonosítók. Többször is megadható. Kizárja az összes megadott "
+"kulcsszóval rendelkező receptet."
#: .\cookbook\views\api.py:747
msgid "ID of food a recipe should have. For multiple repeat parameter."
msgstr ""
-"Az ételek azonosítója egy receptnek tartalmaznia kell. Többszörös ismétlődő "
-"paraméter esetén."
+"Annak az összetevőnek az azonosítója, amelynek receptjeit fel kell sorolni. "
+"Többször is megadható."
#: .\cookbook\views\api.py:750
msgid "Food IDs, repeat for multiple. Return recipes with any of the foods"
msgstr ""
+"Összetevő azonosító. Többször is megadható. Legalább egy összetevő "
+"receptjeinek listája"
#: .\cookbook\views\api.py:752
msgid "Food IDs, repeat for multiple. Return recipes with all of the foods."
msgstr ""
+"Összetevő azonosító. Többször is megadható. Az összes megadott összetevőt "
+"tartalmazó receptek listája."
#: .\cookbook\views\api.py:754
msgid "Food IDs, repeat for multiple. Exclude recipes with any of the foods."
msgstr ""
+"Összetevő azonosító. Többször is megadható. Kizárja azokat a recepteket, "
+"amelyek a megadott összetevők bármelyikét tartalmazzák."
#: .\cookbook\views\api.py:756
msgid "Food IDs, repeat for multiple. Exclude recipes with all of the foods."
msgstr ""
+"Összetevő azonosító. Többször is megadható. Kizárja az összes megadott "
+"összetevőt tartalmazó recepteket."
#: .\cookbook\views\api.py:757
msgid "ID of unit a recipe should have."
-msgstr "Az egység azonosítója, amellyel a receptnek rendelkeznie kell."
+msgstr "A recepthez tartozó mértékegység azonosítója."
#: .\cookbook\views\api.py:759
msgid ""
"Rating a recipe should have or greater. [0 - 5] Negative value filters "
"rating less than."
msgstr ""
+"Egy recept minimális értékelése (0-5). A negatív értékek a maximális "
+"értékelés szerint szűrnek."
#: .\cookbook\views\api.py:760
msgid "ID of book a recipe should be in. For multiple repeat parameter."
msgstr ""
-"A könyv azonosítója, amelyben a receptnek szerepelnie kell. Többszörös "
-"ismétlés esetén paraméter."
+"A könyv azonosítója, amelyben a recept található. Többször is megadható."
#: .\cookbook\views\api.py:762
msgid "Book IDs, repeat for multiple. Return recipes with any of the books"
msgstr ""
+"A könyv azonosítója. Többször is megadható. A megadott könyvek összes "
+"receptjének listája"
#: .\cookbook\views\api.py:764
msgid "Book IDs, repeat for multiple. Return recipes with all of the books."
msgstr ""
+"A könyv azonosítója. Többször is megadható. Az összes könyvben szereplő "
+"recept listája."
#: .\cookbook\views\api.py:766
msgid "Book IDs, repeat for multiple. Exclude recipes with any of the books."
msgstr ""
+"A könyv azonosítói. Többször is megadható. Kizárja a megadott könyvek "
+"receptjeit."
#: .\cookbook\views\api.py:768
msgid "Book IDs, repeat for multiple. Exclude recipes with all of the books."
msgstr ""
+"A könyv azonosítói. Többször is megadható. Kizárja az összes megadott "
+"könyvben szereplő receptet."
#: .\cookbook\views\api.py:770
msgid "If only internal recipes should be returned. [true/false]"
@@ -2587,36 +2581,50 @@ msgid ""
"Filter recipes cooked X times or more. Negative values returns cooked less "
"than X times"
msgstr ""
+"X-szer vagy többször főzött receptek szűrése. A negatív értékek X "
+"alkalomnál kevesebbet főzött recepteket jelenítik meg"
#: .\cookbook\views\api.py:778
msgid ""
"Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on "
"or before date."
msgstr ""
+"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
+"vagy később főztek meg utoljára. A - jelölve az adott dátumon vagy azt "
+"megelőzően elkészítettek kerülnek be a receptek listájába."
#: .\cookbook\views\api.py:780
msgid ""
"Filter recipes created on or after YYYY-MM-DD. Prepending - filters on or "
"before date."
msgstr ""
+"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
+"vagy később hoztak létre. A - jelölve az adott dátumon vagy azt megelőzően "
+"hozták létre."
#: .\cookbook\views\api.py:782
msgid ""
"Filter recipes updated on or after YYYY-MM-DD. Prepending - filters on or "
"before date."
msgstr ""
+"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
+"vagy később frissültek. A - jelölve az adott dátumon vagy azt megelőzően "
+"frissültek."
#: .\cookbook\views\api.py:784
msgid ""
"Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending - filters on "
"or before date."
msgstr ""
+"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
+"vagy később néztek meg utoljára. A - jelölve az adott dátumon vagy azt "
+"megelőzően néztek meg utoljára."
#: .\cookbook\views\api.py:786
-#, fuzzy
-#| msgid "If only internal recipes should be returned. [true/false]"
msgid "Filter recipes that can be made with OnHand food. [true/false]"
-msgstr "Ha csak a belső recepteket kell visszaadni. [true/false]"
+msgstr ""
+"Felsorolja azokat a recepteket, amelyeket a rendelkezésre álló összetevőkből "
+"el lehet készíteni. [true/false]"
#: .\cookbook\views\api.py:946
msgid ""
@@ -2647,7 +2655,7 @@ msgstr "Semmi feladat."
#: .\cookbook\views\api.py:1198
msgid "Invalid Url"
-msgstr ""
+msgstr "Érvénytelen URL"
#: .\cookbook\views\api.py:1205
msgid "Connection Refused."
@@ -2655,13 +2663,11 @@ msgstr "Kapcsolat megtagadva."
#: .\cookbook\views\api.py:1210
msgid "Bad URL Schema."
-msgstr ""
+msgstr "Rossz URL séma."
#: .\cookbook\views\api.py:1233
-#, fuzzy
-#| msgid "No useable data could be found."
msgid "No usable data could be found."
-msgstr "Nem találtam használható adatokat."
+msgstr "Nem sikerült használható adatokat találni."
#: .\cookbook\views\api.py:1326 .\cookbook\views\import_export.py:117
msgid "Importing is not implemented for this provider"
@@ -2774,10 +2780,8 @@ msgid "Shopping Categories"
msgstr "Bevásárlási kategóriák"
#: .\cookbook\views\lists.py:187
-#, fuzzy
-#| msgid "Filter"
msgid "Custom Filters"
-msgstr "Szűrő"
+msgstr "Egyedi szűrők"
#: .\cookbook\views\lists.py:224
msgid "Steps"
diff --git a/cookbook/locale/pt/LC_MESSAGES/django.po b/cookbook/locale/pt/LC_MESSAGES/django.po
index 192d465c1e..1bb6cbe4ef 100644
--- a/cookbook/locale/pt/LC_MESSAGES/django.po
+++ b/cookbook/locale/pt/LC_MESSAGES/django.po
@@ -12,8 +12,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
-"PO-Revision-Date: 2023-01-08 17:55+0000\n"
-"Last-Translator: Joachim Weber \n"
+"PO-Revision-Date: 2023-10-07 18:02+0000\n"
+"Last-Translator: Guilherme Roda \n"
"Language-Team: Portuguese \n"
"Language: pt\n"
@@ -206,8 +206,8 @@ msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (/remote."
"php/webdav/
is added automatically)"
msgstr ""
-"Deixar vazio para Dropbox e inserir apenas url base para Nextcloud (/"
-"remote.php/webdav/
é adicionado automaticamente). "
+"Deixar vazio para Dropbox e inserir apenas url base para Nextcloud "
+"(/remote.php/webdav/
é adicionado automaticamente)"
#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
@@ -277,16 +277,12 @@ msgstr ""
"ignorados)."
#: .\cookbook\forms.py:461
-#, fuzzy
-#| msgid ""
-#| "Select type method of search. Click here "
-#| "for full desciption of choices."
msgid ""
"Select type method of search. Click here for "
"full description of choices."
msgstr ""
-"Selecionar o método de pesquisa. Uma descrição completa das opções pode ser "
-"encontrada aqui."
+"Selecionar o método de pesquisa. Uma descrição completa das opções pode "
+"ser encontrada aqui."
#: .\cookbook\forms.py:462
msgid ""
@@ -329,10 +325,8 @@ msgid ""
msgstr ""
#: .\cookbook\forms.py:476
-#, fuzzy
-#| msgid "Search"
msgid "Search Method"
-msgstr "Procurar"
+msgstr "Método de Pesquisa"
#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
@@ -351,16 +345,12 @@ msgid "Starts With"
msgstr ""
#: .\cookbook\forms.py:481
-#, fuzzy
-#| msgid "Search"
msgid "Fuzzy Search"
-msgstr "Procurar"
+msgstr "Pesquisa Fuzzy"
#: .\cookbook\forms.py:482
-#, fuzzy
-#| msgid "Text"
msgid "Full Text"
-msgstr "Texto"
+msgstr "Texto Completo"
#: .\cookbook\forms.py:507
msgid ""
@@ -405,10 +395,8 @@ msgid "Prefix to add when copying list to the clipboard."
msgstr ""
#: .\cookbook\forms.py:524
-#, fuzzy
-#| msgid "Shopping"
msgid "Share Shopping List"
-msgstr "Compras"
+msgstr "Compartilhar Lista de Compras"
#: .\cookbook\forms.py:525
msgid "Autosync"
@@ -459,10 +447,8 @@ msgid "Reset all food to inherit the fields configured."
msgstr ""
#: .\cookbook\forms.py:557
-#, fuzzy
-#| msgid "Food that should be replaced."
msgid "Fields on food that should be inherited by default."
-msgstr "Prato a ser alterado."
+msgstr "Campos do alimento que devem ser herdados por padrão."
#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
@@ -516,10 +502,8 @@ msgid "One of queryset or hash_key must be provided"
msgstr ""
#: .\cookbook\helper\recipe_url_import.py:266
-#, fuzzy
-#| msgid "Use fractions"
msgid "reverse rotation"
-msgstr "Usar frações"
+msgstr "rotação reversa"
#: .\cookbook\helper\recipe_url_import.py:267
msgid "careful rotation"
@@ -585,16 +569,12 @@ msgid "Imported %s recipes."
msgstr "%s receitas importadas."
#: .\cookbook\integration\openeats.py:26
-#, fuzzy
-#| msgid "Recipes"
msgid "Recipe source:"
-msgstr "Receitas"
+msgstr "Fonte da Receita:"
#: .\cookbook\integration\paprika.py:49
-#, fuzzy
-#| msgid "Note"
msgid "Notes"
-msgstr "Nota"
+msgstr "Notas"
#: .\cookbook\integration\paprika.py:52
msgid "Nutritional Information"
@@ -606,10 +586,8 @@ msgstr ""
#: .\cookbook\integration\recettetek.py:54
#: .\cookbook\integration\recipekeeper.py:70
-#, fuzzy
-#| msgid "Import"
msgid "Imported from"
-msgstr "Importar"
+msgstr "Importado de"
#: .\cookbook\integration\saffron.py:23
msgid "Servings"
@@ -706,32 +684,24 @@ msgid "Raw"
msgstr ""
#: .\cookbook\models.py:1231
-#, fuzzy
-#| msgid "New Food"
msgid "Food Alias"
-msgstr "Novo Prato"
+msgstr "Apelido do Alimento"
#: .\cookbook\models.py:1231
-#, fuzzy
-#| msgid "Units"
msgid "Unit Alias"
-msgstr "Unidades"
+msgstr "Apelido da Unidade"
#: .\cookbook\models.py:1231
-#, fuzzy
-#| msgid "Keywords"
msgid "Keyword Alias"
-msgstr "Palavras-chave"
+msgstr "Apelido de Palavra-chave"
#: .\cookbook\models.py:1232
msgid "Description Replace"
msgstr ""
#: .\cookbook\models.py:1232
-#, fuzzy
-#| msgid "Instructions"
msgid "Instruction Replace"
-msgstr "Instruções"
+msgstr "Substituir Instruções"
#: .\cookbook\models.py:1258 .\cookbook\views\delete.py:36
#: .\cookbook\views\edit.py:251 .\cookbook\views\new.py:48
@@ -739,10 +709,8 @@ msgid "Recipe"
msgstr "Receita"
#: .\cookbook\models.py:1259
-#, fuzzy
-#| msgid "New Food"
msgid "Food"
-msgstr "Novo Prato"
+msgstr "Alimento"
#: .\cookbook\models.py:1260 .\cookbook\templates\base.html:141
msgid "Keyword"
@@ -880,10 +848,8 @@ msgid "Primary"
msgstr ""
#: .\cookbook\templates\account\email.html:47
-#, fuzzy
-#| msgid "Make Header"
msgid "Make Primary"
-msgstr "Adicionar Cabeçalho"
+msgstr "Tornar Primeiro"
#: .\cookbook\templates\account\email.html:49
msgid "Re-send Verification"
@@ -1004,10 +970,8 @@ msgstr ""
#: .\cookbook\templates\account\password_change.html:12
#: .\cookbook\templates\account\password_set.html:12
-#, fuzzy
-#| msgid "Settings"
msgid "Password"
-msgstr "Definições"
+msgstr "Senha"
#: .\cookbook\templates\account\password_change.html:22
msgid "Forgot Password?"
@@ -1050,10 +1014,8 @@ msgid ""
msgstr ""
#: .\cookbook\templates\account\password_reset_from_key.html:33
-#, fuzzy
-#| msgid "Settings"
msgid "change password"
-msgstr "Definições"
+msgstr "alterar senha"
#: .\cookbook\templates\account\password_reset_from_key.html:36
#: .\cookbook\templates\account\password_reset_from_key_done.html:19
@@ -1125,10 +1087,8 @@ msgid "Shopping"
msgstr "Compras"
#: .\cookbook\templates\base.html:153 .\cookbook\views\lists.py:105
-#, fuzzy
-#| msgid "New Food"
msgid "Foods"
-msgstr "Novo Prato"
+msgstr "Alimentos"
#: .\cookbook\templates\base.html:165 .\cookbook\views\lists.py:122
msgid "Units"
@@ -1139,20 +1099,16 @@ msgid "Supermarket"
msgstr ""
#: .\cookbook\templates\base.html:191
-#, fuzzy
-#| msgid "Batch edit Category"
msgid "Supermarket Category"
-msgstr "Editar Categorias em massa"
+msgstr "Categoria de Supermercado"
#: .\cookbook\templates\base.html:203 .\cookbook\views\lists.py:171
msgid "Automations"
msgstr ""
#: .\cookbook\templates\base.html:217 .\cookbook\views\lists.py:207
-#, fuzzy
-#| msgid "File ID"
msgid "Files"
-msgstr "ID the ficheiro"
+msgstr "Arquivos"
#: .\cookbook\templates\base.html:229
msgid "Batch Edit"
@@ -1166,10 +1122,8 @@ msgstr "Histórico"
#: .\cookbook\templates\base.html:255
#: .\cookbook\templates\ingredient_editor.html:7
#: .\cookbook\templates\ingredient_editor.html:13
-#, fuzzy
-#| msgid "Ingredients"
msgid "Ingredient Editor"
-msgstr "Ingredientes"
+msgstr "Editor de Ingrediente"
#: .\cookbook\templates\base.html:267
#: .\cookbook\templates\export_response.html:7
@@ -1191,10 +1145,8 @@ msgid "External Recipes"
msgstr ""
#: .\cookbook\templates\base.html:301 .\cookbook\templates\space_manage.html:15
-#, fuzzy
-#| msgid "Settings"
msgid "Space Settings"
-msgstr "Definições"
+msgstr "Configurar Espaço"
#: .\cookbook\templates\base.html:306 .\cookbook\templates\system.html:13
msgid "System"
@@ -1206,10 +1158,8 @@ msgstr "Administração"
#: .\cookbook\templates\base.html:312
#: .\cookbook\templates\space_overview.html:25
-#, fuzzy
-#| msgid "Create"
msgid "Your Spaces"
-msgstr "Criar"
+msgstr "Seus Espaços"
#: .\cookbook\templates\base.html:323
#: .\cookbook\templates\space_overview.html:6
@@ -1288,19 +1238,15 @@ msgstr ""
#: .\cookbook\templates\batch\monitor.html:28
msgid "Sync Now!"
-msgstr "Sincronizar"
+msgstr "Sincronizar Agora!"
#: .\cookbook\templates\batch\monitor.html:29
-#, fuzzy
-#| msgid "Recipes"
msgid "Show Recipes"
-msgstr "Receitas"
+msgstr "Mostrar Receitas"
#: .\cookbook\templates\batch\monitor.html:30
-#, fuzzy
-#| msgid "View Log"
msgid "Show Log"
-msgstr "Ver Registro"
+msgstr "Mostrar Log"
#: .\cookbook\templates\batch\waiting.html:4
#: .\cookbook\templates\batch\waiting.html:10
@@ -1335,7 +1281,7 @@ msgstr "Editar Receita"
#: .\cookbook\templates\generic\delete_template.html:21
#, python-format
msgid "Are you sure you want to delete the %(title)s: %(object)s "
-msgstr "Tem a certeza que quer apagar %(title)s: %(object)s"
+msgstr "Tem certeza que deseja apagar %(title)s: %(object)s "
#: .\cookbook\templates\generic\delete_template.html:22
msgid "This cannot be undone!"
@@ -1369,7 +1315,7 @@ msgstr "Apagar ficheiro original"
#: .\cookbook\templates\generic\list_template.html:6
#: .\cookbook\templates\generic\list_template.html:22
msgid "List"
-msgstr "Listar "
+msgstr "Listar"
#: .\cookbook\templates\generic\list_template.html:36
msgid "Filter"
@@ -1422,13 +1368,13 @@ msgid ""
" "
msgstr ""
"\n"
-" Os campos da senha e Token são guardados dentro da base de "
-"dados como texto simples.\n"
-"Isto é necessário porque eles são usados para fazer pedidos á API, mas "
-"também aumenta o risco de\n"
-"de alguém os roubar.
\n"
-"Para limitar os possíveis danos, tokens e contas com acesso limitado podem "
-"ser usadas.\n"
+" Os campos de senha e Token são armazenados na base de dados "
+"como texto simples.\n"
+" Isto é necessário porque eles são usados para fazer pedidos à API, "
+"mas também aumenta o risco\n"
+" de alguém os roubar.
\n"
+" Para limitar os possíveis danos, tokens e contas com acesso limitado "
+"podem ser usadas.\n"
" "
#: .\cookbook\templates\index.html:29
@@ -1441,7 +1387,7 @@ msgstr "Nova Receita"
#: .\cookbook\templates\index.html:53
msgid "Advanced Search"
-msgstr "Procura avançada "
+msgstr "Pesquisa avançada"
#: .\cookbook\templates\index.html:57
msgid "Reset Search"
@@ -1493,8 +1439,6 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:57
#: .\cookbook\templates\markdown_info.html:73
-#, fuzzy
-#| msgid "or by leaving a blank line inbetween."
msgid "or by leaving a blank line in between."
msgstr "ou deixando uma linha em branco no meio."
@@ -1518,10 +1462,6 @@ msgid "Lists"
msgstr "Listas"
#: .\cookbook\templates\markdown_info.html:85
-#, fuzzy
-#| msgid ""
-#| "Lists can ordered or unorderd. It is important to leave a blank line "
-#| "before the list!"
msgid ""
"Lists can ordered or unordered. It is important to leave a blank line "
"before the list!"
@@ -1598,7 +1538,7 @@ msgstr "Cabeçalho"
#: .\cookbook\templates\markdown_info.html:157
#: .\cookbook\templates\markdown_info.html:178
msgid "Cell"
-msgstr "Célula "
+msgstr "Célula"
#: .\cookbook\templates\no_groups_info.html:5
#: .\cookbook\templates\no_groups_info.html:12
@@ -1666,10 +1606,8 @@ msgstr ""
#: .\cookbook\templates\search_info.html:5
#: .\cookbook\templates\search_info.html:9
#: .\cookbook\templates\settings.html:24
-#, fuzzy
-#| msgid "Search String"
msgid "Search Settings"
-msgstr "Procurar"
+msgstr "Configurações de Pesquisa"
#: .\cookbook\templates\search_info.html:10
msgid ""
@@ -1684,10 +1622,8 @@ msgid ""
msgstr ""
#: .\cookbook\templates\search_info.html:19
-#, fuzzy
-#| msgid "Search"
msgid "Search Methods"
-msgstr "Procurar"
+msgstr "Métodos de Pesquisa"
#: .\cookbook\templates\search_info.html:23
msgid ""
@@ -1769,10 +1705,8 @@ msgid ""
msgstr ""
#: .\cookbook\templates\search_info.html:69
-#, fuzzy
-#| msgid "Search Recipe"
msgid "Search Fields"
-msgstr "Procure Receita"
+msgstr "Campos de Pesquisa"
#: .\cookbook\templates\search_info.html:73
msgid ""
@@ -1810,10 +1744,8 @@ msgid ""
msgstr ""
#: .\cookbook\templates\search_info.html:95
-#, fuzzy
-#| msgid "Search"
msgid "Search Index"
-msgstr "Procurar"
+msgstr "Índice de Pesquisa"
#: .\cookbook\templates\search_info.html:99
msgid ""
@@ -2012,10 +1944,8 @@ msgid "Owner"
msgstr ""
#: .\cookbook\templates\space_overview.html:57
-#, fuzzy
-#| msgid "Create"
msgid "Leave Space"
-msgstr "Criar"
+msgstr "Sair do Espaço"
#: .\cookbook\templates\space_overview.html:78
#: .\cookbook\templates\space_overview.html:88
@@ -2034,10 +1964,8 @@ msgstr ""
#: .\cookbook\templates\space_overview.html:96
#: .\cookbook\templates\space_overview.html:105
-#, fuzzy
-#| msgid "Create"
msgid "Create Space"
-msgstr "Criar"
+msgstr "Criar Espaço"
#: .\cookbook\templates\space_overview.html:99
msgid "Create your own recipe space."
@@ -2487,10 +2415,8 @@ msgid "Shopping Categories"
msgstr ""
#: .\cookbook\views\lists.py:187
-#, fuzzy
-#| msgid "Filter"
msgid "Custom Filters"
-msgstr "Filtrar"
+msgstr "Filtros Customizados"
#: .\cookbook\views\lists.py:224
msgid "Steps"
diff --git a/cookbook/locale/pt_BR/LC_MESSAGES/django.po b/cookbook/locale/pt_BR/LC_MESSAGES/django.po
index c3eff99e4f..f92cc563b5 100644
--- a/cookbook/locale/pt_BR/LC_MESSAGES/django.po
+++ b/cookbook/locale/pt_BR/LC_MESSAGES/django.po
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-02-11 08:52+0100\n"
-"PO-Revision-Date: 2023-04-12 11:55+0000\n"
-"Last-Translator: noxonad \n"
+"PO-Revision-Date: 2023-10-09 01:54+0000\n"
+"Last-Translator: Guilherme Roda \n"
"Language-Team: Portuguese (Brazil) \n"
"Language: pt_BR\n"
@@ -35,7 +35,7 @@ msgstr "Usar frações"
#: .\cookbook\forms.py:58
msgid "Use KJ"
-msgstr ""
+msgstr "Usar KJ"
#: .\cookbook\forms.py:59
msgid "Theme"
@@ -105,10 +105,12 @@ msgstr "Exibir informações nutricionais em Joules ao invés de Calorias"
#: .\cookbook\forms.py:79
msgid "Users with whom newly created meal plans should be shared by default."
msgstr ""
+"Usuários com os quais novos planos de refeição devem ser compartilhados por "
+"padrão."
#: .\cookbook\forms.py:80
msgid "Users with whom to share shopping lists."
-msgstr ""
+msgstr "Usuários com os quais novas listas de compras serão compartilhadas."
#: .\cookbook\forms.py:82
msgid "Show recently viewed recipes on search page."
@@ -116,7 +118,7 @@ msgstr ""
#: .\cookbook\forms.py:83
msgid "Number of decimals to round ingredients."
-msgstr ""
+msgstr "Número de casas decimais para arredondamento dos ingredientes."
#: .\cookbook\forms.py:84
msgid "If you want to be able to create and see comments underneath recipes."
@@ -129,10 +131,15 @@ msgid ""
"Useful when shopping with multiple people but might use a little bit of "
"mobile data. If lower than instance limit it is reset when saving."
msgstr ""
+"Definir esta opção como 0 desativará a sincronização automática. Ao "
+"visualizar uma lista de compras, a lista é atualizada a cada período aqui "
+"definido para sincronizar as alterações que outro usuário possa ter feito. "
+"Útil ao fazer compras com vários usuários, mas pode aumentar o uso de dados "
+"móveis."
#: .\cookbook\forms.py:89
msgid "Makes the navbar stick to the top of the page."
-msgstr ""
+msgstr "Mantém a barra de navegação no topo da página."
#: .\cookbook\forms.py:90 .\cookbook\forms.py:496
msgid "Automatically add meal plan ingredients to shopping list."
@@ -147,11 +154,13 @@ msgid ""
"Both fields are optional. If none are given the username will be displayed "
"instead"
msgstr ""
+"Ambos os campos são opcionais. Se nenhum for preenchido o nome do usuário "
+"será mostrado"
#: .\cookbook\forms.py:129 .\cookbook\forms.py:298
#: .\cookbook\templates\url_import.html:161
msgid "Name"
-msgstr ""
+msgstr "Nome"
#: .\cookbook\forms.py:130 .\cookbook\forms.py:299
#: .\cookbook\templates\space.html:44 .\cookbook\templates\stats.html:24
@@ -162,99 +171,109 @@ msgstr "Palavras-chave"
#: .\cookbook\forms.py:131
msgid "Preparation time in minutes"
-msgstr ""
+msgstr "Tempo de preparação em minutos"
#: .\cookbook\forms.py:132
msgid "Waiting time (cooking/baking) in minutes"
-msgstr ""
+msgstr "Tempo de espera (cozimento) em minutos"
#: .\cookbook\forms.py:133 .\cookbook\forms.py:267 .\cookbook\forms.py:300
msgid "Path"
-msgstr ""
+msgstr "Caminho"
#: .\cookbook\forms.py:134
msgid "Storage UID"
-msgstr ""
+msgstr "UID de armazenamento"
#: .\cookbook\forms.py:164
msgid "Default"
-msgstr ""
+msgstr "Padrão"
#: .\cookbook\forms.py:175 .\cookbook\templates\url_import.html:97
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
msgstr ""
+"Para evitar repetições, receitas com o mesmo nome de receitas já existentes "
+"são ignoradas. Marque esta caixa para importar tudo."
#: .\cookbook\forms.py:197
msgid "Add your comment: "
-msgstr ""
+msgstr "Incluir seu comentário: "
#: .\cookbook\forms.py:212
msgid "Leave empty for dropbox and enter app password for nextcloud."
-msgstr ""
+msgstr "Deixar vazio para Dropbox e inserir senha de aplicação para Nextcloud."
#: .\cookbook\forms.py:219
msgid "Leave empty for nextcloud and enter api token for dropbox."
-msgstr ""
+msgstr "Deixar vazio para Nextcloud e inserir token api para Dropbox."
#: .\cookbook\forms.py:228
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (/remote."
"php/webdav/
is added automatically)"
msgstr ""
+"Deixar vazio para Dropbox e inserir apenas url base para Nextcloud "
+"(/remote.php/webdav/
é adicionado automaticamente)"
#: .\cookbook\forms.py:266 .\cookbook\views\edit.py:166
msgid "Storage"
-msgstr ""
+msgstr "Armazenamento"
#: .\cookbook\forms.py:268
msgid "Active"
-msgstr ""
+msgstr "Ativo"
#: .\cookbook\forms.py:274
msgid "Search String"
-msgstr ""
+msgstr "String de Pesquisa"
#: .\cookbook\forms.py:301
msgid "File ID"
-msgstr ""
+msgstr "ID do Arquivo"
#: .\cookbook\forms.py:323
msgid "You must provide at least a recipe or a title."
-msgstr ""
+msgstr "Você precisa informar ao menos uma receita ou um título."
#: .\cookbook\forms.py:336
msgid "You can list default users to share recipes with in the settings."
msgstr ""
+"É possível escolher os usuários com quem compartilhar receitas por padrão "
+"nas definições."
#: .\cookbook\forms.py:337
msgid ""
"You can use markdown to format this field. See the docs here"
msgstr ""
+"É possível utilizar markdown para editar este campo. Documentação disponível aqui"
#: .\cookbook\forms.py:363
msgid "Maximum number of users for this space reached."
-msgstr ""
+msgstr "Número máximo de usuários para este espaço atingido."
#: .\cookbook\forms.py:369
msgid "Email address already taken!"
-msgstr ""
+msgstr "Endereço de email já utilizado!"
#: .\cookbook\forms.py:377
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
msgstr ""
+"Um endereço de email não é obrigatório mas se fornecido será enviada uma "
+"mensagem ao usuário."
#: .\cookbook\forms.py:392
msgid "Name already taken."
-msgstr ""
+msgstr "Nome já existente."
#: .\cookbook\forms.py:403
msgid "Accept Terms and Privacy"
-msgstr ""
+msgstr "Termos de Aceite e Privacidade"
#: .\cookbook\forms.py:435
msgid ""
@@ -267,18 +286,24 @@ msgid ""
"Select type method of search. Click here for "
"full description of choices."
msgstr ""
+"Selecionar o método de pesquisa. Uma descrição completa das opções pode "
+"ser encontrada aqui."
#: .\cookbook\forms.py:446
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
msgstr ""
+"Utilizar correspondência fonética em unidades, palavras-chave e ingredientes "
+"ao editar e importar receitas."
#: .\cookbook\forms.py:448
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
msgstr ""
+"Campos de pesquisa que ignoram pontuação. Esta opção pode aumentar ou "
+"diminuir a qualidade de pesquisa dependendo do idioma em uso"
#: .\cookbook\forms.py:450
msgid ""
@@ -306,7 +331,7 @@ msgstr ""
#: .\cookbook\forms.py:460
msgid "Search Method"
-msgstr ""
+msgstr "Método de Pesquisa"
#: .\cookbook\forms.py:461
msgid "Fuzzy Lookups"
@@ -322,15 +347,15 @@ msgstr ""
#: .\cookbook\forms.py:464
msgid "Starts With"
-msgstr ""
+msgstr "Inicia Com"
#: .\cookbook\forms.py:465
msgid "Fuzzy Search"
-msgstr ""
+msgstr "Pesquisa Fuzzy"
#: .\cookbook\forms.py:466
msgid "Full Text"
-msgstr ""
+msgstr "Texto Completo"
#: .\cookbook\forms.py:491
msgid ""
@@ -376,15 +401,15 @@ msgstr ""
#: .\cookbook\forms.py:508
msgid "Share Shopping List"
-msgstr ""
+msgstr "Compartilhar Lista de Compras"
#: .\cookbook\forms.py:509
msgid "Autosync"
-msgstr ""
+msgstr "Sincronização automática"
#: .\cookbook\forms.py:510
msgid "Auto Add Meal Plan"
-msgstr ""
+msgstr "Auto Incluir Plano de Refeição"
#: .\cookbook\forms.py:511
msgid "Exclude On Hand"
@@ -400,19 +425,19 @@ msgstr ""
#: .\cookbook\forms.py:514
msgid "Filter to Supermarket"
-msgstr ""
+msgstr "Filtro para Supermercado"
#: .\cookbook\forms.py:515
msgid "Recent Days"
-msgstr ""
+msgstr "Dias Recentes"
#: .\cookbook\forms.py:516
msgid "CSV Delimiter"
-msgstr ""
+msgstr "Delimitador CSV"
#: .\cookbook\forms.py:517 .\cookbook\templates\shopping_list.html:322
msgid "List Prefix"
-msgstr ""
+msgstr "Lista de Prefixos"
#: .\cookbook\forms.py:518
msgid "Auto On Hand"
@@ -428,11 +453,11 @@ msgstr ""
#: .\cookbook\forms.py:541
msgid "Fields on food that should be inherited by default."
-msgstr ""
+msgstr "Campos do alimento que devem ser herdados por padrão."
#: .\cookbook\forms.py:542
msgid "Show recipe counts on search filters"
-msgstr ""
+msgstr "Mostrar contador de receitas nos filtros de pesquisa"
#: .\cookbook\helper\AllAuthCustomAdapter.py:36
msgid ""
@@ -443,7 +468,7 @@ msgstr ""
#: .\cookbook\helper\permission_helper.py:136
#: .\cookbook\helper\permission_helper.py:159 .\cookbook\views\views.py:148
msgid "You are not logged in and therefore cannot view this page!"
-msgstr ""
+msgstr "Autenticação necessária para acessar esta página!"
#: .\cookbook\helper\permission_helper.py:140
#: .\cookbook\helper\permission_helper.py:146
@@ -455,7 +480,7 @@ msgstr ""
#: .\cookbook\views\views.py:159 .\cookbook\views\views.py:166
#: .\cookbook\views\views.py:232
msgid "You do not have the required permissions to view this page!"
-msgstr ""
+msgstr "Sem permissões para acessar esta página!"
#: .\cookbook\helper\permission_helper.py:164
#: .\cookbook\helper\permission_helper.py:187
@@ -469,7 +494,7 @@ msgstr ""
#: .\cookbook\helper\shopping_helper.py:148
msgid "You must supply a servings size"
-msgstr ""
+msgstr "Você precisa informar um tamanho de porção"
#: .\cookbook\helper\template_helper.py:61
#: .\cookbook\helper\template_helper.py:63
@@ -495,19 +520,19 @@ msgstr ""
#: .\cookbook\integration\integration.py:225
#, python-format
msgid "Imported %s recipes."
-msgstr ""
+msgstr "%s receitas importadas."
#: .\cookbook\integration\paprika.py:46
msgid "Notes"
-msgstr ""
+msgstr "Notas"
#: .\cookbook\integration\paprika.py:49
msgid "Nutritional Information"
-msgstr ""
+msgstr "Informação Nutricional"
#: .\cookbook\integration\paprika.py:53 .\cookbook\templates\url_import.html:42
msgid "Source"
-msgstr ""
+msgstr "Fonte"
#: .\cookbook\integration\saffron.py:23
#: .\cookbook\templates\url_import.html:231
@@ -517,21 +542,21 @@ msgstr "Porções"
#: .\cookbook\integration\saffron.py:25
msgid "Waiting time"
-msgstr ""
+msgstr "Tempo de espera"
#: .\cookbook\integration\saffron.py:27
msgid "Preparation Time"
-msgstr ""
+msgstr "Tempo de Preparação"
#: .\cookbook\integration\saffron.py:29 .\cookbook\templates\base.html:78
#: .\cookbook\templates\forms\ingredients.html:7
#: .\cookbook\templates\index.html:7
msgid "Cookbook"
-msgstr ""
+msgstr "Livro de Receita"
#: .\cookbook\integration\saffron.py:31
msgid "Section"
-msgstr ""
+msgstr "Seção"
#: .\cookbook\management\commands\rebuildindex.py:14
msgid "Rebuilds full text search index on Recipe"
@@ -551,19 +576,19 @@ msgstr ""
#: .\cookbook\migrations\0047_auto_20200602_1133.py:14
msgid "Breakfast"
-msgstr ""
+msgstr "Café da Manhã"
#: .\cookbook\migrations\0047_auto_20200602_1133.py:19
msgid "Lunch"
-msgstr ""
+msgstr "Almoço"
#: .\cookbook\migrations\0047_auto_20200602_1133.py:24
msgid "Dinner"
-msgstr ""
+msgstr "Jantar"
#: .\cookbook\migrations\0047_auto_20200602_1133.py:29
msgid "Other"
-msgstr ""
+msgstr "Outro"
#: .\cookbook\models.py:246
msgid ""
@@ -574,14 +599,14 @@ msgstr ""
#: .\cookbook\models.py:300 .\cookbook\templates\search.html:7
#: .\cookbook\templates\shopping_list.html:53
msgid "Search"
-msgstr ""
+msgstr "Pesquisa"
#: .\cookbook\models.py:301 .\cookbook\templates\base.html:82
#: .\cookbook\templates\meal_plan.html:7
#: .\cookbook\templates\meal_plan_new.html:7 .\cookbook\views\delete.py:181
#: .\cookbook\views\edit.py:220 .\cookbook\views\new.py:184
msgid "Meal-Plan"
-msgstr ""
+msgstr "Plano de Refeição"
#: .\cookbook\models.py:302 .\cookbook\templates\base.html:90
msgid "Books"
@@ -589,11 +614,11 @@ msgstr "Livros"
#: .\cookbook\models.py:310
msgid "Small"
-msgstr ""
+msgstr "Pequeno"
#: .\cookbook\models.py:310
msgid "Large"
-msgstr ""
+msgstr "Grande"
#: .\cookbook\models.py:310 .\cookbook\templates\generic\new_template.html:6
#: .\cookbook\templates\generic\new_template.html:14
@@ -606,35 +631,35 @@ msgstr ""
#: .\cookbook\models.py:1065 .\cookbook\templates\search_info.html:28
msgid "Simple"
-msgstr ""
+msgstr "Simples"
#: .\cookbook\models.py:1066 .\cookbook\templates\search_info.html:33
msgid "Phrase"
-msgstr ""
+msgstr "Frase"
#: .\cookbook\models.py:1067 .\cookbook\templates\search_info.html:38
msgid "Web"
-msgstr ""
+msgstr "Web"
#: .\cookbook\models.py:1068 .\cookbook\templates\search_info.html:47
msgid "Raw"
-msgstr ""
+msgstr "Raw"
#: .\cookbook\models.py:1106
msgid "Food Alias"
-msgstr ""
+msgstr "Apelido do Alimento"
#: .\cookbook\models.py:1106
msgid "Unit Alias"
-msgstr ""
+msgstr "Apelido da Unidade"
#: .\cookbook\models.py:1106
msgid "Keyword Alias"
-msgstr ""
+msgstr "Apelido da Palavra-chave"
#: .\cookbook\serializer.py:180
msgid "A user is required"
-msgstr ""
+msgstr "Um usuário é obrigatório"
#: .\cookbook\serializer.py:200
msgid "File uploads are not enabled for this Space."
@@ -685,11 +710,11 @@ msgstr "Editar"
#: .\cookbook\templates\generic\edit_template.html:28
#: .\cookbook\templates\recipes_table.html:90
msgid "Delete"
-msgstr ""
+msgstr "Apagar"
#: .\cookbook\templates\404.html:5
msgid "404 Error"
-msgstr ""
+msgstr "Erro 404"
#: .\cookbook\templates\404.html:18
msgid "The page you are looking for could not be found."
@@ -701,12 +726,12 @@ msgstr ""
#: .\cookbook\templates\404.html:35
msgid "Report a Bug"
-msgstr ""
+msgstr "Reportar um Bug"
#: .\cookbook\templates\account\email.html:6
#: .\cookbook\templates\account\email.html:17
msgid "E-mail Addresses"
-msgstr ""
+msgstr "Endereço de E-mail"
#: .\cookbook\templates\account\email.html:12
#: .\cookbook\templates\account\password_change.html:11
@@ -719,7 +744,7 @@ msgstr "Configurações"
#: .\cookbook\templates\account\email.html:13
msgid "Email"
-msgstr ""
+msgstr "Email"
#: .\cookbook\templates\account\email.html:19
msgid "The following e-mail addresses are associated with your account:"
@@ -727,33 +752,33 @@ msgstr ""
#: .\cookbook\templates\account\email.html:36
msgid "Verified"
-msgstr ""
+msgstr "Verificado"
#: .\cookbook\templates\account\email.html:38
msgid "Unverified"
-msgstr ""
+msgstr "Não verificado"
#: .\cookbook\templates\account\email.html:40
msgid "Primary"
-msgstr ""
+msgstr "Primário"
#: .\cookbook\templates\account\email.html:47
msgid "Make Primary"
-msgstr ""
+msgstr "Tornar Primário"
#: .\cookbook\templates\account\email.html:49
msgid "Re-send Verification"
-msgstr ""
+msgstr "Reenviar Verificação"
#: .\cookbook\templates\account\email.html:50
#: .\cookbook\templates\generic\delete_template.html:56
#: .\cookbook\templates\socialaccount\connections.html:44
msgid "Remove"
-msgstr ""
+msgstr "Remover"
#: .\cookbook\templates\account\email.html:58
msgid "Warning:"
-msgstr ""
+msgstr "Alerta:"
#: .\cookbook\templates\account\email.html:58
msgid ""
@@ -763,11 +788,11 @@ msgstr ""
#: .\cookbook\templates\account\email.html:64
msgid "Add E-mail Address"
-msgstr ""
+msgstr "Incluir Endereço de E-mail"
#: .\cookbook\templates\account\email.html:69
msgid "Add E-mail"
-msgstr ""
+msgstr "Incluir E-mail"
#: .\cookbook\templates\account\email.html:79
msgid "Do you really want to remove the selected e-mail address?"
@@ -776,7 +801,7 @@ msgstr ""
#: .\cookbook\templates\account\email_confirm.html:6
#: .\cookbook\templates\account\email_confirm.html:10
msgid "Confirm E-mail Address"
-msgstr ""
+msgstr "Confirmar Endereço de E-mail"
#: .\cookbook\templates\account\email_confirm.html:16
#, python-format
@@ -790,7 +815,7 @@ msgstr ""
#: .\cookbook\templates\account\email_confirm.html:22
#: .\cookbook\templates\generic\delete_template.html:71
msgid "Confirm"
-msgstr ""
+msgstr "Confirmar"
#: .\cookbook\templates\account\email_confirm.html:29
#, python-format
@@ -802,7 +827,7 @@ msgstr ""
#: .\cookbook\templates\account\login.html:8 .\cookbook\templates\base.html:289
msgid "Login"
-msgstr ""
+msgstr "Login"
#: .\cookbook\templates\account\login.html:15
#: .\cookbook\templates\account\login.html:31
@@ -821,11 +846,11 @@ msgstr ""
#: .\cookbook\templates\account\login.html:41
#: .\cookbook\templates\account\password_reset.html:29
msgid "Reset My Password"
-msgstr ""
+msgstr "Resetar Minha Senha"
#: .\cookbook\templates\account\login.html:40
msgid "Lost your password?"
-msgstr ""
+msgstr "Esqueceu sua senha?"
#: .\cookbook\templates\account\login.html:52
msgid "Social Login"
@@ -853,24 +878,24 @@ msgstr ""
#: .\cookbook\templates\account\password_reset_from_key_done.html:7
#: .\cookbook\templates\account\password_reset_from_key_done.html:13
msgid "Change Password"
-msgstr ""
+msgstr "Alterar Senha"
#: .\cookbook\templates\account\password_change.html:12
#: .\cookbook\templates\account\password_set.html:12
#: .\cookbook\templates\settings.html:76
msgid "Password"
-msgstr ""
+msgstr "Senha"
#: .\cookbook\templates\account\password_change.html:22
msgid "Forgot Password?"
-msgstr ""
+msgstr "Esqueceu a Senha?"
#: .\cookbook\templates\account\password_reset.html:7
#: .\cookbook\templates\account\password_reset.html:13
#: .\cookbook\templates\account\password_reset_done.html:7
#: .\cookbook\templates\account\password_reset_done.html:10
msgid "Password Reset"
-msgstr ""
+msgstr "Resetar Senha"
#: .\cookbook\templates\account\password_reset.html:24
msgid ""
@@ -903,36 +928,36 @@ msgstr ""
#: .\cookbook\templates\account\password_reset_from_key.html:33
msgid "change password"
-msgstr ""
+msgstr "alterar senha"
#: .\cookbook\templates\account\password_reset_from_key.html:36
#: .\cookbook\templates\account\password_reset_from_key_done.html:19
msgid "Your password is now changed."
-msgstr ""
+msgstr "Sua senha foi alterada."
#: .\cookbook\templates\account\password_set.html:6
#: .\cookbook\templates\account\password_set.html:16
#: .\cookbook\templates\account\password_set.html:21
msgid "Set Password"
-msgstr ""
+msgstr "Definir Senha"
#: .\cookbook\templates\account\signup.html:6
msgid "Register"
-msgstr ""
+msgstr "Registrar"
#: .\cookbook\templates\account\signup.html:12
msgid "Create an Account"
-msgstr ""
+msgstr "Criar uma Conta"
#: .\cookbook\templates\account\signup.html:42
#: .\cookbook\templates\socialaccount\signup.html:33
msgid "I accept the follwoing"
-msgstr ""
+msgstr "Eu aceito os seguintes"
#: .\cookbook\templates\account\signup.html:45
#: .\cookbook\templates\socialaccount\signup.html:36
msgid "Terms and Conditions"
-msgstr ""
+msgstr "Termos e Condições"
#: .\cookbook\templates\account\signup.html:48
#: .\cookbook\templates\socialaccount\signup.html:39
@@ -942,15 +967,15 @@ msgstr "e"
#: .\cookbook\templates\account\signup.html:52
#: .\cookbook\templates\socialaccount\signup.html:43
msgid "Privacy Policy"
-msgstr ""
+msgstr "Política de Privacidade"
#: .\cookbook\templates\account\signup.html:65
msgid "Create User"
-msgstr ""
+msgstr "Criar Usuário"
#: .\cookbook\templates\account\signup.html:69
msgid "Already have an account?"
-msgstr ""
+msgstr "Já possui uma conta?"
#: .\cookbook\templates\account\signup_closed.html:5
#: .\cookbook\templates\account\signup_closed.html:11
@@ -964,26 +989,26 @@ msgstr ""
#: .\cookbook\templates\api_info.html:5 .\cookbook\templates\base.html:279
#: .\cookbook\templates\rest_framework\api.html:11
msgid "API Documentation"
-msgstr ""
+msgstr "Documentação da API"
#: .\cookbook\templates\base.html:86
msgid "Shopping"
-msgstr ""
+msgstr "Compras"
#: .\cookbook\templates\base.html:113
msgid "Keyword"
-msgstr ""
+msgstr "Palavra-chave"
#: .\cookbook\templates\base.html:125 .\cookbook\views\lists.py:114
msgid "Foods"
-msgstr ""
+msgstr "Alimentos"
#: .\cookbook\templates\base.html:137
#: .\cookbook\templates\forms\ingredients.html:24
#: .\cookbook\templates\space.html:47 .\cookbook\templates\stats.html:26
#: .\cookbook\views\lists.py:131
msgid "Units"
-msgstr ""
+msgstr "Unidades"
#: .\cookbook\templates\base.html:151
#: .\cookbook\templates\shopping_list.html:208
@@ -993,35 +1018,35 @@ msgstr "Supermercado"
#: .\cookbook\templates\base.html:163
msgid "Supermarket Category"
-msgstr ""
+msgstr "Categoria de Supermercado"
#: .\cookbook\templates\base.html:175 .\cookbook\views\lists.py:180
msgid "Automations"
-msgstr ""
+msgstr "Automações"
#: .\cookbook\templates\base.html:189 .\cookbook\views\lists.py:200
msgid "Files"
-msgstr ""
+msgstr "Arquivos"
#: .\cookbook\templates\base.html:201
msgid "Batch Edit"
-msgstr ""
+msgstr "Edição em Lote"
#: .\cookbook\templates\base.html:213 .\cookbook\templates\history.html:6
#: .\cookbook\templates\history.html:14
msgid "History"
-msgstr ""
+msgstr "Histórico"
#: .\cookbook\templates\base.html:228
#: .\cookbook\templates\export_response.html:7
#: .\cookbook\templates\shopping_list.html:310
#: .\cookbook\templates\test2.html:14 .\cookbook\templates\test2.html:20
msgid "Export"
-msgstr ""
+msgstr "Exportar"
#: .\cookbook\templates\base.html:244 .\cookbook\templates\index.html:47
msgid "Import Recipe"
-msgstr ""
+msgstr "Importar Receita"
#: .\cookbook\templates\base.html:246
#: .\cookbook\templates\shopping_list.html:165
@@ -1033,20 +1058,20 @@ msgstr "Criar"
#: .\cookbook\templates\generic\list_template.html:14
#: .\cookbook\templates\space.html:69 .\cookbook\templates\stats.html:43
msgid "External Recipes"
-msgstr ""
+msgstr "Receitas Externas"
#: .\cookbook\templates\base.html:262 .\cookbook\templates\space.html:8
#: .\cookbook\templates\space.html:20 .\cookbook\templates\space.html:150
msgid "Space Settings"
-msgstr ""
+msgstr "Configurar Espaço"
#: .\cookbook\templates\base.html:267 .\cookbook\templates\system.html:13
msgid "System"
-msgstr ""
+msgstr "Sistema"
#: .\cookbook\templates\base.html:269
msgid "Admin"
-msgstr ""
+msgstr "Admin"
#: .\cookbook\templates\base.html:273
msgid "Markdown Guide"
@@ -1054,39 +1079,39 @@ msgstr ""
#: .\cookbook\templates\base.html:275
msgid "GitHub"
-msgstr ""
+msgstr "GitHub"
#: .\cookbook\templates\base.html:277
msgid "Translate Tandoor"
-msgstr ""
+msgstr "Traduzir Tandoor"
#: .\cookbook\templates\base.html:281
msgid "API Browser"
-msgstr ""
+msgstr "API Browser"
#: .\cookbook\templates\base.html:284
msgid "Log out"
-msgstr ""
+msgstr "Logout"
#: .\cookbook\templates\batch\edit.html:6
msgid "Batch edit Category"
-msgstr ""
+msgstr "Editar Categorias em Lote"
#: .\cookbook\templates\batch\edit.html:15
msgid "Batch edit Recipes"
-msgstr ""
+msgstr "Editar Receitas em Lote"
#: .\cookbook\templates\batch\edit.html:20
msgid "Add the specified keywords to all recipes containing a word"
-msgstr ""
+msgstr "Adicionar palavras-chave a todas as receitas que contenham uma palavra"
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:82
msgid "Sync"
-msgstr ""
+msgstr "Sincronizar"
#: .\cookbook\templates\batch\monitor.html:10
msgid "Manage watched Folders"
-msgstr ""
+msgstr "Gerenciar pastas monitoradas"
#: .\cookbook\templates\batch\monitor.html:14
msgid ""
@@ -1096,7 +1121,7 @@ msgstr ""
#: .\cookbook\templates\batch\monitor.html:16
msgid "The path must be in the following format"
-msgstr ""
+msgstr "O caminho deve estar no seguinte formato"
#: .\cookbook\templates\batch\monitor.html:20
#: .\cookbook\templates\forms\edit_import_recipe.html:14
@@ -1109,55 +1134,57 @@ msgstr ""
#: .\cookbook\templates\shopping_list.html:311
#: .\cookbook\templates\space.html:155
msgid "Save"
-msgstr ""
+msgstr "Gravar"
#: .\cookbook\templates\batch\monitor.html:21
msgid "Manage External Storage"
-msgstr ""
+msgstr "Gerenciar Armazenamento Externo"
#: .\cookbook\templates\batch\monitor.html:28
msgid "Sync Now!"
-msgstr ""
+msgstr "Sincronizar Agora!"
#: .\cookbook\templates\batch\monitor.html:29
msgid "Show Recipes"
-msgstr ""
+msgstr "Mostrar Receitas"
#: .\cookbook\templates\batch\monitor.html:30
msgid "Show Log"
-msgstr ""
+msgstr "Mostrar Log"
#: .\cookbook\templates\batch\waiting.html:4
#: .\cookbook\templates\batch\waiting.html:10
msgid "Importing Recipes"
-msgstr ""
+msgstr "Importando Receitas"
#: .\cookbook\templates\batch\waiting.html:28
msgid ""
"This can take a few minutes, depending on the number of recipes in sync, "
"please wait."
msgstr ""
+"Este processo pode demorar alguns minutos, dependendo do número de receitas "
+"a serem importadas."
#: .\cookbook\templates\books.html:7
msgid "Recipe Books"
-msgstr ""
+msgstr "Livros de Receita"
#: .\cookbook\templates\export.html:8 .\cookbook\templates\test2.html:6
msgid "Export Recipes"
-msgstr ""
+msgstr "Exportar Receitas"
#: .\cookbook\templates\forms\edit_import_recipe.html:5
#: .\cookbook\templates\forms\edit_import_recipe.html:9
msgid "Import new Recipe"
-msgstr ""
+msgstr "Importar nova Receita"
#: .\cookbook\templates\forms\edit_internal_recipe.html:7
msgid "Edit Recipe"
-msgstr ""
+msgstr "Editar Receita"
#: .\cookbook\templates\forms\ingredients.html:15
msgid "Edit Ingredients"
-msgstr ""
+msgstr "Editar Ingredientes"
#: .\cookbook\templates\forms\ingredients.html:16
msgid ""
@@ -1177,16 +1204,16 @@ msgstr ""
#: .\cookbook\templates\forms\ingredients.html:31
#: .\cookbook\templates\forms\ingredients.html:40
msgid "Merge"
-msgstr ""
+msgstr "Mesclar"
#: .\cookbook\templates\forms\ingredients.html:36
msgid "Are you sure that you want to merge these two ingredients?"
-msgstr ""
+msgstr "Tem certeza que deseja mesclar estes dois ingredientes?"
#: .\cookbook\templates\generic\delete_template.html:21
#, python-format
msgid "Are you sure you want to delete the %(title)s: %(object)s "
-msgstr ""
+msgstr "Tem certeza que deseja apagar %(title)s: %(object)s "
#: .\cookbook\templates\generic\delete_template.html:26
msgid "Protected"
@@ -1198,47 +1225,47 @@ msgstr ""
#: .\cookbook\templates\generic\delete_template.html:72
msgid "Cancel"
-msgstr ""
+msgstr "Cancelar"
#: .\cookbook\templates\generic\edit_template.html:32
msgid "View"
-msgstr ""
+msgstr "Visualizar"
#: .\cookbook\templates\generic\edit_template.html:36
msgid "Delete original file"
-msgstr ""
+msgstr "Apagar arquivo original"
#: .\cookbook\templates\generic\list_template.html:6
#: .\cookbook\templates\generic\list_template.html:22
msgid "List"
-msgstr ""
+msgstr "Lista"
#: .\cookbook\templates\generic\list_template.html:33
#: .\cookbook\templates\shopping_list.html:33
msgid "Try the new shopping list"
-msgstr ""
+msgstr "Tentar a nova lista de compras"
#: .\cookbook\templates\generic\list_template.html:45
msgid "Filter"
-msgstr ""
+msgstr "Filtro"
#: .\cookbook\templates\generic\list_template.html:50
msgid "Import all"
-msgstr ""
+msgstr "Importar tudo"
#: .\cookbook\templates\generic\table_template.html:76
#: .\cookbook\templates\recipes_table.html:121
msgid "previous"
-msgstr ""
+msgstr "anterior"
#: .\cookbook\templates\generic\table_template.html:98
#: .\cookbook\templates\recipes_table.html:143
msgid "next"
-msgstr ""
+msgstr "próximo"
#: .\cookbook\templates\history.html:20
msgid "View Log"
-msgstr ""
+msgstr "Mostrar Log"
#: .\cookbook\templates\history.html:24
msgid "Cook Log"
@@ -1246,7 +1273,7 @@ msgstr ""
#: .\cookbook\templates\import.html:6 .\cookbook\templates\test.html:6
msgid "Import Recipes"
-msgstr ""
+msgstr "Importar Receitas"
#: .\cookbook\templates\import.html:14 .\cookbook\templates\import.html:20
#: .\cookbook\templates\import_response.html:7
@@ -1258,13 +1285,13 @@ msgstr ""
#: .\cookbook\templates\url_import.html:618 .\cookbook\views\delete.py:89
#: .\cookbook\views\edit.py:200
msgid "Import"
-msgstr ""
+msgstr "Importar"
#: .\cookbook\templates\include\recipe_open_modal.html:7
#: .\cookbook\views\delete.py:39 .\cookbook\views\edit.py:260
#: .\cookbook\views\new.py:53
msgid "Recipe"
-msgstr ""
+msgstr "Receita"
#: .\cookbook\templates\include\recipe_open_modal.html:18
msgid "Close"
@@ -1272,11 +1299,11 @@ msgstr "Fechar"
#: .\cookbook\templates\include\recipe_open_modal.html:32
msgid "Open Recipe"
-msgstr ""
+msgstr "Abrir Receita"
#: .\cookbook\templates\include\storage_backend_warning.html:4
msgid "Security Warning"
-msgstr ""
+msgstr "Alerta de Segurança"
#: .\cookbook\templates\include\storage_backend_warning.html:5
msgid ""
@@ -1293,28 +1320,28 @@ msgstr ""
#: .\cookbook\templates\index.html:29
msgid "Search recipe ..."
-msgstr ""
+msgstr "Pesquisar receita ..."
#: .\cookbook\templates\index.html:44
msgid "New Recipe"
-msgstr ""
+msgstr "Nova Receita"
#: .\cookbook\templates\index.html:53
msgid "Advanced Search"
-msgstr ""
+msgstr "Pesquisa Avançada"
#: .\cookbook\templates\index.html:57
msgid "Reset Search"
-msgstr ""
+msgstr "Reinicializar Pesquisa"
#: .\cookbook\templates\index.html:85
msgid "Last viewed"
-msgstr ""
+msgstr "Último visualizado"
#: .\cookbook\templates\index.html:87 .\cookbook\templates\space.html:37
#: .\cookbook\templates\stats.html:22
msgid "Recipes"
-msgstr ""
+msgstr "Receitas"
#: .\cookbook\templates\index.html:94
msgid "Log in to view recipes"
@@ -1323,7 +1350,7 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:5
#: .\cookbook\templates\markdown_info.html:13
msgid "Markdown Info"
-msgstr ""
+msgstr "Informação Markdown"
#: .\cookbook\templates\markdown_info.html:14
msgid ""
@@ -1343,31 +1370,32 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:25
msgid "Headers"
-msgstr ""
+msgstr "Cabeçalhos"
#: .\cookbook\templates\markdown_info.html:54
msgid "Formatting"
-msgstr ""
+msgstr "Formatando"
#: .\cookbook\templates\markdown_info.html:56
#: .\cookbook\templates\markdown_info.html:72
msgid "Line breaks are inserted by adding two spaces after the end of a line"
msgstr ""
+"As quebras de linha são inseridas adicionando dois espaços no final da linha"
#: .\cookbook\templates\markdown_info.html:57
#: .\cookbook\templates\markdown_info.html:73
msgid "or by leaving a blank line in between."
-msgstr ""
+msgstr "ou deixando uma linha em branco no meio."
#: .\cookbook\templates\markdown_info.html:59
#: .\cookbook\templates\markdown_info.html:74
msgid "This text is bold"
-msgstr ""
+msgstr "Este texto está em negrito"
#: .\cookbook\templates\markdown_info.html:60
#: .\cookbook\templates\markdown_info.html:75
msgid "This text is italic"
-msgstr ""
+msgstr "Este texto está em itálico"
#: .\cookbook\templates\markdown_info.html:61
#: .\cookbook\templates\markdown_info.html:77
@@ -1376,18 +1404,20 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:84
msgid "Lists"
-msgstr ""
+msgstr "Listas"
#: .\cookbook\templates\markdown_info.html:85
msgid ""
"Lists can ordered or unordered. It is important to leave a blank line "
"before the list!"
msgstr ""
+"As listas podem ser ordenadas ou não ordenadas. É importante deixar uma "
+"linha em branco antes da lista!"
#: .\cookbook\templates\markdown_info.html:87
#: .\cookbook\templates\markdown_info.html:108
msgid "Ordered List"
-msgstr ""
+msgstr "Lista Ordenada"
#: .\cookbook\templates\markdown_info.html:89
#: .\cookbook\templates\markdown_info.html:90
@@ -1396,12 +1426,12 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:111
#: .\cookbook\templates\markdown_info.html:112
msgid "unordered list item"
-msgstr ""
+msgstr "item da lista não ordenada"
#: .\cookbook\templates\markdown_info.html:93
#: .\cookbook\templates\markdown_info.html:114
msgid "Unordered List"
-msgstr ""
+msgstr "Lista Não Ordenada"
#: .\cookbook\templates\markdown_info.html:95
#: .\cookbook\templates\markdown_info.html:96
@@ -1410,11 +1440,11 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:117
#: .\cookbook\templates\markdown_info.html:118
msgid "ordered list item"
-msgstr ""
+msgstr "item da lista ordenada"
#: .\cookbook\templates\markdown_info.html:125
msgid "Images & Links"
-msgstr ""
+msgstr "Imagens e Links"
#: .\cookbook\templates\markdown_info.html:126
msgid ""
@@ -1429,7 +1459,7 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:152
msgid "Tables"
-msgstr ""
+msgstr "Tabelas"
#: .\cookbook\templates\markdown_info.html:153
msgid ""
@@ -1443,30 +1473,30 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:171
#: .\cookbook\templates\markdown_info.html:177
msgid "Table"
-msgstr ""
+msgstr "Tabela"
#: .\cookbook\templates\markdown_info.html:155
#: .\cookbook\templates\markdown_info.html:172
msgid "Header"
-msgstr ""
+msgstr "Cabeçalho"
#: .\cookbook\templates\markdown_info.html:157
#: .\cookbook\templates\markdown_info.html:178
msgid "Cell"
-msgstr ""
+msgstr "Célula"
#: .\cookbook\templates\meal_plan_entry.html:6
msgid "Meal Plan View"
-msgstr ""
+msgstr "Visualizar Plano de Refeição"
#: .\cookbook\templates\meal_plan_entry.html:18
msgid "Created by"
-msgstr ""
+msgstr "Criado por"
#: .\cookbook\templates\meal_plan_entry.html:20
#: .\cookbook\templates\shopping_list.html:232
msgid "Shared with"
-msgstr ""
+msgstr "Compartilhado com"
#: .\cookbook\templates\meal_plan_entry.html:48
#: .\cookbook\templates\recipes_table.html:64
@@ -1479,37 +1509,39 @@ msgstr ""
#: .\cookbook\templates\meal_plan_entry.html:76
msgid "Other meals on this day"
-msgstr ""
+msgstr "Outras refeições neste dia"
#: .\cookbook\templates\no_groups_info.html:5
#: .\cookbook\templates\no_groups_info.html:12
msgid "No Permissions"
-msgstr ""
+msgstr "Sem permissão"
#: .\cookbook\templates\no_groups_info.html:17
msgid "You do not have any groups and therefor cannot use this application."
-msgstr ""
+msgstr "Você não tem nenhum grupo e, portanto, não pode usar este aplicativo."
#: .\cookbook\templates\no_groups_info.html:18
#: .\cookbook\templates\no_perm_info.html:15
msgid "Please contact your administrator."
-msgstr ""
+msgstr "Por favor contate seu administrador."
#: .\cookbook\templates\no_perm_info.html:5
#: .\cookbook\templates\no_perm_info.html:12
msgid "No Permission"
-msgstr ""
+msgstr "Sem Permissão"
#: .\cookbook\templates\no_perm_info.html:15
msgid ""
"You do not have the required permissions to view this page or perform this "
"action."
msgstr ""
+"Você não tem as permissões necessárias para visualizar esta página ou "
+"executar esta ação."
#: .\cookbook\templates\no_space_info.html:6
#: .\cookbook\templates\no_space_info.html:13
msgid "No Space"
-msgstr ""
+msgstr "Sem Espaço"
#: .\cookbook\templates\no_space_info.html:17
msgid ""
@@ -1525,11 +1557,11 @@ msgstr ""
#: .\cookbook\templates\no_space_info.html:31
#: .\cookbook\templates\no_space_info.html:40
msgid "Join Space"
-msgstr ""
+msgstr "Entrar no Espaço"
#: .\cookbook\templates\no_space_info.html:34
msgid "Join an existing space."
-msgstr ""
+msgstr "Entrar em um espaço existente."
#: .\cookbook\templates\no_space_info.html:35
msgid ""
@@ -1540,11 +1572,11 @@ msgstr ""
#: .\cookbook\templates\no_space_info.html:48
#: .\cookbook\templates\no_space_info.html:56
msgid "Create Space"
-msgstr ""
+msgstr "Criar Espaço"
#: .\cookbook\templates\no_space_info.html:51
msgid "Create your own recipe space."
-msgstr ""
+msgstr "Criar seu próprio espaço de receita."
#: .\cookbook\templates\no_space_info.html:52
msgid "Start your own recipe space and invite other users to it."
@@ -1552,11 +1584,11 @@ msgstr ""
#: .\cookbook\templates\offline.html:6
msgid "Offline"
-msgstr ""
+msgstr "Offline"
#: .\cookbook\templates\offline.html:19
msgid "You are currently offline!"
-msgstr ""
+msgstr "Você está atualmente offline!"
#: .\cookbook\templates\offline.html:20
msgid ""
@@ -1566,18 +1598,18 @@ msgstr ""
#: .\cookbook\templates\recipe_view.html:26
msgid "by"
-msgstr ""
+msgstr "por"
#: .\cookbook\templates\recipe_view.html:44 .\cookbook\views\delete.py:147
#: .\cookbook\views\edit.py:180
msgid "Comment"
-msgstr ""
+msgstr "Comentário"
#: .\cookbook\templates\recipes_table.html:19
#: .\cookbook\templates\recipes_table.html:23
#: .\cookbook\templates\url_import.html:447
msgid "Recipe Image"
-msgstr ""
+msgstr "Imagem da Receita"
#: .\cookbook\templates\recipes_table.html:51
#: .\cookbook\templates\url_import.html:452
@@ -1591,7 +1623,7 @@ msgstr ""
#: .\cookbook\templates\recipes_table.html:60
msgid "External"
-msgstr ""
+msgstr "Externo"
#: .\cookbook\templates\recipes_table.html:86
msgid "Log Cooking"
@@ -1605,7 +1637,7 @@ msgstr ""
#: .\cookbook\templates\search_info.html:9
#: .\cookbook\templates\settings.html:172
msgid "Search Settings"
-msgstr ""
+msgstr "Configurações da Pesquisa"
#: .\cookbook\templates\search_info.html:10
msgid ""
@@ -1621,7 +1653,7 @@ msgstr ""
#: .\cookbook\templates\search_info.html:19
msgid "Search Methods"
-msgstr ""
+msgstr "Métodos de Pesquisa"
#: .\cookbook\templates\search_info.html:23
msgid ""
@@ -1704,7 +1736,7 @@ msgstr ""
#: .\cookbook\templates\search_info.html:69
msgid "Search Fields"
-msgstr ""
+msgstr "Campos de Pesquisa"
#: .\cookbook\templates\search_info.html:73
msgid ""
@@ -1743,7 +1775,7 @@ msgstr ""
#: .\cookbook\templates\search_info.html:95
msgid "Search Index"
-msgstr ""
+msgstr "Índice de Pesquisa"
#: .\cookbook\templates\search_info.html:99
msgid ""
@@ -1760,52 +1792,52 @@ msgstr ""
#: .\cookbook\templates\settings.html:28
msgid "Account"
-msgstr ""
+msgstr "Conta"
#: .\cookbook\templates\settings.html:35
msgid "Preferences"
-msgstr ""
+msgstr "Preferências"
#: .\cookbook\templates\settings.html:42
msgid "API-Settings"
-msgstr ""
+msgstr "API-Configurações"
#: .\cookbook\templates\settings.html:49
msgid "Search-Settings"
-msgstr ""
+msgstr "Pesquisa-Configurações"
#: .\cookbook\templates\settings.html:56
msgid "Shopping-Settings"
-msgstr ""
+msgstr "Compras-Configurações"
#: .\cookbook\templates\settings.html:65
msgid "Name Settings"
-msgstr ""
+msgstr "Configurações de Nome"
#: .\cookbook\templates\settings.html:73
msgid "Account Settings"
-msgstr ""
+msgstr "Configurações de Conta"
#: .\cookbook\templates\settings.html:75
msgid "Emails"
-msgstr ""
+msgstr "Emails"
#: .\cookbook\templates\settings.html:78
#: .\cookbook\templates\socialaccount\connections.html:11
msgid "Social"
-msgstr ""
+msgstr "Social"
#: .\cookbook\templates\settings.html:91
msgid "Language"
-msgstr ""
+msgstr "Idioma"
#: .\cookbook\templates\settings.html:121
msgid "Style"
-msgstr ""
+msgstr "Estilo"
#: .\cookbook\templates\settings.html:142
msgid "API Token"
-msgstr ""
+msgstr "Token API"
#: .\cookbook\templates\settings.html:143
msgid ""
@@ -1859,7 +1891,7 @@ msgstr ""
#: .\cookbook\templates\settings.html:183
#: .\cookbook\templates\settings.html:191
msgid "Apply"
-msgstr ""
+msgstr "Aplicar"
#: .\cookbook\templates\settings.html:188
msgid "Precise"
@@ -1877,7 +1909,7 @@ msgstr ""
#: .\cookbook\templates\settings.html:207
msgid "Shopping Settings"
-msgstr ""
+msgstr "Configurações de Compras"
#: .\cookbook\templates\setup.html:6 .\cookbook\templates\system.html:5
msgid "Cookbook Setup"
@@ -1894,17 +1926,17 @@ msgstr ""
#: .\cookbook\templates\setup.html:20
msgid "Create Superuser account"
-msgstr ""
+msgstr "Criar conta Superusuário"
#: .\cookbook\templates\shopping_list.html:8
#: .\cookbook\templates\shopping_list.html:29
#: .\cookbook\templates\shopping_list.html:663
msgid "Shopping List"
-msgstr ""
+msgstr "Lista de Compras"
#: .\cookbook\templates\shopping_list.html:55
msgid "Search Recipe"
-msgstr ""
+msgstr "Pesquisar Receita"
#: .\cookbook\templates\shopping_list.html:72
msgid "Shopping Recipes"
@@ -1912,23 +1944,23 @@ msgstr ""
#: .\cookbook\templates\shopping_list.html:74
msgid "No recipes selected"
-msgstr ""
+msgstr "Nenhuma receita selecionada"
#: .\cookbook\templates\shopping_list.html:131
msgid "Entry Mode"
-msgstr ""
+msgstr "Modo Entrada"
#: .\cookbook\templates\shopping_list.html:139
msgid "Add Entry"
-msgstr ""
+msgstr "Incluir Entrada"
#: .\cookbook\templates\shopping_list.html:152
msgid "Amount"
-msgstr ""
+msgstr "Quantidade"
#: .\cookbook\templates\shopping_list.html:164
msgid "Select Unit"
-msgstr ""
+msgstr "Selecionar Unidade"
#: .\cookbook\templates\shopping_list.html:166
#: .\cookbook\templates\shopping_list.html:189
@@ -1937,23 +1969,23 @@ msgstr ""
#: .\cookbook\templates\url_import.html:505
#: .\cookbook\templates\url_import.html:537
msgid "Select"
-msgstr ""
+msgstr "Selecionar"
#: .\cookbook\templates\shopping_list.html:187
msgid "Select Food"
-msgstr ""
+msgstr "Selecionar Alimento"
#: .\cookbook\templates\shopping_list.html:218
msgid "Select Supermarket"
-msgstr ""
+msgstr "Selecionar Supermercado"
#: .\cookbook\templates\shopping_list.html:242
msgid "Select User"
-msgstr ""
+msgstr "Selecionar Usuário"
#: .\cookbook\templates\shopping_list.html:258
msgid "Finished"
-msgstr ""
+msgstr "Finalizado"
#: .\cookbook\templates\shopping_list.html:267
msgid "You are offline, shopping list might not synchronize."
@@ -1961,12 +1993,12 @@ msgstr ""
#: .\cookbook\templates\shopping_list.html:318
msgid "Copy/Export"
-msgstr ""
+msgstr "Copiar/Exportar"
#: .\cookbook\templates\socialaccount\connections.html:4
#: .\cookbook\templates\socialaccount\connections.html:15
msgid "Account Connections"
-msgstr ""
+msgstr "Conexões de Conta"
#: .\cookbook\templates\socialaccount\connections.html:18
msgid ""
@@ -1981,7 +2013,7 @@ msgstr ""
#: .\cookbook\templates\socialaccount\connections.html:55
msgid "Add a 3rd Party Account"
-msgstr ""
+msgstr "Incluir uma conta de terceiros"
#: .\cookbook\templates\socialaccount\signup.html:5
msgid "Signup"
@@ -2014,15 +2046,15 @@ msgstr ""
#: .\cookbook\templates\space.html:25
msgid "Space:"
-msgstr ""
+msgstr "Espaço:"
#: .\cookbook\templates\space.html:26
msgid "Manage Subscription"
-msgstr ""
+msgstr "Gerenciar Assinatura"
#: .\cookbook\templates\space.html:34 .\cookbook\templates\stats.html:19
msgid "Number of objects"
-msgstr ""
+msgstr "Número de objetos"
#: .\cookbook\templates\space.html:54 .\cookbook\templates\stats.html:30
msgid "Recipe Imports"
@@ -2034,31 +2066,31 @@ msgstr ""
#: .\cookbook\templates\space.html:65 .\cookbook\templates\stats.html:41
msgid "Recipes without Keywords"
-msgstr ""
+msgstr "Receitas sem Palavras-chaves"
#: .\cookbook\templates\space.html:73 .\cookbook\templates\stats.html:45
msgid "Internal Recipes"
-msgstr ""
+msgstr "Receitas Internas"
#: .\cookbook\templates\space.html:89
msgid "Members"
-msgstr ""
+msgstr "Membros"
#: .\cookbook\templates\space.html:95
msgid "Invite User"
-msgstr ""
+msgstr "Convidar Usuário"
#: .\cookbook\templates\space.html:107
msgid "User"
-msgstr ""
+msgstr "Usuário"
#: .\cookbook\templates\space.html:108
msgid "Groups"
-msgstr ""
+msgstr "Grupos"
#: .\cookbook\templates\space.html:119
msgid "admin"
-msgstr ""
+msgstr "admin"
#: .\cookbook\templates\space.html:120
msgid "user"
@@ -2066,15 +2098,15 @@ msgstr "usuário"
#: .\cookbook\templates\space.html:121
msgid "guest"
-msgstr ""
+msgstr "convidado"
#: .\cookbook\templates\space.html:122
msgid "remove"
-msgstr ""
+msgstr "remover"
#: .\cookbook\templates\space.html:126
msgid "Update"
-msgstr ""
+msgstr "Atualizar"
#: .\cookbook\templates\space.html:130
msgid "You cannot edit yourself."
@@ -2082,28 +2114,28 @@ msgstr ""
#: .\cookbook\templates\space.html:136
msgid "There are no members in your space yet!"
-msgstr ""
+msgstr "Ainda não há membros no seu espaço!"
#: .\cookbook\templates\space.html:143 .\cookbook\templates\system.html:21
#: .\cookbook\views\lists.py:85
msgid "Invite Links"
-msgstr ""
+msgstr "Links de Convite"
#: .\cookbook\templates\stats.html:4
msgid "Stats"
-msgstr ""
+msgstr "Estatísticas"
#: .\cookbook\templates\stats.html:10
msgid "Statistics"
-msgstr ""
+msgstr "Estatísticas"
#: .\cookbook\templates\system.html:22
msgid "Show Links"
-msgstr ""
+msgstr "Mostrar Links"
#: .\cookbook\templates\system.html:32
msgid "System Information"
-msgstr ""
+msgstr "Informações do Sistema"
#: .\cookbook\templates\system.html:34
msgid ""
@@ -2123,12 +2155,12 @@ msgstr ""
#: .\cookbook\templates\system.html:49 .\cookbook\templates\system.html:64
#: .\cookbook\templates\system.html:80
msgid "Warning"
-msgstr ""
+msgstr "Alerta"
#: .\cookbook\templates\system.html:49 .\cookbook\templates\system.html:64
#: .\cookbook\templates\system.html:80 .\cookbook\templates\system.html:95
msgid "Ok"
-msgstr ""
+msgstr "Ok"
#: .\cookbook\templates\system.html:51
msgid ""
@@ -2143,11 +2175,11 @@ msgstr ""
#: .\cookbook\templates\system.html:57 .\cookbook\templates\system.html:73
#: .\cookbook\templates\system.html:88 .\cookbook\templates\system.html:102
msgid "Everything is fine!"
-msgstr ""
+msgstr "Tudo está bem!"
#: .\cookbook\templates\system.html:62
msgid "Secret Key"
-msgstr ""
+msgstr "Chave Secreta"
#: .\cookbook\templates\system.html:66
msgid ""
@@ -2164,7 +2196,7 @@ msgstr ""
#: .\cookbook\templates\system.html:78
msgid "Debug Mode"
-msgstr ""
+msgstr "Modo Debug"
#: .\cookbook\templates\system.html:82
msgid ""
@@ -2179,11 +2211,11 @@ msgstr ""
#: .\cookbook\templates\system.html:93
msgid "Database"
-msgstr ""
+msgstr "Banco de Dados"
#: .\cookbook\templates\system.html:95
msgid "Info"
-msgstr ""
+msgstr "Informação"
#: .\cookbook\templates\system.html:97
msgid ""
@@ -2212,19 +2244,19 @@ msgstr "URL"
#: .\cookbook\templates\url_import.html:40
msgid "App"
-msgstr ""
+msgstr "App"
#: .\cookbook\templates\url_import.html:44
msgid "Text"
-msgstr ""
+msgstr "Texto"
#: .\cookbook\templates\url_import.html:46
msgid "File"
-msgstr ""
+msgstr "Arquivo"
#: .\cookbook\templates\url_import.html:64
msgid "Enter website URL"
-msgstr ""
+msgstr "Entre a URL do website"
#: .\cookbook\templates\url_import.html:104
msgid "Select recipe files to import or drop them here..."
@@ -2232,7 +2264,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:125
msgid "Paste json or html source here to load recipe."
-msgstr ""
+msgstr "Colar aqui o código json ou html para carregar a receita."
#: .\cookbook\templates\url_import.html:153
msgid "Preview Recipe Data"
@@ -2253,7 +2285,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:307
#: .\cookbook\templates\url_import.html:358
msgid "Clear Contents"
-msgstr ""
+msgstr "Limpar Conteúdo"
#: .\cookbook\templates\url_import.html:165
msgid "Text dragged here will be appended to the name."
@@ -2261,7 +2293,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:178
msgid "Description"
-msgstr ""
+msgstr "Descrição"
#: .\cookbook\templates\url_import.html:182
msgid "Text dragged here will be appended to the description."
@@ -2277,11 +2309,11 @@ msgstr "Imagem"
#: .\cookbook\templates\url_import.html:246
msgid "Prep Time"
-msgstr ""
+msgstr "Tempo de Preparação"
#: .\cookbook\templates\url_import.html:261
msgid "Cook Time"
-msgstr ""
+msgstr "Tempo de Cozimento"
#: .\cookbook\templates\url_import.html:282
msgid "Ingredients dragged here will be appended to current list."
@@ -2290,7 +2322,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:304
#: .\cookbook\templates\url_import.html:579
msgid "Instructions"
-msgstr ""
+msgstr "Instruções"
#: .\cookbook\templates\url_import.html:309
msgid ""
@@ -2299,7 +2331,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:332
msgid "Discovered Attributes"
-msgstr ""
+msgstr "Atributos Descobertos"
#: .\cookbook\templates\url_import.html:334
msgid ""
@@ -2309,11 +2341,11 @@ msgstr ""
#: .\cookbook\templates\url_import.html:351
msgid "Show Blank Field"
-msgstr ""
+msgstr "Mostrar Campo em Branco"
#: .\cookbook\templates\url_import.html:356
msgid "Blank Field"
-msgstr ""
+msgstr "Campo Branco"
#: .\cookbook\templates\url_import.html:360
msgid "Items dragged to Blank Field will be appended."
@@ -2321,37 +2353,37 @@ msgstr ""
#: .\cookbook\templates\url_import.html:407
msgid "Delete Text"
-msgstr ""
+msgstr "Apagar Texto"
#: .\cookbook\templates\url_import.html:420
msgid "Delete image"
-msgstr ""
+msgstr "Apagar Imagem"
#: .\cookbook\templates\url_import.html:436
msgid "Recipe Name"
-msgstr ""
+msgstr "Nome da Receita"
#: .\cookbook\templates\url_import.html:440
msgid "Recipe Description"
-msgstr ""
+msgstr "Descrição da Receita"
#: .\cookbook\templates\url_import.html:504
#: .\cookbook\templates\url_import.html:536
#: .\cookbook\templates\url_import.html:596
msgid "Select one"
-msgstr ""
+msgstr "Selecione um"
#: .\cookbook\templates\url_import.html:554
msgid "Note"
-msgstr ""
+msgstr "Nota"
#: .\cookbook\templates\url_import.html:597
msgid "Add Keyword"
-msgstr ""
+msgstr "Incluir Palavra-chave"
#: .\cookbook\templates\url_import.html:610
msgid "All Keywords"
-msgstr ""
+msgstr "Todas as Palavras-chaves"
#: .\cookbook\templates\url_import.html:613
msgid "Import all keywords, not only the ones already existing."
@@ -2378,7 +2410,7 @@ msgstr ""
#: .\cookbook\templates\url_import.html:653
msgid "GitHub Issues"
-msgstr ""
+msgstr "Issues GitHub"
#: .\cookbook\templates\url_import.html:655
msgid "Recipe Markup Specification"
@@ -2537,7 +2569,7 @@ msgstr ""
#: .\cookbook\views\api.py:971
msgid "Sync successful!"
-msgstr ""
+msgstr "Sincronização realizada com sucesso!"
#: .\cookbook\views\api.py:976
msgid "Error synchronizing with Storage"
@@ -2545,7 +2577,7 @@ msgstr ""
#: .\cookbook\views\api.py:1055
msgid "Nothing to do."
-msgstr ""
+msgstr "Nada para fazer."
#: .\cookbook\views\api.py:1070
msgid "The requested site provided malformed data and cannot be read."
@@ -2594,7 +2626,7 @@ msgstr[1] ""
#: .\cookbook\views\delete.py:101
msgid "Monitor"
-msgstr ""
+msgstr "Monitor"
#: .\cookbook\views\delete.py:125 .\cookbook\views\lists.py:71
#: .\cookbook\views\new.py:101
@@ -2608,7 +2640,7 @@ msgstr ""
#: .\cookbook\views\delete.py:158
msgid "Recipe Book"
-msgstr ""
+msgstr "Livro de Receita"
#: .\cookbook\views\delete.py:170
msgid "Bookmarks"
@@ -2650,7 +2682,7 @@ msgstr ""
#: .\cookbook\views\lists.py:25
msgid "Import Log"
-msgstr ""
+msgstr "Importar Log"
#: .\cookbook\views\lists.py:38
msgid "Discovery"
@@ -2658,27 +2690,27 @@ msgstr ""
#: .\cookbook\views\lists.py:54
msgid "Shopping Lists"
-msgstr ""
+msgstr "Listas de Compras"
#: .\cookbook\views\lists.py:148
msgid "Supermarkets"
-msgstr ""
+msgstr "Supermercados"
#: .\cookbook\views\lists.py:164
msgid "Shopping Categories"
-msgstr ""
+msgstr "Categorias de Compras"
#: .\cookbook\views\lists.py:217
msgid "Steps"
-msgstr ""
+msgstr "Etapas"
#: .\cookbook\views\lists.py:232
msgid "New Shopping List"
-msgstr ""
+msgstr "Nova Lista de Compras"
#: .\cookbook\views\new.py:126
msgid "Imported new recipe!"
-msgstr ""
+msgstr "Nova receita importada!"
#: .\cookbook\views\new.py:129
msgid "There was an error importing this recipe!"
@@ -2686,11 +2718,11 @@ msgstr ""
#: .\cookbook\views\new.py:212
msgid "Hello"
-msgstr ""
+msgstr "Olá"
#: .\cookbook\views\new.py:212
msgid "You have been invited by "
-msgstr ""
+msgstr "Você foi convidado por "
#: .\cookbook\views\new.py:213
msgid " to join their Tandoor Recipes space "
@@ -2744,7 +2776,7 @@ msgstr ""
#: .\cookbook\views\views.py:185
msgid "Comment saved!"
-msgstr ""
+msgstr "Comentário gravado!"
#: .\cookbook\views\views.py:276
msgid "This feature is not available in the demo version!"
diff --git a/cookbook/management/commands/rebuildindex.py b/cookbook/management/commands/rebuildindex.py
index 6ca4038cdf..c0bba9fa60 100644
--- a/cookbook/management/commands/rebuildindex.py
+++ b/cookbook/management/commands/rebuildindex.py
@@ -1,9 +1,9 @@
from django.conf import settings
from django.contrib.postgres.search import SearchVector
from django.core.management.base import BaseCommand
-from django_scopes import scopes_disabled
from django.utils import translation
from django.utils.translation import gettext_lazy as _
+from django_scopes import scopes_disabled
from cookbook.managers import DICTIONARY
from cookbook.models import Recipe, Step
@@ -14,7 +14,7 @@ class Command(BaseCommand):
help = _('Rebuilds full text search index on Recipe')
def handle(self, *args, **options):
- if settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
+ if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.postgresql':
self.stdout.write(self.style.WARNING(_('Only Postgresql databases use full text search, no index to rebuild')))
try:
diff --git a/cookbook/managers.py b/cookbook/managers.py
index 5bfc60e43c..09f4bd58ab 100644
--- a/cookbook/managers.py
+++ b/cookbook/managers.py
@@ -34,35 +34,14 @@ def search(self, search_text, space):
+ SearchVector(StringAgg('steps__ingredients__food__name__unaccent', delimiter=' '), weight='B', config=language)
+ SearchVector(StringAgg('keywords__name__unaccent', delimiter=' '), weight='B', config=language))
search_rank = SearchRank(search_vectors, search_query)
- # USING TRIGRAM BREAKS WEB SEARCH
- # ADDING MULTIPLE TRIGRAMS CREATES DUPLICATE RESULTS
- # DISTINCT NOT COMPAITBLE WITH ANNOTATE
- # trigram_name = (TrigramSimilarity('name', search_text))
- # trigram_description = (TrigramSimilarity('description', search_text))
- # trigram_food = (TrigramSimilarity('steps__ingredients__food__name', search_text))
- # trigram_keyword = (TrigramSimilarity('keywords__name', search_text))
- # adding additional trigrams created duplicates
- # + TrigramSimilarity('description', search_text)
- # + TrigramSimilarity('steps__ingredients__food__name', search_text)
- # + TrigramSimilarity('keywords__name', search_text)
+
return (
self.get_queryset()
.annotate(
search=search_vectors,
rank=search_rank,
- # trigram=trigram_name+trigram_description+trigram_food+trigram_keyword
- # trigram_name=trigram_name,
- # trigram_description=trigram_description,
- # trigram_food=trigram_food,
- # trigram_keyword=trigram_keyword
)
.filter(
Q(search=search_query)
- # | Q(trigram_name__gt=0.1)
- # | Q(name__icontains=search_text)
- # | Q(trigram_name__gt=0.2)
- # | Q(trigram_description__gt=0.2)
- # | Q(trigram_food__gt=0.2)
- # | Q(trigram_keyword__gt=0.2)
)
.order_by('-rank'))
diff --git a/cookbook/migrations/0143_build_full_text_index.py b/cookbook/migrations/0143_build_full_text_index.py
index 641efe4ea1..793e83be58 100644
--- a/cookbook/migrations/0143_build_full_text_index.py
+++ b/cookbook/migrations/0143_build_full_text_index.py
@@ -9,7 +9,7 @@
from django_scopes import scopes_disabled
from cookbook.managers import DICTIONARY
-from cookbook.models import (Index, PermissionModelMixin, Recipe, Step, SearchFields)
+from cookbook.models import Index, PermissionModelMixin, Recipe, SearchFields, Step
def allSearchFields():
@@ -21,7 +21,7 @@ def nameSearchField():
def set_default_search_vector(apps, schema_editor):
- if settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
+ if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.postgresql':
return
language = DICTIONARY.get(translation.get_language(), 'simple')
with scopes_disabled():
diff --git a/cookbook/migrations/0199_alter_propertytype_options_alter_automation_type_and_more.py b/cookbook/migrations/0199_alter_propertytype_options_alter_automation_type_and_more.py
index 56da9d2ab4..39734349ea 100644
--- a/cookbook/migrations/0199_alter_propertytype_options_alter_automation_type_and_more.py
+++ b/cookbook/migrations/0199_alter_propertytype_options_alter_automation_type_and_more.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.1.10 on 2023-08-25 13:05
+# Generated by Django 4.1.10 on 2023-09-01 17:03
from django.db import migrations, models
@@ -15,20 +15,16 @@ class Migration(migrations.Migration):
name='type',
field=models.CharField(
choices=[
- ('FOOD_ALIAS',
- 'Food Alias'),
- ('UNIT_ALIAS',
- 'Unit Alias'),
- ('KEYWORD_ALIAS',
- 'Keyword Alias'),
- ('DESCRIPTION_REPLACE',
- 'Description Replace'),
- ('INSTRUCTION_REPLACE',
- 'Instruction Replace'),
- ('NEVER_UNIT',
- 'Never Unit'),
- ('TRANSPOSE_WORDS',
- 'Transpose Words')],
+ ('FOOD_ALIAS', 'Food Alias'),
+ ('UNIT_ALIAS', 'Unit Alias'),
+ ('KEYWORD_ALIAS', 'Keyword Alias'),
+ ('DESCRIPTION_REPLACE', 'Description Replace'),
+ ('INSTRUCTION_REPLACE', 'Instruction Replace'),
+ ('NEVER_UNIT', 'Never Unit'),
+ ('TRANSPOSE_WORDS', 'Transpose Words'),
+ ('FOOD_REPLACE', 'Food Replace'),
+ ('UNIT_REPLACE', 'Unit Replace'),
+ ('NAME_REPLACE', 'Name Replace')],
max_length=128),
),
]
diff --git a/cookbook/migrations/0200_alter_propertytype_options_remove_keyword_icon_and_more.py b/cookbook/migrations/0200_alter_propertytype_options_remove_keyword_icon_and_more.py
new file mode 100644
index 0000000000..104d6a7211
--- /dev/null
+++ b/cookbook/migrations/0200_alter_propertytype_options_remove_keyword_icon_and_more.py
@@ -0,0 +1,52 @@
+# Generated by Django 4.1.10 on 2023-08-29 11:59
+
+from django.db import migrations
+from django.db.models import F, Value
+from django.db.models.functions import Concat
+from django_scopes import scopes_disabled
+
+
+def migrate_icons(apps, schema_editor):
+ with scopes_disabled():
+ MealType = apps.get_model('cookbook', 'MealType')
+ Keyword = apps.get_model('cookbook', 'Keyword')
+ PropertyType = apps.get_model('cookbook', 'PropertyType')
+ RecipeBook = apps.get_model('cookbook', 'RecipeBook')
+
+ MealType.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
+ Keyword.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
+ PropertyType.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
+ RecipeBook.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('cookbook', '0199_alter_propertytype_options_alter_automation_type_and_more'),
+ ]
+
+ operations = [
+
+ migrations.RunPython(
+ migrate_icons
+ ),
+ migrations.AlterModelOptions(
+ name='propertytype',
+ options={'ordering': ('order',)},
+ ),
+ migrations.RemoveField(
+ model_name='keyword',
+ name='icon',
+ ),
+ migrations.RemoveField(
+ model_name='mealtype',
+ name='icon',
+ ),
+ migrations.RemoveField(
+ model_name='propertytype',
+ name='icon',
+ ),
+ migrations.RemoveField(
+ model_name='recipebook',
+ name='icon',
+ ),
+ ]
diff --git a/cookbook/migrations/0201_rename_date_mealplan_from_date_mealplan_to_date.py b/cookbook/migrations/0201_rename_date_mealplan_from_date_mealplan_to_date.py
new file mode 100644
index 0000000000..343d758edb
--- /dev/null
+++ b/cookbook/migrations/0201_rename_date_mealplan_from_date_mealplan_to_date.py
@@ -0,0 +1,36 @@
+# Generated by Django 4.1.10 on 2023-09-08 12:20
+
+from django.db import migrations, models
+from django.db.models import F
+from django_scopes import scopes_disabled
+
+
+def apply_migration(apps, schema_editor):
+ with scopes_disabled():
+ MealPlan = apps.get_model('cookbook', 'MealPlan')
+ MealPlan.objects.update(to_date=F('from_date'))
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('cookbook', '0200_alter_propertytype_options_remove_keyword_icon_and_more'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='mealplan',
+ old_name='date',
+ new_name='from_date',
+ ),
+ migrations.AddField(
+ model_name='mealplan',
+ name='to_date',
+ field=models.DateField(blank=True, null=True),
+ ),
+ migrations.RunPython(apply_migration),
+ migrations.AlterField(
+ model_name='mealplan',
+ name='to_date',
+ field=models.DateField(),
+ ),
+ ]
diff --git a/cookbook/migrations/0202_remove_space_show_facet_count.py b/cookbook/migrations/0202_remove_space_show_facet_count.py
new file mode 100644
index 0000000000..147c718dd9
--- /dev/null
+++ b/cookbook/migrations/0202_remove_space_show_facet_count.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.1.10 on 2023-09-12 13:37
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cookbook', '0201_rename_date_mealplan_from_date_mealplan_to_date'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='space',
+ name='show_facet_count',
+ ),
+ ]
diff --git a/cookbook/migrations/0203_alter_unique_contstraints.py b/cookbook/migrations/0203_alter_unique_contstraints.py
new file mode 100644
index 0000000000..7dc6cc08c4
--- /dev/null
+++ b/cookbook/migrations/0203_alter_unique_contstraints.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.5 on 2023-09-14 12:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cookbook', '0202_remove_space_show_facet_count'),
+ ]
+
+ operations = [
+ migrations.AddConstraint(
+ model_name='mealtype',
+ constraint=models.UniqueConstraint(fields=('space', 'name'), name='mt_unique_name_per_space'),
+ ),
+ ]
diff --git a/cookbook/migrations/0204_propertytype_fdc_id.py b/cookbook/migrations/0204_propertytype_fdc_id.py
new file mode 100644
index 0000000000..1744cc9bc1
--- /dev/null
+++ b/cookbook/migrations/0204_propertytype_fdc_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.7 on 2023-11-27 21:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cookbook', '0203_alter_unique_contstraints'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='propertytype',
+ name='fdc_id',
+ field=models.CharField(blank=True, default=None, max_length=128, null=True),
+ ),
+ ]
diff --git a/cookbook/models.py b/cookbook/models.py
index 80ef548dc6..66a4534d3c 100644
--- a/cookbook/models.py
+++ b/cookbook/models.py
@@ -116,10 +116,7 @@ class TreeModel(MP_Node):
_full_name_separator = ' > '
def __str__(self):
- if self.icon:
- return f"{self.icon} {self.name}"
- else:
- return f"{self.name}"
+ return f"{self.name}"
@property
def parent(self):
@@ -188,7 +185,6 @@ def exclude_descendants(queryset=None, filter=None):
:param filter: Filter (include) the descendants nodes with the provided Q filter
"""
descendants = Q()
- # TODO filter the queryset nodes to exclude descendants of objects in the queryset
nodes = queryset.values('path', 'depth')
for node in nodes:
descendants |= Q(path__startswith=node['path'], depth__gt=node['depth'])
@@ -268,7 +264,6 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
no_sharing_limit = models.BooleanField(default=False)
demo = models.BooleanField(default=False)
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
- show_facet_count = models.BooleanField(default=False)
internal_note = models.TextField(blank=True, null=True)
@@ -533,7 +528,6 @@ class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelM
if SORT_TREE_BY_NAME:
node_order_by = ['name']
name = models.CharField(max_length=64)
- icon = models.CharField(max_length=16, blank=True, null=True)
description = models.TextField(default="", blank=True)
created_at = models.DateTimeField(auto_now_add=True) # TODO deprecate
updated_at = models.DateTimeField(auto_now=True) # TODO deprecate
@@ -767,13 +761,13 @@ class PropertyType(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
unit = models.CharField(max_length=64, blank=True, null=True)
- icon = models.CharField(max_length=16, blank=True, null=True)
order = models.IntegerField(default=0)
description = models.CharField(max_length=512, blank=True, null=True)
category = models.CharField(max_length=64, choices=((NUTRITION, _('Nutrition')), (ALLERGEN, _('Allergen')),
- (PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
+ (PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
+ fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None)
# TODO show if empty property?
# TODO formatting property?
@@ -937,7 +931,6 @@ def __str__(self):
class RecipeBook(ExportModelOperationsMixin('book'), models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
description = models.TextField(blank=True)
- icon = models.CharField(max_length=16, blank=True, null=True)
shared = models.ManyToManyField(User, blank=True, related_name='shared_with')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
filter = models.ForeignKey('cookbook.CustomFilter', null=True, blank=True, on_delete=models.SET_NULL)
@@ -980,7 +973,6 @@ class Meta:
class MealType(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
order = models.IntegerField(default=0)
- icon = models.CharField(max_length=16, blank=True, null=True)
color = models.CharField(max_length=7, blank=True, null=True)
default = models.BooleanField(default=False, blank=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
@@ -991,6 +983,11 @@ class MealType(models.Model, PermissionModelMixin):
def __str__(self):
return self.name
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(fields=['space', 'name'], name='mt_unique_name_per_space'),
+ ]
+
class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
@@ -1000,7 +997,8 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission
shared = models.ManyToManyField(User, blank=True, related_name='plan_share')
meal_type = models.ForeignKey(MealType, on_delete=models.CASCADE)
note = models.TextField(blank=True)
- date = models.DateField()
+ from_date = models.DateField()
+ to_date = models.DateField()
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@@ -1014,7 +1012,7 @@ def get_meal_name(self):
return self.meal_type.name
def __str__(self):
- return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
+ return f'{self.get_label()} - {self.from_date} - {self.meal_type.name}'
class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), models.Model, PermissionModelMixin):
@@ -1302,7 +1300,7 @@ class UserFile(ExportModelOperationsMixin('user_files'), models.Model, Permissio
def is_image(self):
try:
- img = Image.open(self.file.file.file)
+ Image.open(self.file.file.file)
return True
except Exception:
return False
@@ -1322,11 +1320,23 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
INSTRUCTION_REPLACE = 'INSTRUCTION_REPLACE'
NEVER_UNIT = 'NEVER_UNIT'
TRANSPOSE_WORDS = 'TRANSPOSE_WORDS'
+ FOOD_REPLACE = 'FOOD_REPLACE'
+ UNIT_REPLACE = 'UNIT_REPLACE'
+ NAME_REPLACE = 'NAME_REPLACE'
type = models.CharField(max_length=128,
- choices=((FOOD_ALIAS, _('Food Alias')), (UNIT_ALIAS, _('Unit Alias')), (KEYWORD_ALIAS, _('Keyword Alias')),
- (DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')),
- (NEVER_UNIT, _('Never Unit')), (TRANSPOSE_WORDS, _('Transpose Words')),))
+ choices=(
+ (FOOD_ALIAS, _('Food Alias')),
+ (UNIT_ALIAS, _('Unit Alias')),
+ (KEYWORD_ALIAS, _('Keyword Alias')),
+ (DESCRIPTION_REPLACE, _('Description Replace')),
+ (INSTRUCTION_REPLACE, _('Instruction Replace')),
+ (NEVER_UNIT, _('Never Unit')),
+ (TRANSPOSE_WORDS, _('Transpose Words')),
+ (FOOD_REPLACE, _('Food Replace')),
+ (UNIT_REPLACE, _('Unit Replace')),
+ (NAME_REPLACE, _('Name Replace')),
+ ))
name = models.CharField(max_length=128, default='')
description = models.TextField(blank=True, null=True)
diff --git a/cookbook/schemas.py b/cookbook/schemas.py
index 36ce665551..e465de45b8 100644
--- a/cookbook/schemas.py
+++ b/cookbook/schemas.py
@@ -67,17 +67,3 @@ def get_path_parameters(self, path, method):
'schema': {'type': 'string', },
})
return parameters
-
-
-# class QueryOnlySchema(AutoSchema):
-# def get_path_parameters(self, path, method):
-# if not is_list_view(path, method, self.view):
-# return super(QueryOnlySchema, self).get_path_parameters(path, method)
-
-# parameters = super().get_path_parameters(path, method)
-# parameters.append({
-# "name": 'query', "in": "query", "required": False,
-# "description": 'Query string matched (fuzzy) against object name.',
-# 'schema': {'type': 'string', },
-# })
-# return parameters
diff --git a/cookbook/serializer.py b/cookbook/serializer.py
index 04f9bcad4d..e012d330a0 100644
--- a/cookbook/serializer.py
+++ b/cookbook/serializer.py
@@ -1,4 +1,3 @@
-import random
import traceback
import uuid
from datetime import datetime, timedelta
@@ -7,34 +6,34 @@
from html import escape
from smtplib import SMTPException
-from django.contrib.auth.models import Group, User, AnonymousUser
+from django.contrib.auth.models import AnonymousUser, Group, User
from django.core.cache import caches
from django.core.mail import send_mail
-from django.db.models import Avg, Q, QuerySet, Sum
+from django.db.models import Q, QuerySet, Sum
from django.http import BadHeaderError
from django.urls import reverse
from django.utils import timezone
from django_scopes import scopes_disabled
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
-from PIL import Image
from oauth2_provider.models import AccessToken
+from PIL import Image
from rest_framework import serializers
from rest_framework.exceptions import NotFound, ValidationError
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
from cookbook.helper.HelperFunctions import str2bool
-from cookbook.helper.property_helper import FoodPropertyHelper
from cookbook.helper.permission_helper import above_space_limit
+from cookbook.helper.property_helper import FoodPropertyHelper
from cookbook.helper.shopping_helper import RecipeShoppingEditor
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter,
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink,
- Keyword, MealPlan, MealType, NutritionInformation, Recipe, RecipeBook,
- RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
- ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
- Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
- SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, Property,
- PropertyType, Property)
+ Keyword, MealPlan, MealType, NutritionInformation, Property,
+ PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport,
+ ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
+ Step, Storage, Supermarket, SupermarketCategory,
+ SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
+ UserFile, UserPreference, UserSpace, ViewLog)
from cookbook.templatetags.custom_tags import markdown
from recipes.settings import AWS_ENABLED, MEDIA_URL
@@ -57,10 +56,9 @@ def get_fields(self, *args, **kwargs):
api_serializer = None
# extended values are computationally expensive and not needed in normal circumstances
try:
- if str2bool(
- self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
+ if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
return fields
- except (AttributeError, KeyError) as e:
+ except (AttributeError, KeyError):
pass
try:
del fields['image']
@@ -104,9 +102,9 @@ def to_representation(self, value):
return round(value, 2).normalize()
def to_internal_value(self, data):
- if type(data) == int or type(data) == float:
+ if isinstance(data, int) or isinstance(data, float):
return data
- elif type(data) == str:
+ elif isinstance(data, str):
if data == '':
return 0
try:
@@ -146,11 +144,11 @@ class SpaceFilterSerializer(serializers.ListSerializer):
def to_representation(self, data):
if self.context.get('request', None) is None:
return
- if (type(data) == QuerySet and data.query.is_sliced):
+ if (isinstance(data, QuerySet) and data.query.is_sliced):
# if query is sliced it came from api request not nested serializer
return super().to_representation(data)
if self.child.Meta.model == User:
- if type(self.context['request'].user) == AnonymousUser:
+ if isinstance(self.context['request'].user, AnonymousUser):
data = []
else:
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
@@ -210,7 +208,7 @@ def get_download_link(self, obj):
def get_preview_link(self, obj):
try:
- img = Image.open(obj.file.file.file)
+ Image.open(obj.file.file.file)
return self.context['request'].build_absolute_uri(obj.file.url)
except Exception:
traceback.print_exc()
@@ -258,7 +256,7 @@ def get_download_link(self, obj):
def get_preview_link(self, obj):
try:
- img = Image.open(obj.file.file.file)
+ Image.open(obj.file.file.file)
return self.context['request'].build_absolute_uri(obj.file.url)
except Exception:
traceback.print_exc()
@@ -302,7 +300,7 @@ class Meta:
model = Space
fields = (
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
- 'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb',
+ 'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
'image', 'use_plural',)
read_only_fields = (
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
@@ -336,13 +334,16 @@ def create(self, validated_data):
class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
def create(self, validated_data):
+ validated_data['name'] = validated_data['name'].strip()
+ space = validated_data.pop('space', self.context['request'].space)
validated_data['created_by'] = self.context['request'].user
- return super().create(validated_data)
+ obj, created = MealType.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
+ return obj
class Meta:
list_serializer_class = SpaceFilterSerializer
model = MealType
- fields = ('id', 'name', 'order', 'icon', 'color', 'default', 'created_by')
+ fields = ('id', 'name', 'order', 'color', 'default', 'created_by')
read_only_fields = ('created_by',)
@@ -449,7 +450,7 @@ def create(self, validated_data):
class Meta:
model = Keyword
fields = (
- 'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
+ 'id', 'name', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
'updated_at', 'full_name')
read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
@@ -458,17 +459,17 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin)
recipe_filter = 'steps__ingredients__unit'
def create(self, validated_data):
- name = validated_data.pop('name').strip()
-
- if plural_name := validated_data.pop('plural_name', None):
- plural_name = plural_name.strip()
+ # get_or_create drops any field that contains '__' when creating so values must be included in validated data
+ space = validated_data.pop('space', self.context['request'].space)
+ if x := validated_data.get('name', None):
+ validated_data['name'] = x.strip()
+ if x := validated_data.get('name', None):
+ validated_data['plural_name'] = x.strip()
- if unit := Unit.objects.filter(Q(name=name) | Q(plural_name=name)).first():
+ if unit := Unit.objects.filter(Q(name__iexact=validated_data['name']) | Q(plural_name__iexact=validated_data['name']), space=space).first():
return unit
- space = validated_data.pop('space', self.context['request'].space)
- obj, created = Unit.objects.get_or_create(name=name, plural_name=plural_name, space=space,
- defaults=validated_data)
+ obj, created = Unit.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
return obj
def update(self, instance, validated_data):
@@ -486,9 +487,9 @@ class Meta:
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer, OpenDataModelMixin):
def create(self, validated_data):
- name = validated_data.pop('name').strip()
+ validated_data['name'] = validated_data['name'].strip()
space = validated_data.pop('space', self.context['request'].space)
- obj, created = SupermarketCategory.objects.get_or_create(name=name, space=space)
+ obj, created = SupermarketCategory.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
return obj
def update(self, instance, validated_data):
@@ -510,6 +511,12 @@ class Meta:
class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataModelMixin):
category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
+ def create(self, validated_data):
+ validated_data['name'] = validated_data['name'].strip()
+ space = validated_data.pop('space', self.context['request'].space)
+ obj, created = Supermarket.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
+ return obj
+
class Meta:
model = Supermarket
fields = ('id', 'name', 'description', 'category_to_supermarket', 'open_data_slug')
@@ -519,16 +526,14 @@ class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer,
id = serializers.IntegerField(required=False)
def create(self, validated_data):
- validated_data['space'] = self.context['request'].space
-
- if property_type := PropertyType.objects.filter(Q(name=validated_data['name'])).filter(space=self.context['request'].space).first():
- return property_type
-
- return super().create(validated_data)
+ validated_data['name'] = validated_data['name'].strip()
+ space = validated_data.pop('space', self.context['request'].space)
+ obj, created = PropertyType.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
+ return obj
class Meta:
model = PropertyType
- fields = ('id', 'name', 'icon', 'unit', 'description', 'order', 'open_data_slug')
+ fields = ('id', 'name', 'unit', 'description', 'order', 'open_data_slug', 'fdc_id',)
class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
@@ -572,7 +577,6 @@ class Meta:
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin, OpenDataModelMixin):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
- # shopping = serializers.SerializerMethodField('get_shopping_status')
shopping = serializers.ReadOnlyField(source='shopping_status')
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
child_inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
@@ -611,9 +615,6 @@ def get_substitute_onhand(self, obj):
filter |= Q(path__startswith=obj.path, depth__gt=obj.depth)
return Food.objects.filter(filter).filter(onhand_users__id__in=shared_users).exists()
- # def get_shopping_status(self, obj):
- # return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0
-
def create(self, validated_data):
name = validated_data['name'].strip()
@@ -636,7 +637,7 @@ def create(self, validated_data):
validated_data['recipe'] = Recipe.objects.get(**recipe)
# assuming if on hand for user also onhand for shopping_share users
- if not onhand is None:
+ if onhand is not None:
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
if self.instance:
onhand_users = self.instance.onhand_users.all()
@@ -669,7 +670,7 @@ def update(self, instance, validated_data):
# assuming if on hand for user also onhand for shopping_share users
onhand = validated_data.get('food_onhand', None)
reset_inherit = self.initial_data.get('reset_inherit', False)
- if not onhand is None:
+ if onhand is not None:
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
if onhand:
validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users
@@ -686,7 +687,7 @@ class Meta:
model = Food
fields = (
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url',
- 'properties', 'properties_food_amount', 'properties_food_unit',
+ 'properties', 'properties_food_amount', 'properties_food_unit', 'fdc_id',
'food_onhand', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug',
@@ -764,7 +765,7 @@ def get_step_recipes(self, obj):
def get_step_recipe_data(self, obj):
# check if root type is recipe to prevent infinite recursion
# can be improved later to allow multi level embedding
- if obj.step_recipe and type(self.parent.root) == RecipeSerializer:
+ if obj.step_recipe and isinstance(self.parent.root, RecipeSerializer):
return StepRecipeSerializer(obj.step_recipe, context={'request': self.context['request']}).data
class Meta:
@@ -801,9 +802,17 @@ def get_conversion_name(self, obj):
return text + f' = {round(obj.converted_amount)} {obj.converted_unit}'
def create(self, validated_data):
- validated_data['space'] = self.context['request'].space
- validated_data['created_by'] = self.context['request'].user
- return super().create(validated_data)
+ validated_data['space'] = validated_data.pop('space', self.context['request'].space)
+ try:
+ return UnitConversion.objects.get(
+ food__name__iexact=validated_data.get('food', {}).get('name', None),
+ base_unit__name__iexact=validated_data.get('base_unit', {}).get('name', None),
+ converted_unit__name__iexact=validated_data.get('converted_unit', {}).get('name', None),
+ space=validated_data['space']
+ )
+ except UnitConversion.DoesNotExist:
+ validated_data['created_by'] = self.context['request'].user
+ return super().create(validated_data)
class Meta:
model = UnitConversion
@@ -939,7 +948,7 @@ def create(self, validated_data):
class Meta:
model = RecipeBook
- fields = ('id', 'name', 'description', 'icon', 'shared', 'created_by', 'filter')
+ fields = ('id', 'name', 'description', 'shared', 'created_by', 'filter')
read_only_fields = ('created_by',)
@@ -956,8 +965,7 @@ def get_recipe_content(self, obj):
def create(self, validated_data):
book = validated_data['book']
recipe = validated_data['recipe']
- if not book.get_owner() == self.context['request'].user and not self.context[
- 'request'].user in book.get_shared():
+ if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
raise NotFound(detail=None, code=None)
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
return obj
@@ -995,7 +1003,7 @@ class Meta:
model = MealPlan
fields = (
'id', 'title', 'recipe', 'servings', 'note', 'note_markdown',
- 'date', 'meal_type', 'created_by', 'shared', 'recipe_name',
+ 'from_date', 'to_date', 'meal_type', 'created_by', 'shared', 'recipe_name',
'meal_type_name', 'shopping'
)
read_only_fields = ('created_by',)
@@ -1309,7 +1317,7 @@ class Meta:
class KeywordExportSerializer(KeywordSerializer):
class Meta:
model = Keyword
- fields = ('name', 'icon', 'description', 'created_at', 'updated_at')
+ fields = ('name', 'description', 'created_at', 'updated_at')
class NutritionInformationExportSerializer(NutritionInformationSerializer):
diff --git a/cookbook/signals.py b/cookbook/signals.py
index f50f0701f4..a93ffba1ea 100644
--- a/cookbook/signals.py
+++ b/cookbook/signals.py
@@ -1,4 +1,3 @@
-from decimal import Decimal
from functools import wraps
from django.conf import settings
@@ -13,12 +12,11 @@
from cookbook.helper.cache_helper import CacheHelper
from cookbook.helper.shopping_helper import RecipeShoppingEditor
from cookbook.managers import DICTIONARY
-from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe,
- ShoppingListEntry, Step, UserPreference, SearchPreference, SearchFields, Unit, PropertyType)
+from cookbook.models import (Food, MealPlan, PropertyType, Recipe, SearchFields, SearchPreference,
+ Step, Unit, UserPreference)
SQLITE = True
-if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
- 'django.db.backends.postgresql']:
+if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql':
SQLITE = False
diff --git a/cookbook/static/assets/recipe_no_image.svg b/cookbook/static/assets/recipe_no_image.svg
index f2650c453f..1128e2ca0f 100644
--- a/cookbook/static/assets/recipe_no_image.svg
+++ b/cookbook/static/assets/recipe_no_image.svg
@@ -51,13 +51,6 @@
inkscape:window-y="54"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
-
.nav-link, .navbar-dark .navbar-nav .nav-link.active, .navbar-dark .navbar-nav .nav-link.show, .navbar-dark .navbar-nav .show > .nav-link {
- color: #2e2e2e
+ color: #1E1E1E
}
.navbar-dark .navbar-toggler {
@@ -4636,13 +4637,15 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
.card-img-top {
width: 100%;
border-top-left-radius: calc(.25rem - 1px);
- border-top-right-radius: calc(.25rem - 1px)
+ border-top-right-radius: calc(.25rem - 1px);
+ background-color: #141414;
}
.card-img-bottom {
width: 100%;
border-bottom-right-radius: calc(.25rem - 1px);
- border-bottom-left-radius: calc(.25rem - 1px)
+ border-bottom-left-radius: calc(.25rem - 1px);
+ background-color: #141414;
}
.card-deck {
@@ -4776,7 +4779,7 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
padding: .75rem 1rem;
margin-bottom: 1rem;
list-style: none;
- background-color: #e9ecef;
+ background-color: #303030;
border-radius: .25rem
}
@@ -4846,16 +4849,16 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
.page-item.active .page-link {
z-index: 1;
color: #fff;
- background-color: #b98766;
+ background-color: var(--primary);
border-color: #b98766
}
.page-item.disabled .page-link {
- color: #6c757d;
+ color: #fff;
pointer-events: none;
cursor: auto;
- background-color: #fff;
- border-color: #dee2e6
+ background-color: #303030;
+ border-color: #303030
}
.pagination-lg .page-link {
@@ -5053,7 +5056,7 @@ a.badge-dark.focus, a.badge-dark:focus {
.jumbotron {
padding: 2rem 1rem;
margin-bottom: 2rem;
- background-color: #f7f7f7;
+ background-color: #3F3F3F;
border-radius: .3rem
}
@@ -5313,7 +5316,7 @@ a.badge-dark.focus, a.badge-dark:focus {
display: block;
padding: .75rem 1.25rem;
margin-bottom: -1px;
- background-color: #fff;
+ background-color: #3F3F3F;
border: 1px solid rgba(0, 0, 0, .125)
}
@@ -5611,13 +5614,13 @@ a.badge-dark.focus, a.badge-dark:focus {
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
- color: #000;
+ color: #fff;
text-shadow: 0 1px 0 #fff;
opacity: .5
}
.close:hover {
- color: #000;
+ color: #fff;
text-decoration: none
}
@@ -6164,7 +6167,8 @@ a.close.disabled {
.popover-body {
padding: .5rem .75rem;
- color: #212529
+ color: #fff;
+ background-color: #242424;
}
.carousel {
@@ -10408,11 +10412,11 @@ footer a:hover {
.form-control {
font-size: 14px;
- background-color: rgb(20 20 20);
+ background-color: rgb(20,20,20);
}
.form-control, .form-control:focus {
- background-color: #f6f7fb;
+ background-color: rgb(20,20,20);
border: none
}
@@ -10456,24 +10460,63 @@ footer a:hover {
background-color: transparent !important;
}
-textarea, input:not([type="submit"]):not([class="multiselect__input"]):not([class="select2-search__field"]):not([class="vue-treeselect__input"]), select {
+textarea, input:not([type="submit"]):not([class="multiselect__input"]):not([class="select2-search__field"]):not([class="vue-treeselect__input"]):not([class="custom-range"]), select {S
background-color: rgb(20 20 20);
color: #fff;
border-radius: .25rem !important;
border: 1px solid #ced4da !important;
}
-.multiselect__tag, .multiselect__option--highlight, .multiselect__option--highlight:after, .vue-treeselect__multi-value-item {
- background-color: #cfd5cd !important;
- color: #212529 !important;
+.b-calendar-grid-help,
+.v-note-op,
+.v-note-show,
+.v-show-content,
+.op-icon.dropdown-item,
+.popup-dropdown,
+.content-input-wrapper,
+.auto-textarea-input,
+.bottom-action-bar,
+.custom-select,
+.multiselect__tag,
+.multiselect__tags,
+.multiselect__input,
+.multiselect__option--highlight,
+.multiselect__option--highlight:after,
+.multiselect__option--selected,
+.multiselect__option--selected:after,
+.vue-treeselect__multi-value-item {
+ background-color: rgb(20, 20, 20) !important;
+ color: #fff !important;
+}
+
+.op-icon:hover, .op-icon.selected {
+ background-color: #303030 !important;
+ color: #fff !important;
}
.multiselect__tag-icon:hover, .multiselect__tag-icon:focus {
- background-color: #a7240e !important;
+ background-color: rgb(20, 20, 20) !important;
}
.multiselect__tag-icon:after, .vue-treeselect__icon vue-treeselect__value-remove, .vue-treeselect__value-remove {
- color: #212529 !important
+ color: rgb(20, 20, 20) !important
+}
+
+.multiselect__input::placeholder {
+ color: #fff !important;
+}
+
+.multiselect__single, .multiselect__content-wrapper {
+ background-color: rgb(20,20,20) !important;
+}
+
+.multiselect__content-wrapper {
+ color: #7A7A7A;
+}
+
+.custom-select {
+ /* add the arrow on the right */
+ background: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23ffffff' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center/8px 10px;
}
.form-control-search {
@@ -10483,4 +10526,39 @@ textarea, input:not([type="submit"]):not([class="multiselect__input"]):not([clas
.ghost {
opacity: 0.5 !important;
background: #b98766 !important;
-}
\ No newline at end of file
+}
+
+/* Meal-Plan */
+
+.cv-header {
+ background-color: #303030 !important;
+}
+
+.cv-weeknumber, .cv-header-day {
+ background-color: #303030 !important;
+ color: #fff !important;
+}
+
+.cv-day.past {
+ background-color: #333333 !important;
+}
+
+.cv-day.today {
+ background-color: var(--primary) !important;
+}
+
+.cv-day.outsideOfMonth {
+ background-color: #0d0d0d !important;
+}
+
+.cv-item {
+ background-color: #4E4E4E !important;
+}
+
+.b-sidebar-body {
+ background-color: #303030 !important;
+}
+
+.bottom-nav-link {
+ color: #898989;
+}
diff --git a/cookbook/tables.py b/cookbook/tables.py
index 4fa1a73143..6392f791e4 100644
--- a/cookbook/tables.py
+++ b/cookbook/tables.py
@@ -3,8 +3,7 @@
from django.utils.translation import gettext as _
from django_tables2.utils import A
-from .models import (CookLog, InviteLink, Recipe, RecipeImport,
- Storage, Sync, SyncLog, ViewLog)
+from .models import CookLog, InviteLink, RecipeImport, Storage, Sync, SyncLog, ViewLog
class StorageTable(tables.Table):
diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html
index 566027101e..2104923096 100644
--- a/cookbook/templates/base.html
+++ b/cookbook/templates/base.html
@@ -82,7 +82,7 @@
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
-
+
{% endif %}
{% endif %}
@@ -96,7 +96,7 @@
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
-
+
{% endif %}
{% endif %}
@@ -136,10 +136,10 @@