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 clairdans 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]
- 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 %} - Logo + Logo {% endif %} {% endif %} @@ -96,7 +96,7 @@ {% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %} - Logo + Logo {% endif %} {% endif %} @@ -136,10 +136,10 @@