diff --git a/.env.template b/.env.template index cb62dc522..a54e0fe0a 100644 --- a/.env.template +++ b/.env.template @@ -3,6 +3,9 @@ DEBUG=0 SQL_DEBUG=0 DEBUG_TOOLBAR=0 +# Gunicorn log level for debugging (default value is "info" when unset) +# (see https://docs.gunicorn.org/en/stable/settings.html#loglevel for available settings) +# GUNICORN_LOG_LEVEL="debug" # HTTP port to bind to # TANDOOR_PORT=8080 diff --git a/.github/workflows/build-docker-open-data.yml b/.github/workflows/build-docker-open-data.yml new file mode 100644 index 000000000..cdf0a023f --- /dev/null +++ b/.github/workflows/build-docker-open-data.yml @@ -0,0 +1,120 @@ +name: Build Docker Container with open data plugin installed + +on: push + +jobs: + build-container: + name: Build ${{ matrix.name }} Container + runs-on: ubuntu-latest + if: github.repository_owner == 'TandoorRecipes' + continue-on-error: ${{ matrix.continue-on-error }} + permissions: + contents: read + packages: write + strategy: + matrix: + include: + # Standard build config + - name: Standard + dockerfile: Dockerfile + platforms: linux/amd64,linux/arm64 + suffix: "" + continue-on-error: false + steps: + - uses: actions/checkout@v3 + + - name: Get version number + id: get_version + run: | + if [[ "$GITHUB_REF" = refs/tags/* ]]; then + echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + elif [[ "$GITHUB_REF" = refs/heads/beta ]]; then + echo VERSION=beta >> $GITHUB_OUTPUT + else + echo VERSION=develop >> $GITHUB_OUTPUT + fi + + # Update Version number + - name: Update version file + uses: DamianReeves/write-file-action@v1.2 + with: + path: recipes/version.py + contents: | + VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}-open-data' + BUILD_REF = '${{ github.sha }}' + write-mode: overwrite + + # clone open data plugin + - name: clone open data plugin repo + uses: actions/checkout@master + with: + repository: TandoorRecipes/open_data_plugin + ref: master + path: ./recipes/plugins/open_data_plugin + + # Build Vue frontend + - uses: actions/setup-node@v3 + with: + node-version: '14' + cache: yarn + cache-dependency-path: vue/yarn.lock + - name: Install dependencies + working-directory: ./vue + run: yarn install --frozen-lockfile + - name: Build dependencies + working-directory: ./vue + run: yarn build + + - name: Setup Open Data Plugin Links + working-directory: ./recipes/plugins/open_data_plugin + run: python setup_repo.py + + - name: Build Open Data Frontend + working-directory: ./recipes/plugins/open_data_plugin/vue + run: yarn build + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to Docker Hub + uses: docker/login-action@v2 + 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 + if: github.secret_source == 'Actions' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + vabene1111/recipes + ghcr.io/TandoorRecipes/recipes + flavor: | + latest=false + suffix=${{ matrix.suffix }} + tags: | + type=raw,value=latest,suffix=-open-data-plugin,enable=${{ startsWith(github.ref, 'refs/tags/') }} + type=semver,suffix=-open-data-plugin,pattern={{version}} + type=semver,suffix=-open-data-plugin,pattern={{major}}.{{minor}} + 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 + with: + context: . + file: ${{ matrix.dockerfile }} + pull: true + push: ${{ github.secret_source == 'Actions' }} + platforms: ${{ matrix.platforms }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/boot.sh b/boot.sh index b63217411..0ff1fba16 100644 --- a/boot.sh +++ b/boot.sh @@ -4,6 +4,7 @@ source venv/bin/activate TANDOOR_PORT="${TANDOOR_PORT:-8080}" GUNICORN_WORKERS="${GUNICORN_WORKERS:-3}" GUNICORN_THREADS="${GUNICORN_THREADS:-2}" +GUNICORN_LOG_LEVEL="${GUNICORN_LOG_LEVEL:-'info'}" NGINX_CONF_FILE=/opt/recipes/nginx/conf.d/Recipes.conf display_warning() { @@ -65,4 +66,4 @@ echo "Done" chmod -R 755 /opt/recipes/mediafiles -exec gunicorn -b :$TANDOOR_PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level INFO recipes.wsgi +exec gunicorn -b :$TANDOOR_PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi diff --git a/cookbook/admin.py b/cookbook/admin.py index aba876dfb..af8b54064 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -10,12 +10,13 @@ from treebeard.forms import movenodeform_factory from cookbook.managers import DICTIONARY -from .models import (BookmarkletImport, Comment, CookLog, Food, FoodInheritField, ImportLog, - Ingredient, InviteLink, Keyword, MealPlan, MealType, NutritionInformation, - Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink, - ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, - Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, - TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation, UserSpace) +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) class CustomUserAdmin(UserAdmin): @@ -150,9 +151,16 @@ class KeywordAdmin(TreeAdmin): admin.site.register(Keyword, KeywordAdmin) +@admin.action(description='Delete Steps not part of a Recipe.') +def delete_unattached_steps(modeladmin, request, queryset): + with scopes_disabled(): + Step.objects.filter(recipe=None).delete() + + class StepAdmin(admin.ModelAdmin): list_display = ('name', 'order',) search_fields = ('name',) + actions = [delete_unattached_steps] admin.site.register(Step, StepAdmin) @@ -201,9 +209,24 @@ class FoodAdmin(TreeAdmin): admin.site.register(Food, FoodAdmin) +class UnitConversionAdmin(admin.ModelAdmin): + list_display = ('base_amount', 'base_unit', 'food', 'converted_amount', 'converted_unit') + search_fields = ('food__name', 'unit__name') + + +admin.site.register(UnitConversion, UnitConversionAdmin) + + +@admin.action(description='Delete Ingredients not part of a Recipe.') +def delete_unattached_ingredients(modeladmin, request, queryset): + with scopes_disabled(): + Ingredient.objects.filter(step__recipe=None).delete() + + class IngredientAdmin(admin.ModelAdmin): list_display = ('food', 'amount', 'unit') search_fields = ('food__name', 'unit__name') + actions = [delete_unattached_ingredients] admin.site.register(Ingredient, IngredientAdmin) @@ -319,6 +342,20 @@ class ShareLinkAdmin(admin.ModelAdmin): admin.site.register(ShareLink, ShareLinkAdmin) +class PropertyTypeAdmin(admin.ModelAdmin): + list_display = ('id', 'name') + + +admin.site.register(PropertyType, PropertyTypeAdmin) + + +class PropertyAdmin(admin.ModelAdmin): + list_display = ('property_amount', 'property_type') + + +admin.site.register(Property, PropertyAdmin) + + class NutritionInformationAdmin(admin.ModelAdmin): list_display = ('id',) diff --git a/cookbook/forms.py b/cookbook/forms.py index 1c1a90c0e..3d5160a43 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -167,8 +167,25 @@ class ImportExportBase(forms.Form): )) +class MultipleFileInput(forms.ClearableFileInput): + allow_multiple_selected = True + + +class MultipleFileField(forms.FileField): + def __init__(self, *args, **kwargs): + kwargs.setdefault("widget", MultipleFileInput()) + super().__init__(*args, **kwargs) + + def clean(self, data, initial=None): + single_file_clean = super().clean + if isinstance(data, (list, tuple)): + result = [single_file_clean(d, initial) for d in data] + else: + result = single_file_clean(data, initial) + return result + class ImportForm(ImportExportBase): - files = forms.FileField(required=True, widget=forms.ClearableFileInput(attrs={'multiple': True})) + files = MultipleFileField(required=True) duplicates = forms.BooleanField(help_text=_( 'To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'), required=False) diff --git a/cookbook/helper/cache_helper.py b/cookbook/helper/cache_helper.py new file mode 100644 index 000000000..da903c53b --- /dev/null +++ b/cookbook/helper/cache_helper.py @@ -0,0 +1,11 @@ +class CacheHelper: + space = None + + BASE_UNITS_CACHE_KEY = None + PROPERTY_TYPE_CACHE_KEY = None + + def __init__(self, space): + self.space = space + + self.BASE_UNITS_CACHE_KEY = f'SPACE_{space.id}_BASE_UNITS' + self.PROPERTY_TYPE_CACHE_KEY = f'SPACE_{space.id}_PROPERTY_TYPES' diff --git a/cookbook/helper/image_processing.py b/cookbook/helper/image_processing.py index 552bf1312..9266141b5 100644 --- a/cookbook/helper/image_processing.py +++ b/cookbook/helper/image_processing.py @@ -40,7 +40,12 @@ def get_filetype(name): # TODO also add env variable to define which images sizes should be compressed # filetype argument can not be optional, otherwise this function will treat all images as if they were a jpeg # Because it's no longer optional, no reason to return it -def handle_image(request, image_object, filetype): +def handle_image(request, image_object, filetype): + try: + Image.open(image_object).verify() + except Exception: + return None + if (image_object.size / 1000) > 500: # if larger than 500 kb compress if filetype == '.jpeg' or filetype == '.jpg': return rescale_image_jpeg(image_object) diff --git a/cookbook/helper/open_data_importer.py b/cookbook/helper/open_data_importer.py new file mode 100644 index 000000000..2644f1f98 --- /dev/null +++ b/cookbook/helper/open_data_importer.py @@ -0,0 +1,214 @@ +from django.db.models import Q + +from cookbook.models import Unit, SupermarketCategory, Property, PropertyType, Supermarket, SupermarketCategoryRelation, Food, Automation, UnitConversion, FoodProperty + + +class OpenDataImporter: + request = None + data = {} + slug_id_cache = {} + update_existing = False + use_metric = True + + def __init__(self, request, data, update_existing=False, use_metric=True): + self.request = request + self.data = data + self.update_existing = update_existing + self.use_metric = use_metric + + def _update_slug_cache(self, object_class, datatype): + self.slug_id_cache[datatype] = dict(object_class.objects.filter(space=self.request.space, open_data_slug__isnull=False).values_list('open_data_slug', 'id', )) + + def import_units(self): + datatype = 'unit' + + insert_list = [] + for u in list(self.data[datatype].keys()): + insert_list.append(Unit( + name=self.data[datatype][u]['name'], + plural_name=self.data[datatype][u]['plural_name'], + base_unit=self.data[datatype][u]['base_unit'] if self.data[datatype][u]['base_unit'] != '' else None, + open_data_slug=u, + space=self.request.space + )) + + 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',)) + else: + return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('open_data_slug',), unique_fields=('space', 'name',)) + + def import_category(self): + datatype = 'category' + + insert_list = [] + for k in list(self.data[datatype].keys()): + insert_list.append(SupermarketCategory( + name=self.data[datatype][k]['name'], + open_data_slug=k, + space=self.request.space + )) + + return SupermarketCategory.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('open_data_slug',), unique_fields=('space', 'name',)) + + def import_property(self): + datatype = 'property' + + insert_list = [] + for k in list(self.data[datatype].keys()): + insert_list.append(PropertyType( + name=self.data[datatype][k]['name'], + unit=self.data[datatype][k]['unit'], + open_data_slug=k, + space=self.request.space + )) + + return PropertyType.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('open_data_slug',), unique_fields=('space', 'name',)) + + def import_supermarket(self): + datatype = 'store' + + self._update_slug_cache(SupermarketCategory, 'category') + insert_list = [] + for k in list(self.data[datatype].keys()): + insert_list.append(Supermarket( + name=self.data[datatype][k]['name'], + open_data_slug=k, + space=self.request.space + )) + + # always add open data slug if matching supermarket is found, otherwise relation might fail + supermarkets = Supermarket.objects.bulk_create(insert_list, unique_fields=('space', 'name',), update_conflicts=True, update_fields=('open_data_slug',)) + self._update_slug_cache(Supermarket, 'store') + + insert_list = [] + for k in list(self.data[datatype].keys()): + relations = [] + order = 0 + for c in self.data[datatype][k]['categories']: + relations.append( + SupermarketCategoryRelation( + supermarket_id=self.slug_id_cache[datatype][k], + category_id=self.slug_id_cache['category'][c], + order=order, + ) + ) + order += 1 + + SupermarketCategoryRelation.objects.bulk_create(relations, ignore_conflicts=True, unique_fields=('supermarket', 'category',)) + + return supermarkets + + def import_food(self): + identifier_list = [] + datatype = 'food' + for k in list(self.data[datatype].keys()): + identifier_list.append(self.data[datatype][k]['name']) + identifier_list.append(self.data[datatype][k]['plural_name']) + + existing_objects_flat = [] + existing_objects = {} + for f in Food.objects.filter(space=self.request.space).filter(name__in=identifier_list).values_list('id', 'name', 'plural_name'): + existing_objects_flat.append(f[1]) + existing_objects_flat.append(f[2]) + existing_objects[f[1]] = f + existing_objects[f[2]] = f + + 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 = [] + 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, + }}) + else: + if self.data[datatype][k]['name'] in existing_objects: + existing_food_id = existing_objects[self.data[datatype][k]['name']][0] + else: + existing_food_id = existing_objects[self.data[datatype][k]['plural_name']][0] + + if self.update_existing: + update_field_list = ['name', 'plural_name', 'preferred_unit_id', 'preferred_shopping_unit_id', 'supermarket_category_id', 'fdc_id', 'open_data_slug', ] + update_list.append(Food( + 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, + )) + else: + update_field_list = ['open_data_slug', ] + update_list.append(Food(id=existing_food_id, open_data_slug=k, )) + + Food.load_bulk(insert_list, None) + if len(update_list) > 0: + Food.objects.bulk_update(update_list, update_field_list) + + self._update_slug_cache(Food, 'food') + + food_property_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, + # )) + + Property.objects.bulk_create(food_property_list, ignore_conflicts=True, unique_fields=('space', 'import_food_id', 'property_type',)) + + property_food_relation_list = [] + for p in Property.objects.filter(space=self.request.space, import_food_id__isnull=False).values_list('import_food_id', 'id', ): + property_food_relation_list.append(Food.properties.through(food_id=p[0], property_id=p[1])) + + 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): + datatype = 'conversion' + + 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, + )) + + 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/property_helper.py b/cookbook/helper/property_helper.py new file mode 100644 index 000000000..7e09a13af --- /dev/null +++ b/cookbook/helper/property_helper.py @@ -0,0 +1,71 @@ +from django.core.cache import caches + +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 + + +class FoodPropertyHelper: + space = None + + def __init__(self, space): + """ + Helper to perform food property calculations + :param space: space to limit scope to + """ + self.space = space + + def calculate_recipe_properties(self, recipe): + """ + Calculate all food properties for a given recipe. + :param recipe: recipe to calculate properties for + :return: dict of with property keys and total/food values for each property available + """ + ingredients = [] + computed_properties = {} + + for s in recipe.steps.all(): + ingredients += s.ingredients.all() + + property_types = caches['default'].get(CacheHelper(self.space).PROPERTY_TYPE_CACHE_KEY, None) + + 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 + + for fpt in property_types: + computed_properties[fpt.id] = {'id': fpt.id, 'name': fpt.name, 'icon': fpt.icon, 'description': fpt.description, 'unit': fpt.unit, 'food_values': {}, 'total_value': 0, 'missing_value': False} + + uch = UnitConversionHelper(self.space) + + for i in ingredients: + if i.food is not None: + conversions = uch.get_conversions(i) + for pt in property_types: + found_property = False + if i.food.properties_food_amount == 0 or i.food.properties_food_unit is None: + computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': 0} + computed_properties[pt.id]['missing_value'] = i.food.properties_food_unit is None + else: + for p in i.food.properties.all(): + if p.property_type == pt: + for c in conversions: + 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) + 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} + + return computed_properties + + # small dict helper to add to existing key or create new, probably a better way of doing this + # TODO move to central helper ? + @staticmethod + def add_or_create(d, key, value, food): + if key in d: + d[key]['value'] += value + else: + d[key] = {'id': food.id, 'food': food.name, 'value': value} + return d diff --git a/cookbook/helper/recipe_url_import.py b/cookbook/helper/recipe_url_import.py index f9398294f..506d3d399 100644 --- a/cookbook/helper/recipe_url_import.py +++ b/cookbook/helper/recipe_url_import.py @@ -1,5 +1,6 @@ # import random import re +import traceback from html import unescape from django.core.cache import caches @@ -12,7 +13,8 @@ from recipe_scrapers._utils import get_host_name, get_minutes # from cookbook.helper import recipe_url_import as helper from cookbook.helper.ingredient_parser import IngredientParser -from cookbook.models import Automation, Keyword +from cookbook.models import Automation, Keyword, PropertyType + # from unicodedata import decomposition @@ -33,6 +35,9 @@ def get_from_scraper(scrape, request): except Exception: recipe_json['name'] = '' + if isinstance(recipe_json['name'], list) and len(recipe_json['name']) > 0: + recipe_json['name'] = recipe_json['name'][0] + try: description = scrape.description() or None except Exception: @@ -193,7 +198,14 @@ def get_from_scraper(scrape, request): except Exception: pass - if recipe_json['source_url']: + try: + recipe_json['properties'] = get_recipe_properties(request.space, scrape.schema.nutrients()) + print(recipe_json['properties']) + except Exception: + 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]): @@ -203,6 +215,30 @@ def get_from_scraper(scrape, request): return recipe_json +def get_recipe_properties(space, property_data): + # {'servingSize': '1', 'calories': '302 kcal', 'proteinContent': '7,66g', 'fatContent': '11,56g', 'carbohydrateContent': '41,33g'} + properties = { + "property-calories": "calories", + "property-carbohydrates": "carbohydrateContent", + "property-proteins": "proteinContent", + "property-fats": "fatContent", + } + recipe_properties = [] + for pt in PropertyType.objects.filter(space=space, open_data_slug__in=list(properties.keys())).all(): + for p in list(properties.keys()): + if pt.open_data_slug == p: + if properties[p] in property_data: + recipe_properties.append({ + 'property_type': { + 'id': pt.id, + 'name': pt.name, + }, + 'property_amount': parse_servings(property_data[properties[p]]) / float(property_data['servingSize']), + }) + + return recipe_properties + + def get_from_youtube_scraper(url, request): """A YouTube Information Scraper.""" kw, created = Keyword.objects.get_or_create(name='YouTube', space=request.space) diff --git a/cookbook/helper/unit_conversion_helper.py b/cookbook/helper/unit_conversion_helper.py new file mode 100644 index 000000000..4b9f6bf0b --- /dev/null +++ b/cookbook/helper/unit_conversion_helper.py @@ -0,0 +1,141 @@ +from django.core.cache import caches +from decimal import Decimal + +from cookbook.helper.cache_helper import CacheHelper +from cookbook.models import Ingredient, Unit + +CONVERSION_TABLE = { + 'weight': { + 'g': 1000, + 'kg': 1, + 'ounce': 35.274, + 'pound': 2.20462 + }, + 'volume': { + 'ml': 1000, + 'l': 1, + 'fluid_ounce': 33.814, + 'pint': 2.11338, + 'quart': 1.05669, + 'gallon': 0.264172, + 'tbsp': 67.628, + 'tsp': 202.884, + 'imperial_fluid_ounce': 35.1951, + 'imperial_pint': 1.75975, + 'imperial_quart': 0.879877, + 'imperial_gallon': 0.219969, + 'imperial_tbsp': 56.3121, + 'imperial_tsp': 168.936, + }, +} + +BASE_UNITS_WEIGHT = list(CONVERSION_TABLE['weight'].keys()) +BASE_UNITS_VOLUME = list(CONVERSION_TABLE['volume'].keys()) + + +class ConversionException(Exception): + pass + + +class UnitConversionHelper: + space = None + + def __init__(self, space): + """ + Initializes unit conversion helper + :param space: space to perform conversions on + """ + self.space = space + + @staticmethod + def convert_from_to(from_unit, to_unit, amount): + """ + Convert from one base unit to another. Throws ConversionException if trying to convert between different systems (weight/volume) or if units are not supported. + :param from_unit: str unit to convert from + :param to_unit: str unit to convert to + :param amount: amount to convert + :return: Decimal converted amount + """ + system = None + if from_unit in BASE_UNITS_WEIGHT and to_unit in BASE_UNITS_WEIGHT: + system = 'weight' + if from_unit in BASE_UNITS_VOLUME and to_unit in BASE_UNITS_VOLUME: + system = 'volume' + + if not system: + raise ConversionException('Trying to convert units not existing or not in one unit system (weight/volume)') + + return Decimal(amount / Decimal(CONVERSION_TABLE[system][from_unit] / CONVERSION_TABLE[system][to_unit])) + + def base_conversions(self, ingredient_list): + """ + Calculates all possible base unit conversions for each ingredient give. + Converts to all common base units IF they exist in the unit database of the space. + For useful results all ingredients passed should be of the same food, otherwise filtering afterwards might be required. + :param ingredient_list: list of ingredients to convert + :return: ingredient list with appended conversions + """ + base_conversion_ingredient_list = ingredient_list.copy() + for i in ingredient_list: + try: + conversion_unit = i.unit.name + if i.unit.base_unit: + conversion_unit = i.unit.base_unit + + # TODO allow setting which units to convert to? possibly only once conversions become visible + units = caches['default'].get(CacheHelper(self.space).BASE_UNITS_CACHE_KEY, None) + if not units: + units = Unit.objects.filter(space=self.space, base_unit__in=(BASE_UNITS_VOLUME + BASE_UNITS_WEIGHT)).all() + caches['default'].set(CacheHelper(self.space).BASE_UNITS_CACHE_KEY, units, 60 * 60) # cache is cleared on unit save signal so long duration is fine + + for u in units: + try: + ingredient = Ingredient(amount=self.convert_from_to(conversion_unit, u.base_unit, i.amount), unit=u, food=ingredient_list[0].food, ) + if not any((x.unit.name == ingredient.unit.name or x.unit.base_unit == ingredient.unit.name) for x in base_conversion_ingredient_list): + base_conversion_ingredient_list.append(ingredient) + except ConversionException: + pass + except Exception: + pass + + return base_conversion_ingredient_list + + def get_conversions(self, ingredient): + """ + Converts an ingredient to all possible conversions based on the custom unit conversion database. + After that passes conversion to UnitConversionHelper.base_conversions() to get all base conversions possible. + :param ingredient: Ingredient object + :return: list of ingredients with all possible custom and base conversions + """ + conversions = [ingredient] + if ingredient.unit: + for c in ingredient.unit.unit_conversion_base_relation.all(): + if c.space == self.space: + r = self._uc_convert(c, ingredient.amount, ingredient.unit, ingredient.food) + if r and r not in conversions: + conversions.append(r) + for c in ingredient.unit.unit_conversion_converted_relation.all(): + if c.space == self.space: + r = self._uc_convert(c, ingredient.amount, ingredient.unit, ingredient.food) + if r and r not in conversions: + conversions.append(r) + + conversions = self.base_conversions(conversions) + + return conversions + + def _uc_convert(self, uc, amount, unit, food): + """ + Helper to calculate values for custom unit conversions. + Converts given base values using the passed UnitConversion object into a converted Ingredient + :param uc: UnitConversion object + :param amount: base amount + :param unit: base unit + :param food: base food + :return: converted ingredient object from base amount/unit/food + """ + if uc.food is None or uc.food == food: + if unit == uc.base_unit: + return Ingredient(amount=amount * (uc.converted_amount / uc.base_amount), unit=uc.converted_unit, food=food, space=self.space) + else: + return Ingredient(amount=amount * (uc.base_amount / uc.converted_amount), unit=uc.base_unit, food=food, space=self.space) diff --git a/cookbook/integration/nextcloud_cookbook.py b/cookbook/integration/nextcloud_cookbook.py index baa8625ce..f2db21cc0 100644 --- a/cookbook/integration/nextcloud_cookbook.py +++ b/cookbook/integration/nextcloud_cookbook.py @@ -51,9 +51,15 @@ class NextcloudCookbook(Integration): ingredients_added = False for s in recipe_json['recipeInstructions']: - step = Step.objects.create( - instruction=s, space=self.request.space, - ) + instruction_text = '' + if 'text' in s: + step = Step.objects.create( + instruction=s['text'], name=s['name'], space=self.request.space, + ) + else: + step = Step.objects.create( + instruction=s, space=self.request.space, + ) if not ingredients_added: if len(recipe_json['description'].strip()) > 500: step.instruction = recipe_json['description'].strip() + '\n\n' + step.instruction @@ -98,11 +104,10 @@ class NextcloudCookbook(Integration): return recipe def formatTime(self, min): - h = min//60 + h = min // 60 m = min % 60 return f'PT{h}H{m}M0S' - def get_file_from_recipe(self, recipe): export = {} @@ -111,7 +116,7 @@ class NextcloudCookbook(Integration): export['url'] = recipe.source_url export['prepTime'] = self.formatTime(recipe.working_time) export['cookTime'] = self.formatTime(recipe.waiting_time) - export['totalTime'] = self.formatTime(recipe.working_time+recipe.waiting_time) + export['totalTime'] = self.formatTime(recipe.working_time + recipe.waiting_time) export['recipeYield'] = recipe.servings export['image'] = f'/Recipes/{recipe.name}/full.jpg' export['imageUrl'] = f'/Recipes/{recipe.name}/full.jpg' @@ -133,7 +138,6 @@ class NextcloudCookbook(Integration): export['recipeIngredient'] = recipeIngredient export['recipeInstructions'] = recipeInstructions - return "recipe.json", json.dumps(export) def get_files_from_recipes(self, recipes, el, cookie): @@ -163,7 +167,7 @@ class NextcloudCookbook(Integration): export_zip_obj.close() - return [[ self.get_export_file_name(), export_zip_stream.getvalue() ]] + return [[self.get_export_file_name(), export_zip_stream.getvalue()]] def getJPEG(self, imageByte): image = Image.open(BytesIO(imageByte)) @@ -172,14 +176,14 @@ class NextcloudCookbook(Integration): bytes = BytesIO() image.save(bytes, "JPEG") return bytes.getvalue() - + def getThumb(self, size, imageByte): image = Image.open(BytesIO(imageByte)) w, h = image.size - m = min(w, h) + m = min(w, h) - image = image.crop(((w-m)//2, (h-m)//2, (w+m)//2, (h+m)//2)) + image = image.crop(((w - m) // 2, (h - m) // 2, (w + m) // 2, (h + m) // 2)) image = image.resize([size, size], Image.Resampling.LANCZOS) image = image.convert('RGB') diff --git a/cookbook/integration/plantoeat.py b/cookbook/integration/plantoeat.py index 8eb4cb0dc..2affd3771 100644 --- a/cookbook/integration/plantoeat.py +++ b/cookbook/integration/plantoeat.py @@ -1,6 +1,7 @@ from io import BytesIO import requests +import validators from cookbook.helper.ingredient_parser import IngredientParser from cookbook.integration.integration import Integration @@ -67,8 +68,9 @@ class Plantoeat(Integration): if image_url: try: - response = requests.get(image_url) - self.import_recipe_image(recipe, BytesIO(response.content)) + if validators.url(image_url, public=True): + response = requests.get(image_url) + self.import_recipe_image(recipe, BytesIO(response.content)) except Exception as e: print('failed to import image ', str(e)) diff --git a/cookbook/integration/recipesage.py b/cookbook/integration/recipesage.py index 149c1c5dc..593ce0591 100644 --- a/cookbook/integration/recipesage.py +++ b/cookbook/integration/recipesage.py @@ -5,6 +5,7 @@ import requests import validators 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 @@ -18,19 +19,21 @@ class RecipeSage(Integration): created_by=self.request.user, internal=True, space=self.request.space) + if file['recipeYield'] != '': + recipe.servings = parse_servings(file['recipeYield']) + recipe.servings_text = parse_servings_text(file['recipeYield']) + try: - if file['recipeYield'] != '': - recipe.servings = int(file['recipeYield']) + if 'totalTime' in file and file['totalTime'] != '': + recipe.working_time = parse_time(file['totalTime']) - if file['totalTime'] != '': - recipe.waiting_time = int(file['totalTime']) - int(file['timePrep']) - - if file['prepTime'] != '': - recipe.working_time = int(file['timePrep']) - - recipe.save() + if 'timePrep' in file and file['prepTime'] != '': + recipe.working_time = parse_time(file['timePrep']) + recipe.waiting_time = parse_time(file['totalTime']) - parse_time(file['timePrep']) except Exception as e: - print('failed to parse yield or time ', str(e)) + print('failed to parse time ', str(e)) + + recipe.save() ingredient_parser = IngredientParser(self.request, True) ingredients_added = False diff --git a/cookbook/integration/rezeptsuitede.py b/cookbook/integration/rezeptsuitede.py index 75682e1f6..7f2f39628 100644 --- a/cookbook/integration/rezeptsuitede.py +++ b/cookbook/integration/rezeptsuitede.py @@ -22,9 +22,12 @@ class Rezeptsuitede(Integration): name=recipe_xml.find('head').attrib['title'].strip(), created_by=self.request.user, internal=True, space=self.request.space) - if recipe_xml.find('head').attrib['servingtype']: - recipe.servings = parse_servings(recipe_xml.find('head').attrib['servingtype'].strip()) - recipe.servings_text = parse_servings_text(recipe_xml.find('head').attrib['servingtype'].strip()) + try: + if recipe_xml.find('head').attrib['servingtype']: + recipe.servings = parse_servings(recipe_xml.find('head').attrib['servingtype'].strip()) + recipe.servings_text = parse_servings_text(recipe_xml.find('head').attrib['servingtype'].strip()) + except KeyError: + pass if recipe_xml.find('remark') is not None: # description is a list of
/remote.php/webdav/ s'afegeix automàticament)"
-#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157
+#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr "Emmagatzematge"
-#: .\cookbook\forms.py:267
+#: .\cookbook\forms.py:284
msgid "Active"
msgstr "Actiu"
-#: .\cookbook\forms.py:273
+#: .\cookbook\forms.py:290
msgid "Search String"
msgstr "Cerca Cadena"
-#: .\cookbook\forms.py:300
+#: .\cookbook\forms.py:317
msgid "File ID"
msgstr "ID d'Arxiu"
-#: .\cookbook\forms.py:322
+#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr "Has de proporcionar com a mínim una recepta o un títol."
-#: .\cookbook\forms.py:335
+#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"Podeu llistar els usuaris predeterminats amb els quals voleu compartir "
"receptes a la configuració."
-#: .\cookbook\forms.py:336
+#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the docs here"
@@ -247,15 +247,15 @@ msgstr ""
"Podeu utilitzar el marcador per donar format a aquest camp. Consulteu els documents aquí "
-#: .\cookbook\forms.py:362
+#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr "Nombre màxim d'usuaris assolit per a aquest espai."
-#: .\cookbook\forms.py:368
+#: .\cookbook\forms.py:385
msgid "Email address already taken!"
msgstr "Adreça de correu electrònic existent!"
-#: .\cookbook\forms.py:376
+#: .\cookbook\forms.py:393
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
@@ -263,15 +263,15 @@ msgstr ""
"No cal una adreça de correu electrònic, però si està present, s'enviarà "
"l'enllaç d'invitació a l'usuari."
-#: .\cookbook\forms.py:391
+#: .\cookbook\forms.py:408
msgid "Name already taken."
msgstr "Nom agafat."
-#: .\cookbook\forms.py:402
+#: .\cookbook\forms.py:419
msgid "Accept Terms and Privacy"
msgstr "Accepteu les condicions i la privadesa"
-#: .\cookbook\forms.py:434
+#: .\cookbook\forms.py:451
msgid ""
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
"g. low values mean more typos are ignored)."
@@ -280,7 +280,7 @@ msgstr ""
"de trigrama (p. ex., els valors baixos signifiquen que s'ignoren més errors "
"ortogràfics)."
-#: .\cookbook\forms.py:444
+#: .\cookbook\forms.py:461
msgid ""
"Select type method of search. Click here for "
"full description of choices."
@@ -288,7 +288,7 @@ msgstr ""
"Seleccioneu el tipus de mètode de cerca. Feu clic aquí per obtenir una descripció completa de les opcions."
-#: .\cookbook\forms.py:445
+#: .\cookbook\forms.py:462
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
@@ -296,7 +296,7 @@ msgstr ""
"Utilitzeu la concordança difusa en unitats, paraules clau i ingredients quan "
"editeu i importeu receptes."
-#: .\cookbook\forms.py:447
+#: .\cookbook\forms.py:464
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
@@ -304,7 +304,7 @@ msgstr ""
"Camps per cercar ignorant els accents. La selecció d'aquesta opció pot "
"millorar o degradar la qualitat de la cerca en funció de l'idioma"
-#: .\cookbook\forms.py:449
+#: .\cookbook\forms.py:466
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
@@ -312,7 +312,7 @@ msgstr ""
"Camps per cercar coincidències parcials. (p. ex., en cercar \"Pastís\" "
"tornarà \"pastís\" i \"peça\" i \"sabó\")"
-#: .\cookbook\forms.py:451
+#: .\cookbook\forms.py:468
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
@@ -320,7 +320,7 @@ msgstr ""
"Camps per cercar l'inici de les coincidències de paraula. (p. ex., en cercar "
"\"sa\" es tornarà \"amanida\" i \"entrepà\")"
-#: .\cookbook\forms.py:453
+#: .\cookbook\forms.py:470
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."
@@ -329,7 +329,7 @@ msgstr ""
"trobareu \"recepta\".) Nota: aquesta opció entrarà en conflicte amb els "
"mètodes de cerca \"web\" i \"cru\"."
-#: .\cookbook\forms.py:455
+#: .\cookbook\forms.py:472
msgid ""
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
"only function with fulltext fields."
@@ -337,35 +337,35 @@ msgstr ""
"Camps per a la cerca de text complet. Nota: els mètodes de cerca \"web\", "
"\"frase\" i \"en brut\" només funcionen amb camps de text complet."
-#: .\cookbook\forms.py:459
+#: .\cookbook\forms.py:476
msgid "Search Method"
msgstr "Mètode de cerca"
-#: .\cookbook\forms.py:460
+#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
msgstr "Cerques difuses"
-#: .\cookbook\forms.py:461
+#: .\cookbook\forms.py:478
msgid "Ignore Accent"
msgstr "Ignora Accents"
-#: .\cookbook\forms.py:462
+#: .\cookbook\forms.py:479
msgid "Partial Match"
msgstr "Cerca Parcial"
-#: .\cookbook\forms.py:463
+#: .\cookbook\forms.py:480
msgid "Starts With"
msgstr "Comença amb"
-#: .\cookbook\forms.py:464
+#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
msgstr "Cerca Difusa"
-#: .\cookbook\forms.py:465
+#: .\cookbook\forms.py:482
msgid "Full Text"
msgstr "Text Sencer"
-#: .\cookbook\forms.py:490
+#: .\cookbook\forms.py:507
msgid ""
"Users will see all items you add to your shopping list. They must add you "
"to see items on their list."
@@ -373,7 +373,7 @@ msgstr ""
"Els usuaris veuran tots els articles que afegiu a la vostra llista de la "
"compra. Us han d'afegir per veure els elements de la seva llista."
-#: .\cookbook\forms.py:496
+#: .\cookbook\forms.py:513
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
@@ -381,7 +381,7 @@ msgstr ""
"Quan afegiu un pla d'àpats a la llista de la compra (de manera manual o "
"automàtica), inclou totes les receptes relacionades."
-#: .\cookbook\forms.py:497
+#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
@@ -389,93 +389,93 @@ msgstr ""
"Quan afegiu un pla d'àpats a la llista de la compra (manual o "
"automàticament), excloeu els ingredients que teniu a mà."
-#: .\cookbook\forms.py:498
+#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
msgstr ""
"Nombre d'hores per defecte per retardar l'entrada d'una llista de la compra."
-#: .\cookbook\forms.py:499
+#: .\cookbook\forms.py:516
msgid "Filter shopping list to only include supermarket categories."
msgstr ""
"Filtreu la llista de compres per incloure només categories de supermercats."
-#: .\cookbook\forms.py:500
+#: .\cookbook\forms.py:517
msgid "Days of recent shopping list entries to display."
msgstr "Dies de les entrades recents de la llista de la compra per mostrar."
-#: .\cookbook\forms.py:501
+#: .\cookbook\forms.py:518
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr "Marca el menjar com a \"A mà\" quan marqueu la llista de la compra."
-#: .\cookbook\forms.py:502
+#: .\cookbook\forms.py:519
msgid "Delimiter to use for CSV exports."
msgstr "Delimitador per a les exportacions CSV."
-#: .\cookbook\forms.py:503
+#: .\cookbook\forms.py:520
msgid "Prefix to add when copying list to the clipboard."
msgstr "Prefix per afegir en copiar la llista al porta-retalls."
-#: .\cookbook\forms.py:507
+#: .\cookbook\forms.py:524
msgid "Share Shopping List"
msgstr "Compartir Llista de la Compra"
-#: .\cookbook\forms.py:508
+#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr "Autosync"
-#: .\cookbook\forms.py:509
+#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr "Afegeix automàticament un pla d'àpats"
-#: .\cookbook\forms.py:510
+#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
msgstr "Exclou a mà"
-#: .\cookbook\forms.py:511
+#: .\cookbook\forms.py:528
msgid "Include Related"
msgstr "Incloure Relacionats"
-#: .\cookbook\forms.py:512
+#: .\cookbook\forms.py:529
msgid "Default Delay Hours"
msgstr "Hores de retard per defecte"
-#: .\cookbook\forms.py:513
+#: .\cookbook\forms.py:530
msgid "Filter to Supermarket"
msgstr "Filtrar a supermercat"
-#: .\cookbook\forms.py:514
+#: .\cookbook\forms.py:531
msgid "Recent Days"
msgstr "Dies recents"
-#: .\cookbook\forms.py:515
+#: .\cookbook\forms.py:532
msgid "CSV Delimiter"
msgstr "Delimitador CSV"
-#: .\cookbook\forms.py:516
+#: .\cookbook\forms.py:533
msgid "List Prefix"
msgstr "Prefix de Llista"
-#: .\cookbook\forms.py:517
+#: .\cookbook\forms.py:534
msgid "Auto On Hand"
msgstr "Auto a mà"
-#: .\cookbook\forms.py:527
+#: .\cookbook\forms.py:544
msgid "Reset Food Inheritance"
msgstr "Restablir Herència Alimentària"
-#: .\cookbook\forms.py:528
+#: .\cookbook\forms.py:545
msgid "Reset all food to inherit the fields configured."
msgstr "Restableix tots els aliments per heretar els camps configurats."
-#: .\cookbook\forms.py:540
+#: .\cookbook\forms.py:557
msgid "Fields on food that should be inherited by default."
msgstr "Camps dels aliments que s'han d'heretar per defecte."
-#: .\cookbook\forms.py:541
+#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
msgstr "Mostra el recompte de receptes als filtres de cerca"
-#: .\cookbook\forms.py:542
+#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""
diff --git a/cookbook/locale/de/LC_MESSAGES/django.mo b/cookbook/locale/de/LC_MESSAGES/django.mo
index da2c5568a..032490044 100644
Binary files a/cookbook/locale/de/LC_MESSAGES/django.mo and b/cookbook/locale/de/LC_MESSAGES/django.mo differ
diff --git a/cookbook/locale/de/LC_MESSAGES/django.po b/cookbook/locale/de/LC_MESSAGES/django.po
index b2361c937..232e91604 100644
--- a/cookbook/locale/de/LC_MESSAGES/django.po
+++ b/cookbook/locale/de/LC_MESSAGES/django.po
@@ -14,11 +14,11 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-26 07:46+0200\n"
-"PO-Revision-Date: 2023-02-09 13:55+0000\n"
-"Last-Translator: Marion Kämpfer /remote."
"php/webdav/ is added automatically)"
@@ -214,33 +211,33 @@ msgstr ""
"Für Dropbox leer lassen, für Nextcloud Server-URL angeben (/remote.php/"
"webdav/ wird automatisch hinzugefügt)"
-#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157 .\cookbook\forms.py:264
+#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr "Speicher"
-#: .\cookbook\forms.py:267 .\cookbook\forms.py:266
+#: .\cookbook\forms.py:284
msgid "Active"
msgstr "Aktiv"
-#: .\cookbook\forms.py:273 .\cookbook\forms.py:272
+#: .\cookbook\forms.py:290
msgid "Search String"
msgstr "Suchwort"
-#: .\cookbook\forms.py:300 .\cookbook\forms.py:299
+#: .\cookbook\forms.py:317
msgid "File ID"
msgstr "Datei-ID"
-#: .\cookbook\forms.py:322 .\cookbook\forms.py:321
+#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr "Mindestens ein Rezept oder ein Titel müssen angegeben werden."
-#: .\cookbook\forms.py:335 .\cookbook\forms.py:334
+#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"Sie können in den Einstellungen Standardbenutzer auflisten, für die Sie "
"Rezepte freigeben möchten."
-#: .\cookbook\forms.py:336 .\cookbook\forms.py:335
+#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the docs here"
@@ -248,15 +245,15 @@ msgstr ""
"Markdown kann genutzt werden, um dieses Feld zu formatieren. Siehe hier für weitere Information"
-#: .\cookbook\forms.py:362 .\cookbook\forms.py:361
+#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr "Maximale Nutzer-Anzahl wurde für diesen Space erreicht."
-#: .\cookbook\forms.py:368 .\cookbook\forms.py:367
+#: .\cookbook\forms.py:385
msgid "Email address already taken!"
msgstr "Email-Adresse ist bereits vergeben!"
-#: .\cookbook\forms.py:376 .\cookbook\forms.py:375
+#: .\cookbook\forms.py:393
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
@@ -264,15 +261,15 @@ msgstr ""
"Eine Email-Adresse wird nicht benötigt, aber falls vorhanden, wird der "
"Einladungslink zum Benutzer geschickt."
-#: .\cookbook\forms.py:391 .\cookbook\forms.py:390
+#: .\cookbook\forms.py:408
msgid "Name already taken."
msgstr "Name wird bereits verwendet."
-#: .\cookbook\forms.py:402 .\cookbook\forms.py:401
+#: .\cookbook\forms.py:419
msgid "Accept Terms and Privacy"
msgstr "AGB und Datenschutzerklärung akzeptieren"
-#: .\cookbook\forms.py:434 .\cookbook\forms.py:433
+#: .\cookbook\forms.py:451
msgid ""
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
"g. low values mean more typos are ignored)."
@@ -280,7 +277,7 @@ msgstr ""
"Legt fest wie unscharf eine Suche ist, falls Trigramme verwendet werden (i."
"A. führen niedrigere Werte zum ignorieren von mehr Tippfehlern)."
-#: .\cookbook\forms.py:444 .\cookbook\forms.py:443
+#: .\cookbook\forms.py:461
msgid ""
"Select type method of search. Click here for "
"full description of choices."
@@ -288,7 +285,7 @@ msgstr ""
"Suchmethode auswählen. Klicke hier für eine "
"vollständige Erklärung der Optionen."
-#: .\cookbook\forms.py:445 .\cookbook\forms.py:444
+#: .\cookbook\forms.py:462
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
@@ -296,7 +293,7 @@ msgstr ""
"Benutze die unscharfe Suche für Einheiten, Schlüsselwörter und Zutaten beim "
"ändern und importieren von Rezepten."
-#: .\cookbook\forms.py:447 .\cookbook\forms.py:446
+#: .\cookbook\forms.py:464
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
@@ -304,7 +301,7 @@ msgstr ""
"Felder bei welchen Akzente ignoriert werden. Das aktivieren dieser Option "
"kann die Suchqualität je nach Sprache verbessern oder verschlechtern"
-#: .\cookbook\forms.py:449 .\cookbook\forms.py:448
+#: .\cookbook\forms.py:466
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
@@ -312,7 +309,7 @@ msgstr ""
"Felder welche auf partielle Treffer durchsucht werden. (z.B. eine Suche "
"nach 'Spa' wird 'Spaghetti', 'Spargel' und 'Grünspargel' liefern.)"
-#: .\cookbook\forms.py:451 .\cookbook\forms.py:450
+#: .\cookbook\forms.py:468
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
@@ -320,7 +317,7 @@ msgstr ""
"Felder welche auf übereinstimmenden Wortbeginn durchsucht werden. (z.B. eine "
"Suche nach \"Spa\" wird \"Spaghetti\" und \"Spargel\" liefern.)"
-#: .\cookbook\forms.py:453 .\cookbook\forms.py:452
+#: .\cookbook\forms.py:470
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."
@@ -329,7 +326,7 @@ msgstr ""
"\"Kuhcen\" wird \"Kuchen\" liefern.) Tipp: Diese Option konfligiert mit den "
"\"web\" und \"raw\" Suchtypen."
-#: .\cookbook\forms.py:455 .\cookbook\forms.py:454
+#: .\cookbook\forms.py:472
msgid ""
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
"only function with fulltext fields."
@@ -337,35 +334,35 @@ msgstr ""
"Felder welche im Volltext durchsucht werden sollen. Tipp: Die Suchtypen \"web"
"\", \"raw\" und \"phrase\" funktionieren nur mit Volltext-Feldern."
-#: .\cookbook\forms.py:459 .\cookbook\forms.py:458
+#: .\cookbook\forms.py:476
msgid "Search Method"
msgstr "Suchmethode"
-#: .\cookbook\forms.py:460 .\cookbook\forms.py:459
+#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
msgstr "Unscharfe Suche"
-#: .\cookbook\forms.py:461 .\cookbook\forms.py:460
+#: .\cookbook\forms.py:478
msgid "Ignore Accent"
msgstr "Akzente ignorieren"
-#: .\cookbook\forms.py:462 .\cookbook\forms.py:461
+#: .\cookbook\forms.py:479
msgid "Partial Match"
msgstr "Teilweise Übereinstimmung"
-#: .\cookbook\forms.py:463 .\cookbook\forms.py:462
+#: .\cookbook\forms.py:480
msgid "Starts With"
msgstr "Beginnt mit"
-#: .\cookbook\forms.py:464 .\cookbook\forms.py:463
+#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
msgstr "Unpräzise Suche"
-#: .\cookbook\forms.py:465 .\cookbook\forms.py:464
+#: .\cookbook\forms.py:482
msgid "Full Text"
msgstr "Volltext"
-#: .\cookbook\forms.py:490 .\cookbook\forms.py:489
+#: .\cookbook\forms.py:507
msgid ""
"Users will see all items you add to your shopping list. They must add you "
"to see items on their list."
@@ -374,7 +371,7 @@ msgstr ""
"Benutzer müssen Sie hinzufügen, damit Sie Artikel auf der Liste der Benutzer "
"sehen können."
-#: .\cookbook\forms.py:496 .\cookbook\forms.py:495
+#: .\cookbook\forms.py:513
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
@@ -382,7 +379,7 @@ msgstr ""
"Wenn Sie einen Essensplan zur Einkaufsliste hinzufügen (manuell oder "
"automatisch), fügen Sie alle zugehörigen Rezepte hinzu."
-#: .\cookbook\forms.py:497 .\cookbook\forms.py:496
+#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
@@ -390,98 +387,98 @@ msgstr ""
"Wenn Sie einen Essensplan zur Einkaufsliste hinzufügen (manuell oder "
"automatisch), schließen Sie Zutaten aus, die Sie gerade zur Hand haben."
-#: .\cookbook\forms.py:498 .\cookbook\forms.py:497
+#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
msgstr ""
"Voreingestellte Anzahl von Stunden für die Verzögerung eines "
"Einkaufslisteneintrags."
-#: .\cookbook\forms.py:499 .\cookbook\forms.py:498
+#: .\cookbook\forms.py:516
msgid "Filter shopping list to only include supermarket categories."
msgstr ""
"Nur für den Supermarkt konfigurierte Kategorien in Einkaufsliste anzeigen."
-#: .\cookbook\forms.py:500 .\cookbook\forms.py:499
+#: .\cookbook\forms.py:517
msgid "Days of recent shopping list entries to display."
msgstr ""
"Tage der letzten Einträge in der Einkaufsliste, die angezeigt werden sollen."
-#: .\cookbook\forms.py:501 .\cookbook\forms.py:500
+#: .\cookbook\forms.py:518
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr ""
"Lebensmittel als vorrätig markieren, wenn es in der Einkaufliste abgehakt "
"wurde."
-#: .\cookbook\forms.py:502 .\cookbook\forms.py:501
+#: .\cookbook\forms.py:519
msgid "Delimiter to use for CSV exports."
msgstr "Separator für CSV-Export."
-#: .\cookbook\forms.py:503 .\cookbook\forms.py:502
+#: .\cookbook\forms.py:520
msgid "Prefix to add when copying list to the clipboard."
msgstr "Zusatz wird der in die Zwischenablage kopierten Liste vorangestellt."
-#: .\cookbook\forms.py:507 .\cookbook\forms.py:506
+#: .\cookbook\forms.py:524
msgid "Share Shopping List"
msgstr "Einkaufsliste teilen"
-#: .\cookbook\forms.py:508 .\cookbook\forms.py:507
+#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr "Automatischer Abgleich"
-#: .\cookbook\forms.py:509 .\cookbook\forms.py:508
+#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr "automatisch dem Menüplan hinzufügen"
-#: .\cookbook\forms.py:510 .\cookbook\forms.py:509
+#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
msgstr "Ausgenommen Vorrätiges"
-#: .\cookbook\forms.py:511 .\cookbook\forms.py:510
+#: .\cookbook\forms.py:528
msgid "Include Related"
msgstr "dazugehörend"
-#: .\cookbook\forms.py:512 .\cookbook\forms.py:511
+#: .\cookbook\forms.py:529
msgid "Default Delay Hours"
msgstr "Standardmäßige Verzögerung in Stunden"
-#: .\cookbook\forms.py:513 .\cookbook\forms.py:512
+#: .\cookbook\forms.py:530
msgid "Filter to Supermarket"
msgstr "Supermarkt filtern"
-#: .\cookbook\forms.py:514 .\cookbook\forms.py:513
+#: .\cookbook\forms.py:531
msgid "Recent Days"
msgstr "Vergangene Tage"
-#: .\cookbook\forms.py:515 .\cookbook\forms.py:514
+#: .\cookbook\forms.py:532
msgid "CSV Delimiter"
msgstr "CSV Trennzeichen"
-#: .\cookbook\forms.py:516 .\cookbook\forms.py:515
+#: .\cookbook\forms.py:533
msgid "List Prefix"
msgstr "Listenpräfix"
-#: .\cookbook\forms.py:517 .\cookbook\forms.py:516
+#: .\cookbook\forms.py:534
msgid "Auto On Hand"
msgstr "Automatisch als vorrätig markieren"
-#: .\cookbook\forms.py:527 .\cookbook\forms.py:526
+#: .\cookbook\forms.py:544
msgid "Reset Food Inheritance"
msgstr "Lebensmittelvererbung zurücksetzen"
-#: .\cookbook\forms.py:528 .\cookbook\forms.py:527
+#: .\cookbook\forms.py:545
msgid "Reset all food to inherit the fields configured."
msgstr ""
"Alle Lebensmittel zurücksetzen, um die konfigurierten Felder zu übernehmen."
-#: .\cookbook\forms.py:540 .\cookbook\forms.py:539
+#: .\cookbook\forms.py:557
msgid "Fields on food that should be inherited by default."
msgstr "Zutaten, die standardmäßig übernommen werden sollen."
-#: .\cookbook\forms.py:541 .\cookbook\forms.py:540
+#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
msgstr "Rezeptanzahl im Suchfiltern anzeigen"
-#: .\cookbook\forms.py:542 .\cookbook\forms.py:541
+#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr "Pluralform für Einheiten und Essen in diesem Space verwenden."
@@ -495,7 +492,6 @@ msgstr ""
#: .\cookbook\helper\permission_helper.py:164
#: .\cookbook\helper\permission_helper.py:187 .\cookbook\views\views.py:117
-#: .\cookbook\views\views.py:114
msgid "You are not logged in and therefore cannot view this page!"
msgstr "Du bist nicht angemeldet, daher kannst du diese Seite nicht sehen!"
@@ -509,7 +505,6 @@ msgstr "Du bist nicht angemeldet, daher kannst du diese Seite nicht sehen!"
#: .\cookbook\helper\permission_helper.py:321
#: .\cookbook\helper\permission_helper.py:342 .\cookbook\views\data.py:36
#: .\cookbook\views\views.py:128 .\cookbook\views\views.py:135
-#: .\cookbook\views\views.py:125 .\cookbook\views\views.py:132
msgid "You do not have the required permissions to view this page!"
msgstr "Du hast nicht die notwendigen Rechte um diese Seite zu sehen!"
@@ -530,47 +525,38 @@ msgid "You have more users than allowed in your space."
msgstr "Du hast mehr Benutzer in Deinem Space als erlaubt."
#: .\cookbook\helper\recipe_search.py:630
-#: .\cookbook\helper\recipe_search.py:570
msgid "One of queryset or hash_key must be provided"
msgstr "Es muss die Abfrage oder der Hash_Key angeben werden"
#: .\cookbook\helper\recipe_url_import.py:266
-#: .\cookbook\helper\recipe_url_import.py:265
msgid "reverse rotation"
msgstr "Linkslauf"
#: .\cookbook\helper\recipe_url_import.py:267
-#: .\cookbook\helper\recipe_url_import.py:266
msgid "careful rotation"
msgstr "Kochlöffel"
#: .\cookbook\helper\recipe_url_import.py:268
-#: .\cookbook\helper\recipe_url_import.py:267
msgid "knead"
msgstr "Kneten"
#: .\cookbook\helper\recipe_url_import.py:269
-#: .\cookbook\helper\recipe_url_import.py:268
msgid "thicken"
msgstr "Andicken"
#: .\cookbook\helper\recipe_url_import.py:270
-#: .\cookbook\helper\recipe_url_import.py:269
msgid "warm up"
msgstr "Erwärmen"
#: .\cookbook\helper\recipe_url_import.py:271
-#: .\cookbook\helper\recipe_url_import.py:270
msgid "ferment"
msgstr "Fermentieren"
#: .\cookbook\helper\recipe_url_import.py:272
-#: .\cookbook\helper\recipe_url_import.py:271
msgid "sous-vide"
msgstr "Sous-vide"
#: .\cookbook\helper\shopping_helper.py:157
-#: .\cookbook\helper\shopping_helper.py:152
msgid "You must supply a servings size"
msgstr "Sie müssen eine Portionsgröße angeben"
@@ -589,7 +575,6 @@ msgid "I made this"
msgstr "Von mir gekocht"
#: .\cookbook\integration\integration.py:218
-#: .\cookbook\integration\integration.py:223
msgid ""
"Importer expected a .zip file. Did you choose the correct importer type for "
"your data ?"
@@ -598,7 +583,6 @@ msgstr ""
"deine Daten ausgewählt?"
#: .\cookbook\integration\integration.py:221
-#: .\cookbook\integration\integration.py:226
msgid ""
"An unexpected error occurred during the import. Please make sure you have "
"uploaded a valid file."
@@ -607,30 +591,27 @@ msgstr ""
"die hochgeladene Datei gültig ist."
#: .\cookbook\integration\integration.py:226
-#: .\cookbook\integration\integration.py:231
msgid "The following recipes were ignored because they already existed:"
msgstr "Die folgenden Rezepte wurden ignoriert da sie bereits existieren:"
#: .\cookbook\integration\integration.py:230
-#: .\cookbook\integration\integration.py:235
#, python-format
msgid "Imported %s recipes."
msgstr "%s Rezepte importiert."
#: .\cookbook\integration\openeats.py:26
-#, fuzzy
msgid "Recipe source:"
-msgstr "Rezept-Hauptseite"
+msgstr "Rezept-Quelle:"
-#: .\cookbook\integration\paprika.py:49 .\cookbook\integration\paprika.py:46
+#: .\cookbook\integration\paprika.py:49
msgid "Notes"
msgstr "Notizen"
-#: .\cookbook\integration\paprika.py:52 .\cookbook\integration\paprika.py:49
+#: .\cookbook\integration\paprika.py:52
msgid "Nutritional Information"
msgstr "Nährwert Informationen"
-#: .\cookbook\integration\paprika.py:56 .\cookbook\integration\paprika.py:53
+#: .\cookbook\integration\paprika.py:56
msgid "Source"
msgstr "Quelle"
@@ -703,78 +684,70 @@ msgstr ""
#: .\cookbook\models.py:365 .\cookbook\templates\search.html:7
#: .\cookbook\templates\settings.html:18
-#: .\cookbook\templates\space_manage.html:7 .\cookbook\models.py:364
+#: .\cookbook\templates\space_manage.html:7
msgid "Search"
msgstr "Suchen"
#: .\cookbook\models.py:366 .\cookbook\templates\base.html:110
#: .\cookbook\templates\meal_plan.html:7 .\cookbook\views\delete.py:178
#: .\cookbook\views\edit.py:211 .\cookbook\views\new.py:179
-#: .\cookbook\models.py:365
msgid "Meal-Plan"
msgstr "Essensplan"
#: .\cookbook\models.py:367 .\cookbook\templates\base.html:118
-#: .\cookbook\models.py:366
msgid "Books"
msgstr "Bücher"
-#: .\cookbook\models.py:580 .\cookbook\models.py:579
+#: .\cookbook\models.py:580
msgid " is part of a recipe step and cannot be deleted"
msgstr " ist Teil eines Rezepts und kann nicht gelöscht werden"
#: .\cookbook\models.py:1181 .\cookbook\templates\search_info.html:28
-#: .\cookbook\models.py:1180
msgid "Simple"
msgstr "Einfach"
#: .\cookbook\models.py:1182 .\cookbook\templates\search_info.html:33
-#: .\cookbook\models.py:1181
msgid "Phrase"
msgstr "Satz"
#: .\cookbook\models.py:1183 .\cookbook\templates\search_info.html:38
-#: .\cookbook\models.py:1182
msgid "Web"
msgstr "Web"
#: .\cookbook\models.py:1184 .\cookbook\templates\search_info.html:47
-#: .\cookbook\models.py:1183
msgid "Raw"
msgstr "Rohdaten"
-#: .\cookbook\models.py:1231 .\cookbook\models.py:1230
+#: .\cookbook\models.py:1231
msgid "Food Alias"
msgstr "Lebensmittel Alias"
-#: .\cookbook\models.py:1231 .\cookbook\models.py:1230
+#: .\cookbook\models.py:1231
msgid "Unit Alias"
msgstr "Einheiten Alias"
-#: .\cookbook\models.py:1231 .\cookbook\models.py:1230
+#: .\cookbook\models.py:1231
msgid "Keyword Alias"
msgstr "Stichwort Alias"
-#: .\cookbook\models.py:1231
+#: .\cookbook\models.py:1232
msgid "Description Replace"
msgstr "Beschreibung ersetzen"
-#: .\cookbook\models.py:1231
+#: .\cookbook\models.py:1232
msgid "Instruction Replace"
msgstr "Anleitung ersetzen"
#: .\cookbook\models.py:1258 .\cookbook\views\delete.py:36
#: .\cookbook\views\edit.py:251 .\cookbook\views\new.py:48
-#: .\cookbook\models.py:1257
msgid "Recipe"
msgstr "Rezept"
-#: .\cookbook\models.py:1259 .\cookbook\models.py:1258
+#: .\cookbook\models.py:1259
msgid "Food"
msgstr "Lebensmittel"
#: .\cookbook\models.py:1260 .\cookbook\templates\base.html:141
-#: .\cookbook\models.py:1259
msgid "Keyword"
msgstr "Schlüsselwort"
@@ -790,49 +763,49 @@ msgstr "Du hast Dein Datei-Uploadlimit erreicht."
msgid "Cannot modify Space owner permission."
msgstr "Die Eigentumsberechtigung am Space kann nicht geändert werden."
-#: .\cookbook\serializer.py:1093 .\cookbook\serializer.py:1085
+#: .\cookbook\serializer.py:1093
msgid "Hello"
msgstr "Hallo"
-#: .\cookbook\serializer.py:1093 .\cookbook\serializer.py:1085
+#: .\cookbook\serializer.py:1093
msgid "You have been invited by "
msgstr "Du wurdest eingeladen von "
-#: .\cookbook\serializer.py:1094 .\cookbook\serializer.py:1086
+#: .\cookbook\serializer.py:1094
msgid " to join their Tandoor Recipes space "
msgstr " um deren Tandoor Recipes Instanz beizutreten "
-#: .\cookbook\serializer.py:1095 .\cookbook\serializer.py:1087
+#: .\cookbook\serializer.py:1095
msgid "Click the following link to activate your account: "
msgstr "Klicke auf den folgenden Link, um deinen Account zu aktivieren: "
-#: .\cookbook\serializer.py:1096 .\cookbook\serializer.py:1088
+#: .\cookbook\serializer.py:1096
msgid ""
"If the link does not work use the following code to manually join the space: "
msgstr ""
"Falls der Link nicht funktioniert, benutze den folgenden Code um dem Space "
"manuell beizutreten: "
-#: .\cookbook\serializer.py:1097 .\cookbook\serializer.py:1089
+#: .\cookbook\serializer.py:1097
msgid "The invitation is valid until "
msgstr "Die Einladung ist gültig bis "
-#: .\cookbook\serializer.py:1098 .\cookbook\serializer.py:1090
+#: .\cookbook\serializer.py:1098
msgid ""
"Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub "
msgstr ""
"Tandoor Recipes ist ein Open-Source Rezept-Manager. Mehr Informationen sind "
"auf GitHub zu finden "
-#: .\cookbook\serializer.py:1101 .\cookbook\serializer.py:1093
+#: .\cookbook\serializer.py:1101
msgid "Tandoor Recipes Invite"
msgstr "Tandoor Recipes Einladung"
-#: .\cookbook\serializer.py:1242 .\cookbook\serializer.py:1234
+#: .\cookbook\serializer.py:1242
msgid "Existing shopping list to update"
msgstr "Bestehende Einkaufliste, die aktualisiert werden soll"
-#: .\cookbook\serializer.py:1244 .\cookbook\serializer.py:1236
+#: .\cookbook\serializer.py:1244
msgid ""
"List of ingredient IDs from the recipe to add, if not provided all "
"ingredients will be added."
@@ -840,23 +813,23 @@ msgstr ""
"Liste der Zutaten-IDs aus dem Rezept, wenn keine Angabe erfolgt, werden alle "
"Zutaten hinzugefügt."
-#: .\cookbook\serializer.py:1246 .\cookbook\serializer.py:1238
+#: .\cookbook\serializer.py:1246
msgid ""
"Providing a list_recipe ID and servings of 0 will delete that shopping list."
msgstr ""
"Wenn Sie eine list_recipe ID und Portion mit dem Wert 0 angeben, wird diese "
"Einkaufsliste gelöscht."
-#: .\cookbook\serializer.py:1255 .\cookbook\serializer.py:1247
+#: .\cookbook\serializer.py:1255
msgid "Amount of food to add to the shopping list"
msgstr ""
"Menge des Lebensmittels, welches der Einkaufsliste hinzugefügt werden soll"
-#: .\cookbook\serializer.py:1257 .\cookbook\serializer.py:1249
+#: .\cookbook\serializer.py:1257
msgid "ID of unit to use for the shopping list"
msgstr "ID der Einheit, die für die Einkaufsliste verwendet werden soll"
-#: .\cookbook\serializer.py:1259 .\cookbook\serializer.py:1251
+#: .\cookbook\serializer.py:1259
msgid "When set to true will delete all food from active shopping lists."
msgstr ""
"Wenn diese Option aktiviert ist, werden alle Lebensmittel aus den aktiven "
@@ -1701,12 +1674,11 @@ msgid "Profile"
msgstr "Profil"
#: .\cookbook\templates\recipe_view.html:41
-#: .\cookbook\templates\recipe_view.html:26
msgid "by"
msgstr "von"
#: .\cookbook\templates\recipe_view.html:59 .\cookbook\views\delete.py:144
-#: .\cookbook\views\edit.py:171 .\cookbook\templates\recipe_view.html:44
+#: .\cookbook\views\edit.py:171
msgid "Comment"
msgstr "Kommentar"
@@ -2335,88 +2307,84 @@ msgid "URL Import"
msgstr "URL-Import"
#: .\cookbook\views\api.py:110 .\cookbook\views\api.py:202
-#: .\cookbook\views\api.py:109 .\cookbook\views\api.py:201
msgid "Parameter updated_at incorrectly formatted"
msgstr "Der Parameter updated_at ist falsch formatiert"
#: .\cookbook\views\api.py:222 .\cookbook\views\api.py:325
-#: .\cookbook\views\api.py:221 .\cookbook\views\api.py:324
#, python-brace-format
msgid "No {self.basename} with id {pk} exists"
msgstr "Kein {self.basename} mit der ID {pk} existiert"
-#: .\cookbook\views\api.py:226 .\cookbook\views\api.py:225
+#: .\cookbook\views\api.py:226
msgid "Cannot merge with the same object!"
msgstr "Zusammenführen mit selben Objekt nicht möglich!"
-#: .\cookbook\views\api.py:233 .\cookbook\views\api.py:232
+#: .\cookbook\views\api.py:233
#, python-brace-format
msgid "No {self.basename} with id {target} exists"
msgstr "Kein {self.basename} mit der ID {target} existiert"
-#: .\cookbook\views\api.py:238 .\cookbook\views\api.py:237
+#: .\cookbook\views\api.py:238
msgid "Cannot merge with child object!"
msgstr "Zusammenführen mit untergeordnetem Objekt nicht möglich!"
-#: .\cookbook\views\api.py:271 .\cookbook\views\api.py:270
+#: .\cookbook\views\api.py:271
#, python-brace-format
msgid "{source.name} was merged successfully with {target.name}"
msgstr "{source.name} wurde erfolgreich mit {target.name} zusammengeführt"
-#: .\cookbook\views\api.py:276 .\cookbook\views\api.py:275
+#: .\cookbook\views\api.py:276
#, python-brace-format
msgid "An error occurred attempting to merge {source.name} with {target.name}"
msgstr ""
"Beim zusammenführen von {source.name} mit {target.name} ist ein Fehler "
"aufgetreten"
-#: .\cookbook\views\api.py:334 .\cookbook\views\api.py:333
+#: .\cookbook\views\api.py:334
#, python-brace-format
msgid "{child.name} was moved successfully to the root."
msgstr "{child.name} wurde erfolgreich zur Wurzel verschoben."
#: .\cookbook\views\api.py:337 .\cookbook\views\api.py:355
-#: .\cookbook\views\api.py:336 .\cookbook\views\api.py:354
msgid "An error occurred attempting to move "
msgstr "Fehler aufgetreten beim verschieben von "
-#: .\cookbook\views\api.py:340 .\cookbook\views\api.py:339
+#: .\cookbook\views\api.py:340
msgid "Cannot move an object to itself!"
msgstr "Ein Element kann nicht in sich selbst verschoben werden!"
-#: .\cookbook\views\api.py:346 .\cookbook\views\api.py:345
+#: .\cookbook\views\api.py:346
#, python-brace-format
msgid "No {self.basename} with id {parent} exists"
msgstr "Kein {self.basename} mit ID {parent} existiert"
-#: .\cookbook\views\api.py:352 .\cookbook\views\api.py:351
+#: .\cookbook\views\api.py:352
#, python-brace-format
msgid "{child.name} was moved successfully to parent {parent.name}"
msgstr ""
"{child.name} wurde erfolgreich zum Überelement {parent.name} verschoben"
-#: .\cookbook\views\api.py:553 .\cookbook\views\api.py:547
+#: .\cookbook\views\api.py:553
#, python-brace-format
msgid "{obj.name} was removed from the shopping list."
msgstr "{obj.name} wurde von der Einkaufsliste entfernt."
#: .\cookbook\views\api.py:558 .\cookbook\views\api.py:888
-#: .\cookbook\views\api.py:901 .\cookbook\views\api.py:552
-#: .\cookbook\views\api.py:882 .\cookbook\views\api.py:895
+#: .\cookbook\views\api.py:901
#, python-brace-format
msgid "{obj.name} was added to the shopping list."
msgstr "{obj.name} wurde der Einkaufsliste hinzugefügt."
-#: .\cookbook\views\api.py:685 .\cookbook\views\api.py:679
+#: .\cookbook\views\api.py:685
msgid "ID of recipe a step is part of. For multiple repeat parameter."
msgstr ""
"ID des Rezeptes zu dem ein Schritt gehört. Kann mehrfach angegeben werden."
-#: .\cookbook\views\api.py:687 .\cookbook\views\api.py:681
+#: .\cookbook\views\api.py:687
msgid "Query string matched (fuzzy) against object name."
msgstr "Abfragezeichenfolge, die mit dem Objektnamen übereinstimmt (ungenau)."
-#: .\cookbook\views\api.py:731 .\cookbook\views\api.py:725
+#: .\cookbook\views\api.py:731
msgid ""
"Query string matched (fuzzy) against recipe name. In the future also "
"fulltext search."
@@ -2424,7 +2392,7 @@ msgstr ""
"Suchbegriff wird mit dem Rezeptnamen abgeglichen. In Zukunft auch "
"Volltextsuche."
-#: .\cookbook\views\api.py:733 .\cookbook\views\api.py:727
+#: .\cookbook\views\api.py:733
msgid ""
"ID of keyword a recipe should have. For multiple repeat parameter. "
"Equivalent to keywords_or"
@@ -2432,69 +2400,69 @@ msgstr ""
"ID des Stichwortes, das ein Rezept haben muss. Kann mehrfach angegeben "
"werden. Äquivalent zu keywords_or"
-#: .\cookbook\views\api.py:736 .\cookbook\views\api.py:730
+#: .\cookbook\views\api.py:736
msgid ""
"Keyword IDs, repeat for multiple. Return recipes with any of the keywords"
msgstr ""
"Stichwort IDs. Kann mehrfach angegeben werden. Listet Rezepte zu jedem der "
"angegebenen Stichwörter"
-#: .\cookbook\views\api.py:739 .\cookbook\views\api.py:733
+#: .\cookbook\views\api.py:739
msgid ""
"Keyword IDs, repeat for multiple. Return recipes with all of the keywords."
msgstr ""
"Stichwort IDs. Kann mehrfach angegeben werden. Listet Rezepte mit allen "
"angegebenen Stichwörtern."
-#: .\cookbook\views\api.py:742 .\cookbook\views\api.py:736
+#: .\cookbook\views\api.py:742
msgid ""
"Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords."
msgstr ""
"Stichwort ID. Kann mehrfach angegeben werden. Schließt Rezepte einem der "
"angegebenen Stichwörtern aus."
-#: .\cookbook\views\api.py:745 .\cookbook\views\api.py:739
+#: .\cookbook\views\api.py:745
msgid ""
"Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords."
msgstr ""
"Stichwort IDs. Kann mehrfach angegeben werden. Schließt Rezepte mit allen "
"angegebenen Stichwörtern aus."
-#: .\cookbook\views\api.py:747 .\cookbook\views\api.py:741
+#: .\cookbook\views\api.py:747
msgid "ID of food a recipe should have. For multiple repeat parameter."
msgstr ""
"ID einer Zutat, zu der Rezepte gelistet werden sollen. Kann mehrfach "
"angegeben werden."
-#: .\cookbook\views\api.py:750 .\cookbook\views\api.py:744
+#: .\cookbook\views\api.py:750
msgid "Food IDs, repeat for multiple. Return recipes with any of the foods"
msgstr ""
"Zutat ID. Kann mehrfach angegeben werden. Listet Rezepte mindestens einer "
"der Zutaten"
-#: .\cookbook\views\api.py:752 .\cookbook\views\api.py:746
+#: .\cookbook\views\api.py:752
msgid "Food IDs, repeat for multiple. Return recipes with all of the foods."
msgstr ""
"Zutat ID. Kann mehrfach angegeben werden. Listet Rezepte mit allen "
"angegebenen Zutaten."
-#: .\cookbook\views\api.py:754 .\cookbook\views\api.py:748
+#: .\cookbook\views\api.py:754
msgid "Food IDs, repeat for multiple. Exclude recipes with any of the foods."
msgstr ""
"Zutat ID. Kann mehrfach angegeben werden. Schließt Rezepte aus, die eine der "
"angegebenen Zutaten enthalten."
-#: .\cookbook\views\api.py:756 .\cookbook\views\api.py:750
+#: .\cookbook\views\api.py:756
msgid "Food IDs, repeat for multiple. Exclude recipes with all of the foods."
msgstr ""
"Zutat ID. Kann mehrfach angegeben werden. Schließt Rezepte aus, die alle "
"angegebenen Zutaten enthalten."
-#: .\cookbook\views\api.py:757 .\cookbook\views\api.py:751
+#: .\cookbook\views\api.py:757
msgid "ID of unit a recipe should have."
msgstr "ID der Einheit, die ein Rezept haben sollte."
-#: .\cookbook\views\api.py:759 .\cookbook\views\api.py:753
+#: .\cookbook\views\api.py:759
msgid ""
"Rating a recipe should have or greater. [0 - 5] Negative value filters "
"rating less than."
@@ -2502,50 +2470,50 @@ msgstr ""
"Mindestbewertung eines Rezeptes (0-5). Negative Werte filtern nach "
"Maximalbewertung."
-#: .\cookbook\views\api.py:760 .\cookbook\views\api.py:754
+#: .\cookbook\views\api.py:760
msgid "ID of book a recipe should be in. For multiple repeat parameter."
msgstr "Buch ID, in dem das Rezept ist. Kann mehrfach angegeben werden."
-#: .\cookbook\views\api.py:762 .\cookbook\views\api.py:756
+#: .\cookbook\views\api.py:762
msgid "Book IDs, repeat for multiple. Return recipes with any of the books"
msgstr ""
"Buch ID. Kann mehrfach angegeben werden. Listet alle Rezepte aus den "
"angegebenen Büchern"
-#: .\cookbook\views\api.py:764 .\cookbook\views\api.py:758
+#: .\cookbook\views\api.py:764
msgid "Book IDs, repeat for multiple. Return recipes with all of the books."
msgstr ""
"Buch ID. Kann mehrfach angegeben werden. Listet die Rezepte, die in allen "
"Büchern enthalten sind."
-#: .\cookbook\views\api.py:766 .\cookbook\views\api.py:760
+#: .\cookbook\views\api.py:766
msgid "Book IDs, repeat for multiple. Exclude recipes with any of the books."
msgstr ""
"Buch IDs. Kann mehrfach angegeben werden. Schließt Rezepte aus den "
"angegebenen Büchern aus."
-#: .\cookbook\views\api.py:768 .\cookbook\views\api.py:762
+#: .\cookbook\views\api.py:768
msgid "Book IDs, repeat for multiple. Exclude recipes with all of the books."
msgstr ""
"Buch IDs. Kann mehrfach angegeben werden. Schließt Rezepte aus, die in allen "
"angegebenen Büchern enthalten sind."
-#: .\cookbook\views\api.py:770 .\cookbook\views\api.py:764
+#: .\cookbook\views\api.py:770
msgid "If only internal recipes should be returned. [true/false]"
msgstr "Nur interne Rezepte sollen gelistet werden. [ja/nein]"
-#: .\cookbook\views\api.py:772 .\cookbook\views\api.py:766
+#: .\cookbook\views\api.py:772
msgid "Returns the results in randomized order. [true/false]"
msgstr ""
"Die Suchergebnisse sollen in zufälliger Reihenfolge gelistet werden. [ja/"
"nein]"
-#: .\cookbook\views\api.py:774 .\cookbook\views\api.py:768
+#: .\cookbook\views\api.py:774
msgid "Returns new results first in search results. [true/false]"
msgstr ""
"Die neuesten Suchergebnisse sollen zuerst angezeigt werden. [ja/nein]"
-#: .\cookbook\views\api.py:776 .\cookbook\views\api.py:770
+#: .\cookbook\views\api.py:776
msgid ""
"Filter recipes cooked X times or more. Negative values returns cooked less "
"than X times"
@@ -2553,7 +2521,7 @@ msgstr ""
"Rezepte listen, die mindestens x-mal gekocht wurden. Eine negative Zahl "
"listet Rezepte, die weniger als x-mal gekocht wurden"
-#: .\cookbook\views\api.py:778 .\cookbook\views\api.py:772
+#: .\cookbook\views\api.py:778
msgid ""
"Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on "
"or before date."
@@ -2562,7 +2530,7 @@ msgstr ""
"wurden. Mit vorangestelltem - , werden Rezepte am oder vor dem Datum "
"gelistet."
-#: .\cookbook\views\api.py:780 .\cookbook\views\api.py:774
+#: .\cookbook\views\api.py:780
msgid ""
"Filter recipes created on or after YYYY-MM-DD. Prepending - filters on or "
"before date."
@@ -2570,7 +2538,7 @@ msgstr ""
"Rezepte listen, die am angegebenen Datum oder später erstellt wurden. Wenn - "
"vorangestellt wird, wird am oder vor dem Datum gelistet."
-#: .\cookbook\views\api.py:782 .\cookbook\views\api.py:776
+#: .\cookbook\views\api.py:782
msgid ""
"Filter recipes updated on or after YYYY-MM-DD. Prepending - filters on or "
"before date."
@@ -2578,7 +2546,7 @@ msgstr ""
"Rezepte listen, die am angegebenen Datum oder später aktualisiert wurden. "
"Wenn - vorangestellt wird, wird am oder vor dem Datum gelistet."
-#: .\cookbook\views\api.py:784 .\cookbook\views\api.py:778
+#: .\cookbook\views\api.py:784
msgid ""
"Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending - filters on "
"or before date."
@@ -2586,13 +2554,13 @@ msgstr ""
"Rezepte listen, die am angegebenen Datum oder später zuletzt angesehen "
"wurden. Wenn - vorangestellt wird, wird am oder vor dem Datum gelistet."
-#: .\cookbook\views\api.py:786 .\cookbook\views\api.py:780
+#: .\cookbook\views\api.py:786
msgid "Filter recipes that can be made with OnHand food. [true/false]"
msgstr ""
"Rezepte listen, die mit vorhandenen Zutaten gekocht werden können. [ja/"
"nein]"
-#: .\cookbook\views\api.py:946 .\cookbook\views\api.py:940
+#: .\cookbook\views\api.py:946
msgid ""
"Returns the shopping list entry with a primary key of id. Multiple values "
"allowed."
@@ -2600,7 +2568,7 @@ msgstr ""
"Zeigt denjenigen Eintrag auf der Einkaufliste mit der angegebenen ID. Kann "
"mehrfach angegeben werden."
-#: .\cookbook\views\api.py:951 .\cookbook\views\api.py:945
+#: .\cookbook\views\api.py:951
msgid ""
"Filter shopping list entries on checked. [true, false, both, recent"
"b>]/remote."
"php/webdav/ is added automatically)"
msgstr ""
-#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157
+#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr ""
-#: .\cookbook\forms.py:267
+#: .\cookbook\forms.py:284
msgid "Active"
msgstr ""
-#: .\cookbook\forms.py:273
+#: .\cookbook\forms.py:290
msgid "Search String"
msgstr ""
-#: .\cookbook\forms.py:300
+#: .\cookbook\forms.py:317
msgid "File ID"
msgstr ""
-#: .\cookbook\forms.py:322
+#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr ""
-#: .\cookbook\forms.py:335
+#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr ""
-#: .\cookbook\forms.py:336
+#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the docs here"
msgstr ""
-#: .\cookbook\forms.py:362
+#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr ""
-#: .\cookbook\forms.py:368
+#: .\cookbook\forms.py:385
msgid "Email address already taken!"
msgstr ""
-#: .\cookbook\forms.py:376
+#: .\cookbook\forms.py:393
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
msgstr ""
-#: .\cookbook\forms.py:391
+#: .\cookbook\forms.py:408
msgid "Name already taken."
msgstr ""
-#: .\cookbook\forms.py:402
+#: .\cookbook\forms.py:419
msgid "Accept Terms and Privacy"
msgstr ""
-#: .\cookbook\forms.py:434
+#: .\cookbook\forms.py:451
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:444
+#: .\cookbook\forms.py:461
msgid ""
"Select type method of search. Click here for "
"full description of choices."
msgstr ""
-#: .\cookbook\forms.py:445
+#: .\cookbook\forms.py:462
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
msgstr ""
-#: .\cookbook\forms.py:447
+#: .\cookbook\forms.py:464
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
msgstr ""
-#: .\cookbook\forms.py:449
+#: .\cookbook\forms.py:466
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
msgstr ""
-#: .\cookbook\forms.py:451
+#: .\cookbook\forms.py:468
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
msgstr ""
-#: .\cookbook\forms.py:453
+#: .\cookbook\forms.py:470
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:455
+#: .\cookbook\forms.py:472
msgid ""
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
"only function with fulltext fields."
msgstr ""
-#: .\cookbook\forms.py:459
+#: .\cookbook\forms.py:476
msgid "Search Method"
msgstr ""
-#: .\cookbook\forms.py:460
+#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
msgstr ""
-#: .\cookbook\forms.py:461
+#: .\cookbook\forms.py:478
msgid "Ignore Accent"
msgstr ""
-#: .\cookbook\forms.py:462
+#: .\cookbook\forms.py:479
msgid "Partial Match"
msgstr ""
-#: .\cookbook\forms.py:463
+#: .\cookbook\forms.py:480
msgid "Starts With"
msgstr ""
-#: .\cookbook\forms.py:464
+#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
msgstr ""
-#: .\cookbook\forms.py:465
+#: .\cookbook\forms.py:482
msgid "Full Text"
msgstr ""
-#: .\cookbook\forms.py:490
+#: .\cookbook\forms.py:507
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:496
+#: .\cookbook\forms.py:513
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
msgstr ""
-#: .\cookbook\forms.py:497
+#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
msgstr ""
-#: .\cookbook\forms.py:498
+#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
msgstr ""
-#: .\cookbook\forms.py:499
+#: .\cookbook\forms.py:516
msgid "Filter shopping list to only include supermarket categories."
msgstr ""
-#: .\cookbook\forms.py:500
+#: .\cookbook\forms.py:517
msgid "Days of recent shopping list entries to display."
msgstr ""
-#: .\cookbook\forms.py:501
+#: .\cookbook\forms.py:518
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr ""
-#: .\cookbook\forms.py:502
+#: .\cookbook\forms.py:519
msgid "Delimiter to use for CSV exports."
msgstr ""
-#: .\cookbook\forms.py:503
+#: .\cookbook\forms.py:520
msgid "Prefix to add when copying list to the clipboard."
msgstr ""
-#: .\cookbook\forms.py:507
+#: .\cookbook\forms.py:524
msgid "Share Shopping List"
msgstr ""
-#: .\cookbook\forms.py:508
+#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr ""
-#: .\cookbook\forms.py:509
+#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr ""
-#: .\cookbook\forms.py:510
+#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
msgstr ""
-#: .\cookbook\forms.py:511
+#: .\cookbook\forms.py:528
msgid "Include Related"
msgstr ""
-#: .\cookbook\forms.py:512
+#: .\cookbook\forms.py:529
msgid "Default Delay Hours"
msgstr ""
-#: .\cookbook\forms.py:513
+#: .\cookbook\forms.py:530
msgid "Filter to Supermarket"
msgstr ""
-#: .\cookbook\forms.py:514
+#: .\cookbook\forms.py:531
msgid "Recent Days"
msgstr ""
-#: .\cookbook\forms.py:515
+#: .\cookbook\forms.py:532
msgid "CSV Delimiter"
msgstr ""
-#: .\cookbook\forms.py:516
+#: .\cookbook\forms.py:533
msgid "List Prefix"
msgstr ""
-#: .\cookbook\forms.py:517
+#: .\cookbook\forms.py:534
msgid "Auto On Hand"
msgstr ""
-#: .\cookbook\forms.py:527
+#: .\cookbook\forms.py:544
msgid "Reset Food Inheritance"
msgstr ""
-#: .\cookbook\forms.py:528
+#: .\cookbook\forms.py:545
msgid "Reset all food to inherit the fields configured."
msgstr ""
-#: .\cookbook\forms.py:540
+#: .\cookbook\forms.py:557
msgid "Fields on food that should be inherited by default."
msgstr ""
-#: .\cookbook\forms.py:541
+#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
msgstr ""
-#: .\cookbook\forms.py:542
+#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""
diff --git a/cookbook/locale/es/LC_MESSAGES/django.mo b/cookbook/locale/es/LC_MESSAGES/django.mo
index faabf0ed7..70a5c0c48 100644
Binary files a/cookbook/locale/es/LC_MESSAGES/django.mo and b/cookbook/locale/es/LC_MESSAGES/django.mo differ
diff --git a/cookbook/locale/es/LC_MESSAGES/django.po b/cookbook/locale/es/LC_MESSAGES/django.po
index 3aa41cdc7..ff38fc722 100644
--- a/cookbook/locale/es/LC_MESSAGES/django.po
+++ b/cookbook/locale/es/LC_MESSAGES/django.po
@@ -13,9 +13,9 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-26 07:46+0200\n"
-"PO-Revision-Date: 2023-03-13 06:55+0000\n"
-"Last-Translator: Amara Ude /remote."
"php/webdav/ is added automatically)"
@@ -216,33 +216,33 @@ msgstr ""
"Dejar vació para Dropbox e introducir sólo la URL base para Nextcloud "
"(/remote.php/webdav/ se añade automáticamente)"
-#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157
+#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr "Almacenamiento"
-#: .\cookbook\forms.py:267
+#: .\cookbook\forms.py:284
msgid "Active"
msgstr "Activo"
-#: .\cookbook\forms.py:273
+#: .\cookbook\forms.py:290
msgid "Search String"
msgstr "Cadena de búsqueda"
-#: .\cookbook\forms.py:300
+#: .\cookbook\forms.py:317
msgid "File ID"
msgstr "ID de Fichero"
-#: .\cookbook\forms.py:322
+#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr "Debe proporcionar al menos una receta o un título."
-#: .\cookbook\forms.py:335
+#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"Puede enumerar los usuarios predeterminados con los que compartir recetas en "
"la configuración."
-#: .\cookbook\forms.py:336
+#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the docs here"
@@ -250,15 +250,15 @@ msgstr ""
"Puede utilizar Markdown para formatear este campo. Vea la documentación aqui"
-#: .\cookbook\forms.py:362
+#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr "Se ha alcanzado el número máximo de usuarios en este espacio."
-#: .\cookbook\forms.py:368
+#: .\cookbook\forms.py:385
msgid "Email address already taken!"
msgstr "¡El correo electrónico ya existe!"
-#: .\cookbook\forms.py:376
+#: .\cookbook\forms.py:393
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
@@ -266,15 +266,15 @@ msgstr ""
"El correo electrónico es opcional. Si se añade uno se mandará un link de "
"invitación."
-#: .\cookbook\forms.py:391
+#: .\cookbook\forms.py:408
msgid "Name already taken."
msgstr "El nombre ya existe."
-#: .\cookbook\forms.py:402
+#: .\cookbook\forms.py:419
msgid "Accept Terms and Privacy"
msgstr "Aceptar términos y condiciones"
-#: .\cookbook\forms.py:434
+#: .\cookbook\forms.py:451
msgid ""
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
"g. low values mean more typos are ignored)."
@@ -283,7 +283,7 @@ msgstr ""
"similitud de trigramas(Ej. Valores más pequeños indican que más fallos se "
"van a ignorar)."
-#: .\cookbook\forms.py:444
+#: .\cookbook\forms.py:461
msgid ""
"Select type method of search. Click here for "
"full description of choices."
@@ -291,7 +291,7 @@ msgstr ""
"Selecciona el tipo de búsqueda. Haz click aquí"
"a> para una descripción completa de las opciones."
-#: .\cookbook\forms.py:445
+#: .\cookbook\forms.py:462
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
@@ -299,7 +299,7 @@ msgstr ""
"Utilizar comparación difusa en unidades, palabras clave e ingredientes al "
"editar e importar recetas."
-#: .\cookbook\forms.py:447
+#: .\cookbook\forms.py:464
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
@@ -307,7 +307,7 @@ msgstr ""
"Campos de búsqueda ignorando acentos. La selección de esta opción puede "
"mejorar o degradar la calidad de la búsqueda dependiendo del idioma"
-#: .\cookbook\forms.py:449
+#: .\cookbook\forms.py:466
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
@@ -315,7 +315,7 @@ msgstr ""
"Campos de búsqueda para coincidencias parciales. (por ejemplo, buscar 'Pie' "
"devolverá 'pie' y 'piece' y 'soapie')"
-#: .\cookbook\forms.py:451
+#: .\cookbook\forms.py:468
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
@@ -323,7 +323,7 @@ msgstr ""
"Campos de búsqueda para coincidencias al principio de la palabra. (por "
"ejemplo, buscar 'sa' devolverá 'ensalada' y 'sándwich')"
-#: .\cookbook\forms.py:453
+#: .\cookbook\forms.py:470
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."
@@ -332,7 +332,7 @@ msgstr ""
"'receta'). Nota: esta opción entrará en conflicto con los métodos de "
"búsqueda 'web' y 'raw'."
-#: .\cookbook\forms.py:455
+#: .\cookbook\forms.py:472
msgid ""
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
"only function with fulltext fields."
@@ -340,35 +340,35 @@ msgstr ""
"Campos para búsqueda de texto completo. Nota: los métodos de búsqueda 'web', "
"'phrase' y 'raw' solo funcionan con campos de texto completo."
-#: .\cookbook\forms.py:459
+#: .\cookbook\forms.py:476
msgid "Search Method"
msgstr "Método de Búsqueda"
-#: .\cookbook\forms.py:460
+#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
msgstr "Búsquedas difusas"
-#: .\cookbook\forms.py:461
+#: .\cookbook\forms.py:478
msgid "Ignore Accent"
msgstr "Ignorar Acento"
-#: .\cookbook\forms.py:462
+#: .\cookbook\forms.py:479
msgid "Partial Match"
msgstr "Coincidencia Parcial"
-#: .\cookbook\forms.py:463
+#: .\cookbook\forms.py:480
msgid "Starts With"
msgstr "Comienza Con"
-#: .\cookbook\forms.py:464
+#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
msgstr "Búsqueda Difusa"
-#: .\cookbook\forms.py:465
+#: .\cookbook\forms.py:482
msgid "Full Text"
msgstr "Texto Completo"
-#: .\cookbook\forms.py:490
+#: .\cookbook\forms.py:507
msgid ""
"Users will see all items you add to your shopping list. They must add you "
"to see items on their list."
@@ -376,7 +376,7 @@ msgstr ""
"Los usuarios verán todos los elementos que agregues a tu lista de compras. "
"Deben agregarte para ver los elementos en su lista."
-#: .\cookbook\forms.py:496
+#: .\cookbook\forms.py:513
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
@@ -384,7 +384,7 @@ msgstr ""
"Al agregar un plan de comidas a la lista de compras (manualmente o "
"automáticamente), incluir todas las recetas relacionadas."
-#: .\cookbook\forms.py:497
+#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
@@ -392,96 +392,96 @@ msgstr ""
"Al agregar un plan de comidas a la lista de compras (manualmente o "
"automáticamente), excluir los ingredientes que están disponibles."
-#: .\cookbook\forms.py:498
+#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
msgstr ""
"Número predeterminado de horas para retrasar una entrada en la lista de "
"compras."
-#: .\cookbook\forms.py:499
+#: .\cookbook\forms.py:516
msgid "Filter shopping list to only include supermarket categories."
msgstr ""
"Filtrar la lista de compras para incluir solo categorías de supermercados."
-#: .\cookbook\forms.py:500
+#: .\cookbook\forms.py:517
msgid "Days of recent shopping list entries to display."
msgstr "Días de entradas recientes en la lista de compras a mostrar."
-#: .\cookbook\forms.py:501
+#: .\cookbook\forms.py:518
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr ""
"Marcar los alimentos como 'Disponible' cuando se marca en la lista de "
"compras."
-#: .\cookbook\forms.py:502
+#: .\cookbook\forms.py:519
msgid "Delimiter to use for CSV exports."
msgstr "Delimitador a utilizar para exportaciones CSV."
-#: .\cookbook\forms.py:503
+#: .\cookbook\forms.py:520
msgid "Prefix to add when copying list to the clipboard."
msgstr "Prefijo a agregar al copiar la lista al portapapeles."
-#: .\cookbook\forms.py:507
+#: .\cookbook\forms.py:524
msgid "Share Shopping List"
msgstr "Compartir Lista de la Compra"
-#: .\cookbook\forms.py:508
+#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr "Autosincronización"
-#: .\cookbook\forms.py:509
+#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr "Agregar Plan de Comidas automáticamente"
-#: .\cookbook\forms.py:510
+#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
msgstr "Excluir Disponible"
-#: .\cookbook\forms.py:511
+#: .\cookbook\forms.py:528
msgid "Include Related"
msgstr "Incluir Relacionados"
-#: .\cookbook\forms.py:512
+#: .\cookbook\forms.py:529
msgid "Default Delay Hours"
msgstr "Horas de Retraso Predeterminadas"
-#: .\cookbook\forms.py:513
+#: .\cookbook\forms.py:530
msgid "Filter to Supermarket"
msgstr "Filtrar según Supermercado"
-#: .\cookbook\forms.py:514
+#: .\cookbook\forms.py:531
msgid "Recent Days"
msgstr "Días Recientes"
-#: .\cookbook\forms.py:515
+#: .\cookbook\forms.py:532
msgid "CSV Delimiter"
msgstr "Delimitador CSV"
-#: .\cookbook\forms.py:516
+#: .\cookbook\forms.py:533
msgid "List Prefix"
msgstr "Prefijo de la lista"
-#: .\cookbook\forms.py:517
+#: .\cookbook\forms.py:534
msgid "Auto On Hand"
msgstr "Auto en existencia"
-#: .\cookbook\forms.py:527
+#: .\cookbook\forms.py:544
msgid "Reset Food Inheritance"
msgstr "Restablecer la herencia de alimentos"
-#: .\cookbook\forms.py:528
+#: .\cookbook\forms.py:545
msgid "Reset all food to inherit the fields configured."
msgstr "Reiniciar todos los alimentos para heredar los campos configurados."
-#: .\cookbook\forms.py:540
+#: .\cookbook\forms.py:557
msgid "Fields on food that should be inherited by default."
msgstr "Campos en los alimentos que deben ser heredados por defecto."
-#: .\cookbook\forms.py:541
+#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
msgstr "Mostrar cantidad de recetas en los filtros de búsquedas"
-#: .\cookbook\forms.py:542
+#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""
"Utilice la forma plural para las unidades y alimentos dentro de este espacio."
@@ -603,10 +603,8 @@ msgid "Imported %s recipes."
msgstr "Se importaron %s recetas."
#: .\cookbook\integration\openeats.py:26
-#, fuzzy
-#| msgid "Recipe Home"
msgid "Recipe source:"
-msgstr "Página de inicio"
+msgstr "Recipe source:"
#: .\cookbook\integration\paprika.py:49
msgid "Notes"
diff --git a/cookbook/locale/fr/LC_MESSAGES/django.po b/cookbook/locale/fr/LC_MESSAGES/django.po
index 52aea2455..ba6f4a978 100644
--- a/cookbook/locale/fr/LC_MESSAGES/django.po
+++ b/cookbook/locale/fr/LC_MESSAGES/django.po
@@ -13,7 +13,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-26 07:46+0200\n"
+"POT-Creation-Date: 2023-05-18 14:28+0200\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad /remote.php/webdav/ est ajouté automatiquement)"
-#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157
+#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr "Stockage"
-#: .\cookbook\forms.py:267
+#: .\cookbook\forms.py:284
msgid "Active"
msgstr "Actif"
-#: .\cookbook\forms.py:273
+#: .\cookbook\forms.py:290
msgid "Search String"
msgstr "Texte recherché"
-#: .\cookbook\forms.py:300
+#: .\cookbook\forms.py:317
msgid "File ID"
msgstr "ID du fichier"
-#: .\cookbook\forms.py:322
+#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr "Vous devez au moins fournir une recette ou un titre."
-#: .\cookbook\forms.py:335
+#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"Vous pouvez lister les utilisateurs par défaut avec qui partager des "
"recettes dans les paramètres."
-#: .\cookbook\forms.py:336
+#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the docs here"
@@ -254,15 +254,15 @@ msgstr ""
"Vous pouvez utiliser du markdown pour mettre en forme ce champ. Voir la documentation ici"
-#: .\cookbook\forms.py:362
+#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr "Nombre maximum d’utilisateurs atteint pour ce groupe."
-#: .\cookbook\forms.py:368
+#: .\cookbook\forms.py:385
msgid "Email address already taken!"
msgstr "Adresse mail déjà utilisée !"
-#: .\cookbook\forms.py:376
+#: .\cookbook\forms.py:393
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
@@ -270,15 +270,15 @@ msgstr ""
"Une adresse mail n’est pas requise mais si elle est renseignée, le lien "
"d’invitation sera envoyé à l’utilisateur."
-#: .\cookbook\forms.py:391
+#: .\cookbook\forms.py:408
msgid "Name already taken."
msgstr "Nom déjà utilisé."
-#: .\cookbook\forms.py:402
+#: .\cookbook\forms.py:419
msgid "Accept Terms and Privacy"
msgstr "Accepter les conditions d’utilisation"
-#: .\cookbook\forms.py:434
+#: .\cookbook\forms.py:451
msgid ""
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
"g. low values mean more typos are ignored)."
@@ -287,7 +287,7 @@ msgstr ""
"par similarité de trigrammes (par exemple, des valeurs faibles signifient "
"que davantage de fautes de frappe sont ignorées)."
-#: .\cookbook\forms.py:444
+#: .\cookbook\forms.py:461
msgid ""
"Select type method of search. Click here for "
"full description of choices."
@@ -295,7 +295,7 @@ msgstr ""
"Sélectionner la méthode de recherche. Cliquer ici pour une description complète des choix."
-#: .\cookbook\forms.py:445
+#: .\cookbook\forms.py:462
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
@@ -303,7 +303,7 @@ msgstr ""
"Utilisez la correspondance floue sur les unités, les mots-clés et les "
"ingrédients lors de l’édition et de l’importation de recettes."
-#: .\cookbook\forms.py:447
+#: .\cookbook\forms.py:464
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
@@ -312,7 +312,7 @@ msgstr ""
"peut améliorer ou dégrader la qualité de la recherche en fonction de la "
"langue."
-#: .\cookbook\forms.py:449
+#: .\cookbook\forms.py:466
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
@@ -320,7 +320,7 @@ msgstr ""
"Champs à rechercher pour les correspondances partielles. (par exemple, la "
"recherche de « Tarte » renverra « tarte », « tartelette » et « tartes »)"
-#: .\cookbook\forms.py:451
+#: .\cookbook\forms.py:468
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
@@ -329,7 +329,7 @@ msgstr ""
"exemple, si vous recherchez « sa », vous obtiendrez « salade » et "
"« sandwich»)."
-#: .\cookbook\forms.py:453
+#: .\cookbook\forms.py:470
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."
@@ -338,7 +338,7 @@ msgstr ""
"« rectte», vous trouverez « recette ».) Remarque : cette option est "
"incompatible avec les méthodes de recherche « web » et « brute »."
-#: .\cookbook\forms.py:455
+#: .\cookbook\forms.py:472
msgid ""
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
"only function with fulltext fields."
@@ -347,35 +347,35 @@ msgstr ""
"« web », « phrase » et « brute » ne fonctionnent qu’avec des champs en texte "
"intégral."
-#: .\cookbook\forms.py:459
+#: .\cookbook\forms.py:476
msgid "Search Method"
msgstr "Méthode de recherche"
-#: .\cookbook\forms.py:460
+#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
msgstr "Recherches floues"
-#: .\cookbook\forms.py:461
+#: .\cookbook\forms.py:478
msgid "Ignore Accent"
msgstr "Ignorer les accents"
-#: .\cookbook\forms.py:462
+#: .\cookbook\forms.py:479
msgid "Partial Match"
msgstr "correspondance partielle"
-#: .\cookbook\forms.py:463
+#: .\cookbook\forms.py:480
msgid "Starts With"
msgstr "Commence par"
-#: .\cookbook\forms.py:464
+#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
msgstr "Recherche floue"
-#: .\cookbook\forms.py:465
+#: .\cookbook\forms.py:482
msgid "Full Text"
msgstr "Texte intégral"
-#: .\cookbook\forms.py:490
+#: .\cookbook\forms.py:507
msgid ""
"Users will see all items you add to your shopping list. They must add you "
"to see items on their list."
@@ -384,7 +384,7 @@ msgstr ""
"courses. Ils doivent vous ajouter pour que vous puissiez voir les éléments "
"de leur liste."
-#: .\cookbook\forms.py:496
+#: .\cookbook\forms.py:513
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
@@ -392,7 +392,7 @@ msgstr ""
"Lors de l’ajout d’un menu de la semaine à la liste de courses (manuel ou "
"automatique), inclure toutes les recettes connexes."
-#: .\cookbook\forms.py:497
+#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
@@ -400,97 +400,97 @@ msgstr ""
"Lors de l’ajout d’un menu de la semaine à la liste de courses (manuel ou "
"automatique), exclure les ingrédients disponibles."
-#: .\cookbook\forms.py:498
+#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
msgstr ""
"Nombre d'heures par défaut pour retarder l'ajoût d'un article à la liste de "
"courses."
-#: .\cookbook\forms.py:499
+#: .\cookbook\forms.py:516
msgid "Filter shopping list to only include supermarket categories."
msgstr ""
"Filtrer la liste de courses pour n’inclure que des catégories de "
"supermarchés."
-#: .\cookbook\forms.py:500
+#: .\cookbook\forms.py:517
msgid "Days of recent shopping list entries to display."
msgstr "Jours des entrées récentes de la liste de courses à afficher."
-#: .\cookbook\forms.py:501
+#: .\cookbook\forms.py:518
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr ""
"Marquer l’aliment comme disponible lorsqu’il est rayé de la liste de courses."
-#: .\cookbook\forms.py:502
+#: .\cookbook\forms.py:519
msgid "Delimiter to use for CSV exports."
msgstr "Caractère de séparation à utiliser pour les exportations CSV."
-#: .\cookbook\forms.py:503
+#: .\cookbook\forms.py:520
msgid "Prefix to add when copying list to the clipboard."
msgstr "Préfixe à ajouter lors de la copie de la liste dans le presse-papiers."
-#: .\cookbook\forms.py:507
+#: .\cookbook\forms.py:524
msgid "Share Shopping List"
msgstr "Partager la liste de courses"
-#: .\cookbook\forms.py:508
+#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr "Synchronisation automatique"
-#: .\cookbook\forms.py:509
+#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr "Ajouter le menu de la semaine automatiquement"
-#: .\cookbook\forms.py:510
+#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
msgstr "Exclure ingrédients disponibles"
-#: .\cookbook\forms.py:511
+#: .\cookbook\forms.py:528
msgid "Include Related"
msgstr "Inclure recettes connexes"
-#: .\cookbook\forms.py:512
+#: .\cookbook\forms.py:529
msgid "Default Delay Hours"
msgstr "Heures de retard par défaut"
-#: .\cookbook\forms.py:513
+#: .\cookbook\forms.py:530
msgid "Filter to Supermarket"
msgstr "Filtrer par supermarché"
-#: .\cookbook\forms.py:514
+#: .\cookbook\forms.py:531
msgid "Recent Days"
msgstr "Jours récents"
-#: .\cookbook\forms.py:515
+#: .\cookbook\forms.py:532
msgid "CSV Delimiter"
msgstr "Caractère de séparation CSV"
-#: .\cookbook\forms.py:516
+#: .\cookbook\forms.py:533
msgid "List Prefix"
msgstr "Préfixe de la liste"
-#: .\cookbook\forms.py:517
+#: .\cookbook\forms.py:534
msgid "Auto On Hand"
msgstr "Disponible automatique"
-#: .\cookbook\forms.py:527
+#: .\cookbook\forms.py:544
msgid "Reset Food Inheritance"
msgstr "Réinitialiser l'héritage alimentaire"
-#: .\cookbook\forms.py:528
+#: .\cookbook\forms.py:545
msgid "Reset all food to inherit the fields configured."
msgstr "Réinitialiser tous les aliments pour hériter les champs configurés."
-#: .\cookbook\forms.py:540
+#: .\cookbook\forms.py:557
msgid "Fields on food that should be inherited by default."
msgstr "Champs sur les aliments à hériter par défaut."
-#: .\cookbook\forms.py:541
+#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
msgstr ""
"Afficher le nombre de consultations par recette sur les filtres de recherche"
-#: .\cookbook\forms.py:542
+#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""
"Utiliser la forme plurielle pour les unités et les aliments dans ce groupe."
diff --git a/cookbook/locale/hu_HU/LC_MESSAGES/django.po b/cookbook/locale/hu_HU/LC_MESSAGES/django.po
index db8b6503f..8193bf1a8 100644
--- a/cookbook/locale/hu_HU/LC_MESSAGES/django.po
+++ b/cookbook/locale/hu_HU/LC_MESSAGES/django.po
@@ -10,7 +10,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-26 07:46+0200\n"
+"POT-Creation-Date: 2023-05-18 14:28+0200\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad /remote.php/webdav/ automatikusan hozzáadódik)"
-#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157
+#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr "Tárhely"
-#: .\cookbook\forms.py:267
+#: .\cookbook\forms.py:284
msgid "Active"
msgstr "Aktív"
-#: .\cookbook\forms.py:273
+#: .\cookbook\forms.py:290
msgid "Search String"
msgstr "Keresési kifejezés"
-#: .\cookbook\forms.py:300
+#: .\cookbook\forms.py:317
msgid "File ID"
msgstr "Fájl ID"
-#: .\cookbook\forms.py:322
+#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr "Legalább egy receptet vagy címet kell megadnia."
-#: .\cookbook\forms.py:335
+#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"A beállításokban megadhatja a receptek megosztására szolgáló alapértelmezett "
"felhasználókat."
-#: .\cookbook\forms.py:336
+#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the docs here"
@@ -249,15 +249,15 @@ msgstr ""
"A mező formázásához használhatja a markdown formátumot. Lásd a dokumentációt itt"
-#: .\cookbook\forms.py:362
+#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr "Elérte a felhasználók maximális számát ezen a területen."
-#: .\cookbook\forms.py:368
+#: .\cookbook\forms.py:385
msgid "Email address already taken!"
msgstr "Az e-mail cím már foglalt!"
-#: .\cookbook\forms.py:376
+#: .\cookbook\forms.py:393
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
@@ -265,15 +265,15 @@ msgstr ""
"Az e-mail cím megadása nem kötelező, de ha van, a meghívó linket elküldi a "
"felhasználónak."
-#: .\cookbook\forms.py:391
+#: .\cookbook\forms.py:408
msgid "Name already taken."
msgstr "A név már foglalt."
-#: .\cookbook\forms.py:402
+#: .\cookbook\forms.py:419
msgid "Accept Terms and Privacy"
msgstr "Feltételek és adatvédelem elfogadása"
-#: .\cookbook\forms.py:434
+#: .\cookbook\forms.py:451
msgid ""
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
"g. low values mean more typos are ignored)."
@@ -282,7 +282,7 @@ msgstr ""
"párosítást használ (pl. az alacsony értékek azt jelentik, hogy több gépelési "
"hibát figyelmen kívül hagynak)."
-#: .\cookbook\forms.py:444
+#: .\cookbook\forms.py:461
#, fuzzy
#| msgid ""
#| "Select type method of search. Click here "
@@ -294,7 +294,7 @@ msgstr ""
"Válassza ki a keresés típusát. Kattintson ide "
"a lehetőségek teljes leírásáért."
-#: .\cookbook\forms.py:445
+#: .\cookbook\forms.py:462
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
@@ -302,7 +302,7 @@ msgstr ""
"A receptek szerkesztése és importálása során az egységek, kulcsszavak és "
"összetevők bizonytalan megfeleltetése."
-#: .\cookbook\forms.py:447
+#: .\cookbook\forms.py:464
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
@@ -310,7 +310,7 @@ msgstr ""
"Az ékezetek figyelmen kívül hagyásával keresendő mezők. Ennek az opciónak a "
"kiválasztása javíthatja vagy ronthatja a keresés minőségét a nyelvtől függően"
-#: .\cookbook\forms.py:449
+#: .\cookbook\forms.py:466
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
@@ -318,7 +318,7 @@ msgstr ""
"Részleges egyezések keresésére szolgáló mezők. (pl. a 'Pie' keresése a "
"'pie' és a 'piece' és a 'soapie' kifejezéseket adja vissza.)"
-#: .\cookbook\forms.py:451
+#: .\cookbook\forms.py:468
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
@@ -326,7 +326,7 @@ msgstr ""
"Mezők a szó eleji egyezések kereséséhez. (pl. a 'sa' keresés a 'salad' és a "
"'sandwich' kifejezéseket adja vissza)"
-#: .\cookbook\forms.py:453
+#: .\cookbook\forms.py:470
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."
@@ -335,7 +335,7 @@ msgstr ""
"'recipe' szót.) Megjegyzés: ez az opció ütközik a 'web' és a 'raw' keresési "
"módszerekkel."
-#: .\cookbook\forms.py:455
+#: .\cookbook\forms.py:472
msgid ""
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
"only function with fulltext fields."
@@ -343,37 +343,37 @@ msgstr ""
"Mezők a teljes szöveges kereséshez. Megjegyzés: A 'web', 'phrase' és 'raw' "
"keresési módszerek csak teljes szöveges mezőkkel működnek."
-#: .\cookbook\forms.py:459
+#: .\cookbook\forms.py:476
msgid "Search Method"
msgstr "Keresési módszer"
-#: .\cookbook\forms.py:460
+#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
msgstr "Bizonytalan keresések"
-#: .\cookbook\forms.py:461
+#: .\cookbook\forms.py:478
msgid "Ignore Accent"
msgstr "Ékezetek ignorálása"
-#: .\cookbook\forms.py:462
+#: .\cookbook\forms.py:479
msgid "Partial Match"
msgstr "Részleges találat"
-#: .\cookbook\forms.py:463
+#: .\cookbook\forms.py:480
#, fuzzy
#| msgid "Starts Wtih"
msgid "Starts With"
msgstr "Kezdődik a következővel"
-#: .\cookbook\forms.py:464
+#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
msgstr "Bizonytalan keresés"
-#: .\cookbook\forms.py:465
+#: .\cookbook\forms.py:482
msgid "Full Text"
msgstr "Teljes szöveg"
-#: .\cookbook\forms.py:490
+#: .\cookbook\forms.py:507
msgid ""
"Users will see all items you add to your shopping list. They must add you "
"to see items on their list."
@@ -382,7 +382,7 @@ msgstr ""
"Ahhoz, hogy láthassák a saját listájukon szereplő tételeket, hozzá kell "
"adniuk téged."
-#: .\cookbook\forms.py:496
+#: .\cookbook\forms.py:513
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
@@ -390,7 +390,7 @@ msgstr ""
"Amikor étkezési tervet ad hozzá a bevásárlólistához (kézzel vagy "
"automatikusan), vegye fel az összes kapcsolódó receptet."
-#: .\cookbook\forms.py:497
+#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
@@ -398,96 +398,96 @@ 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."
-#: .\cookbook\forms.py:498
+#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
msgstr "A bevásárlólista bejegyzés késleltetésének alapértelmezett ideje."
-#: .\cookbook\forms.py:499
+#: .\cookbook\forms.py:516
msgid "Filter shopping list to only include supermarket categories."
msgstr ""
"Szűrje a bevásárlólistát úgy, hogy csak a szupermarket kategóriákat "
"tartalmazza."
-#: .\cookbook\forms.py:500
+#: .\cookbook\forms.py:517
msgid "Days of recent shopping list entries to display."
msgstr "A legutóbbi bevásárlólista bejegyzések megjelenítendő napjai."
-#: .\cookbook\forms.py:501
+#: .\cookbook\forms.py:518
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr ""
"Jelölje meg a \" Kéznél van\" jelölést, ha a bevásárlólistáról kipipálta az "
"élelmiszert."
-#: .\cookbook\forms.py:502
+#: .\cookbook\forms.py:519
msgid "Delimiter to use for CSV exports."
msgstr "A CSV exportáláshoz használandó elválasztójel."
-#: .\cookbook\forms.py:503
+#: .\cookbook\forms.py:520
msgid "Prefix to add when copying list to the clipboard."
msgstr "A lista vágólapra másolásakor hozzáadandó előtag."
-#: .\cookbook\forms.py:507
+#: .\cookbook\forms.py:524
msgid "Share Shopping List"
msgstr "Bevásárlólista megosztása"
-#: .\cookbook\forms.py:508
+#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr "Automatikus szinkronizálás"
-#: .\cookbook\forms.py:509
+#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr "Automatikus étkezési terv hozzáadása"
-#: .\cookbook\forms.py:510
+#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
msgstr "Kéznél levő kihagyása"
-#: .\cookbook\forms.py:511
+#: .\cookbook\forms.py:528
msgid "Include Related"
msgstr "Tartalmazza a kapcsolódókat"
-#: .\cookbook\forms.py:512
+#: .\cookbook\forms.py:529
msgid "Default Delay Hours"
msgstr "Alapértelmezett késleltetési órák"
-#: .\cookbook\forms.py:513
+#: .\cookbook\forms.py:530
msgid "Filter to Supermarket"
msgstr "Szűrő a szupermarkethez"
-#: .\cookbook\forms.py:514
+#: .\cookbook\forms.py:531
msgid "Recent Days"
msgstr "Legutóbbi napok"
-#: .\cookbook\forms.py:515
+#: .\cookbook\forms.py:532
msgid "CSV Delimiter"
msgstr "CSV elválasztó"
-#: .\cookbook\forms.py:516
+#: .\cookbook\forms.py:533
msgid "List Prefix"
msgstr "Lista előtagja"
-#: .\cookbook\forms.py:517
+#: .\cookbook\forms.py:534
msgid "Auto On Hand"
msgstr "Automatikus Kéznél lévő"
-#: .\cookbook\forms.py:527
+#: .\cookbook\forms.py:544
msgid "Reset Food Inheritance"
msgstr "Élelmiszer-öröklés visszaállítása"
-#: .\cookbook\forms.py:528
+#: .\cookbook\forms.py:545
msgid "Reset all food to inherit the fields configured."
msgstr "Állítsa vissza az összes ételt, hogy örökölje a konfigurált mezőket."
-#: .\cookbook\forms.py:540
+#: .\cookbook\forms.py:557
msgid "Fields on food that should be inherited by default."
msgstr ""
"Az élelmiszerek azon mezői, amelyeket alapértelmezés szerint örökölni kell."
-#: .\cookbook\forms.py:541
+#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
msgstr "A receptek számának megjelenítése a keresési szűrőkön"
-#: .\cookbook\forms.py:542
+#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""
diff --git a/cookbook/locale/it/LC_MESSAGES/django.mo b/cookbook/locale/it/LC_MESSAGES/django.mo
index b9d421921..babc1b13d 100644
Binary files a/cookbook/locale/it/LC_MESSAGES/django.mo and b/cookbook/locale/it/LC_MESSAGES/django.mo differ
diff --git a/cookbook/locale/it/LC_MESSAGES/django.po b/cookbook/locale/it/LC_MESSAGES/django.po
index 1de1eee82..89bd5df86 100644
--- a/cookbook/locale/it/LC_MESSAGES/django.po
+++ b/cookbook/locale/it/LC_MESSAGES/django.po
@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-26 07:46+0200\n"
+"POT-Creation-Date: 2023-05-18 14:28+0200\n"
"PO-Revision-Date: 2023-04-29 07:55+0000\n"
"Last-Translator: Oliver Cervera /"
"remote.php/webdav/ è aggiunto automaticamente)"
-#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157
+#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr "Archiviazione"
-#: .\cookbook\forms.py:267
+#: .\cookbook\forms.py:284
msgid "Active"
msgstr "Attivo"
-#: .\cookbook\forms.py:273
+#: .\cookbook\forms.py:290
msgid "Search String"
msgstr "Stringa di Ricerca"
-#: .\cookbook\forms.py:300
+#: .\cookbook\forms.py:317
msgid "File ID"
msgstr "ID del File"
-#: .\cookbook\forms.py:322
+#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr "Devi fornire almeno una ricetta o un titolo."
-#: .\cookbook\forms.py:335
+#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"È possibile visualizzare l'elenco degli utenti predefiniti con cui "
"condividere le ricette nelle impostazioni."
-#: .\cookbook\forms.py:336
+#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the docs here"
@@ -247,15 +247,15 @@ msgstr ""
"Puoi usare markdown per formattare questo campo. Guarda la documentazione qui"
-#: .\cookbook\forms.py:362
+#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr "È stato raggiunto il numero massimo di utenti per questa istanza."
-#: .\cookbook\forms.py:368
+#: .\cookbook\forms.py:385
msgid "Email address already taken!"
msgstr "Questo indirizzo email è già in uso!"
-#: .\cookbook\forms.py:376
+#: .\cookbook\forms.py:393
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
@@ -263,15 +263,15 @@ msgstr ""
"Non è obbligatorio specificare l'indirizzo email, ma se presente verrà "
"utilizzato per mandare all'utente un link di invito."
-#: .\cookbook\forms.py:391
+#: .\cookbook\forms.py:408
msgid "Name already taken."
msgstr "Nome già in uso."
-#: .\cookbook\forms.py:402
+#: .\cookbook\forms.py:419
msgid "Accept Terms and Privacy"
msgstr "Accetta i Termini d'uso e Privacy"
-#: .\cookbook\forms.py:434
+#: .\cookbook\forms.py:451
msgid ""
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
"g. low values mean more typos are ignored)."
@@ -280,7 +280,7 @@ msgstr ""
"trigrammi (ad esempio, valori bassi significano che vengono ignorati più "
"errori di battitura)."
-#: .\cookbook\forms.py:444
+#: .\cookbook\forms.py:461
msgid ""
"Select type method of search. Click here for "
"full description of choices."
@@ -288,7 +288,7 @@ msgstr ""
"Seleziona il metodo di ricerca. Clicca qui "
"per avere maggiori informazioni."
-#: .\cookbook\forms.py:445
+#: .\cookbook\forms.py:462
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
@@ -296,7 +296,7 @@ msgstr ""
"Usa la corrispondenza vaga per unità, parole chiave e ingredienti durante la "
"modifica e l'importazione di ricette."
-#: .\cookbook\forms.py:447
+#: .\cookbook\forms.py:464
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
@@ -304,7 +304,7 @@ msgstr ""
"Campi da cercare ignorando gli accenti. A seconda alla lingua utilizzata, "
"questa opzione può migliorare o peggiorare la ricerca"
-#: .\cookbook\forms.py:449
+#: .\cookbook\forms.py:466
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
@@ -312,7 +312,7 @@ msgstr ""
"Campi da cercare con corrispondenza parziale. (ad esempio, cercando 'Torta' "
"verranno mostrati 'torta', 'tortino' e 'contorta')"
-#: .\cookbook\forms.py:451
+#: .\cookbook\forms.py:468
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
@@ -320,7 +320,7 @@ msgstr ""
"Campi da cercare all'inizio di parole corrispondenti (es. cercando per 'ins' "
"mostrerà 'insalata' e 'insaccati')"
-#: .\cookbook\forms.py:453
+#: .\cookbook\forms.py:470
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."
@@ -329,7 +329,7 @@ msgstr ""
"verrà mostrato 'ricetta'). Nota: questa opzione non è compatibile con la "
"ricerca 'web' o 'raw'."
-#: .\cookbook\forms.py:455
+#: .\cookbook\forms.py:472
msgid ""
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
"only function with fulltext fields."
@@ -337,35 +337,35 @@ msgstr ""
"Campi per la ricerca full-text. Nota: i metodi di ricerca 'web', 'frase' e "
"'raw' funzionano solo con i campi full-text."
-#: .\cookbook\forms.py:459
+#: .\cookbook\forms.py:476
msgid "Search Method"
msgstr "Metodo di ricerca"
-#: .\cookbook\forms.py:460
+#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
msgstr "Ricerche vaghe"
-#: .\cookbook\forms.py:461
+#: .\cookbook\forms.py:478
msgid "Ignore Accent"
msgstr "Ignora accento"
-#: .\cookbook\forms.py:462
+#: .\cookbook\forms.py:479
msgid "Partial Match"
msgstr "Corrispondenza parziale"
-#: .\cookbook\forms.py:463
+#: .\cookbook\forms.py:480
msgid "Starts With"
msgstr "Inizia con"
-#: .\cookbook\forms.py:464
+#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
msgstr "Ricerca vaga"
-#: .\cookbook\forms.py:465
+#: .\cookbook\forms.py:482
msgid "Full Text"
msgstr "Full Text"
-#: .\cookbook\forms.py:490
+#: .\cookbook\forms.py:507
msgid ""
"Users will see all items you add to your shopping list. They must add you "
"to see items on their list."
@@ -373,7 +373,7 @@ msgstr ""
"Gli utenti potranno vedere tutti gli elementi che aggiungi alla tua lista "
"della spesa. Devono aggiungerti per vedere gli elementi nella loro lista."
-#: .\cookbook\forms.py:496
+#: .\cookbook\forms.py:513
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
@@ -381,7 +381,7 @@ msgstr ""
"Quando si aggiunge un piano alimentare alla lista della spesa (manualmente o "
"automaticamente), includi tutte le ricette correlate."
-#: .\cookbook\forms.py:497
+#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
@@ -389,97 +389,97 @@ msgstr ""
"Quando si aggiunge un piano alimentare alla lista della spesa (manualmente o "
"automaticamente), escludi gli ingredienti già disponibili."
-#: .\cookbook\forms.py:498
+#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
msgstr ""
"Il numero predefinito di ore per ritardare l'inserimento di una lista della "
"spesa."
-#: .\cookbook\forms.py:499
+#: .\cookbook\forms.py:516
msgid "Filter shopping list to only include supermarket categories."
msgstr ""
"Filtra la lista della spesa per includere solo categorie dei supermercati."
-#: .\cookbook\forms.py:500
+#: .\cookbook\forms.py:517
msgid "Days of recent shopping list entries to display."
msgstr "Giorni di visualizzazione di voci recenti della lista della spesa."
-#: .\cookbook\forms.py:501
+#: .\cookbook\forms.py:518
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr ""
"Contrassegna gli alimenti come 'Disponibili' quando spuntati dalla lista "
"della spesa."
-#: .\cookbook\forms.py:502
+#: .\cookbook\forms.py:519
msgid "Delimiter to use for CSV exports."
msgstr "Delimitatore usato per le esportazioni CSV."
-#: .\cookbook\forms.py:503
+#: .\cookbook\forms.py:520
msgid "Prefix to add when copying list to the clipboard."
msgstr "Prefisso da aggiungere quando si copia una lista negli appunti."
-#: .\cookbook\forms.py:507
+#: .\cookbook\forms.py:524
msgid "Share Shopping List"
msgstr "Condividi lista della spesa"
-#: .\cookbook\forms.py:508
+#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr "Sincronizzazione automatica"
-#: .\cookbook\forms.py:509
+#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr "Aggiungi automaticamente al piano alimentare"
-#: .\cookbook\forms.py:510
+#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
msgstr "Escludi Disponibile"
-#: .\cookbook\forms.py:511
+#: .\cookbook\forms.py:528
msgid "Include Related"
msgstr "Includi correlati"
-#: .\cookbook\forms.py:512
+#: .\cookbook\forms.py:529
msgid "Default Delay Hours"
msgstr "Ore di ritardo predefinite"
-#: .\cookbook\forms.py:513
+#: .\cookbook\forms.py:530
msgid "Filter to Supermarket"
msgstr "Filtra per supermercato"
-#: .\cookbook\forms.py:514
+#: .\cookbook\forms.py:531
msgid "Recent Days"
msgstr "Giorni recenti"
-#: .\cookbook\forms.py:515
+#: .\cookbook\forms.py:532
msgid "CSV Delimiter"
msgstr "Delimitatore CSV"
-#: .\cookbook\forms.py:516
+#: .\cookbook\forms.py:533
msgid "List Prefix"
msgstr "Prefisso lista"
-#: .\cookbook\forms.py:517
+#: .\cookbook\forms.py:534
msgid "Auto On Hand"
msgstr "Disponibilità automatica"
-#: .\cookbook\forms.py:527
+#: .\cookbook\forms.py:544
msgid "Reset Food Inheritance"
msgstr "Ripristina Eredità Alimenti"
-#: .\cookbook\forms.py:528
+#: .\cookbook\forms.py:545
msgid "Reset all food to inherit the fields configured."
msgstr "Ripristina tutti gli alimenti per ereditare i campi configurati."
-#: .\cookbook\forms.py:540
+#: .\cookbook\forms.py:557
msgid "Fields on food that should be inherited by default."
msgstr ""
"Campi su alimenti che devono essere ereditati per impostazione predefinita."
-#: .\cookbook\forms.py:541
+#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
msgstr "Mostra il conteggio delle ricette nei filtri di ricerca"
-#: .\cookbook\forms.py:542
+#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""
"Usare la forma plurale per le unità e gli alimenti all'interno di questo "
diff --git a/cookbook/locale/lv/LC_MESSAGES/django.po b/cookbook/locale/lv/LC_MESSAGES/django.po
index d96bc724f..8a5e7295e 100644
--- a/cookbook/locale/lv/LC_MESSAGES/django.po
+++ b/cookbook/locale/lv/LC_MESSAGES/django.po
@@ -10,7 +10,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-26 07:46+0200\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 /"
"remote.php/webdav/ wordt automatisch toegevoegd.)"
-#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157
+#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr "Opslag"
-#: .\cookbook\forms.py:267
+#: .\cookbook\forms.py:284
msgid "Active"
msgstr "Actief"
-#: .\cookbook\forms.py:273
+#: .\cookbook\forms.py:290
msgid "Search String"
msgstr "Zoekopdracht"
-#: .\cookbook\forms.py:300
+#: .\cookbook\forms.py:317
msgid "File ID"
msgstr "Bestands ID"
-#: .\cookbook\forms.py:322
+#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr "Je moet minimaal één recept of titel te specificeren."
-#: .\cookbook\forms.py:335
+#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"Je kan in de instellingen standaard gebruikers in stellen om de recepten met "
"te delen."
-#: .\cookbook\forms.py:336
+#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the docs here"
@@ -243,15 +243,15 @@ msgstr ""
"Je kunt markdown gebruiken om dit veld te op te maken. Bekijk de documentatie hier"
-#: .\cookbook\forms.py:362
+#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr "Maximum aantal gebruikers voor deze ruimte bereikt."
-#: .\cookbook\forms.py:368
+#: .\cookbook\forms.py:385
msgid "Email address already taken!"
msgstr "E-mailadres reeds in gebruik!"
-#: .\cookbook\forms.py:376
+#: .\cookbook\forms.py:393
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
@@ -259,15 +259,15 @@ msgstr ""
"Een e-mailadres is niet vereist, maar indien aanwezig zal de "
"uitnodigingslink naar de gebruiker worden gestuurd."
-#: .\cookbook\forms.py:391
+#: .\cookbook\forms.py:408
msgid "Name already taken."
msgstr "Naam reeds in gebruik."
-#: .\cookbook\forms.py:402
+#: .\cookbook\forms.py:419
msgid "Accept Terms and Privacy"
msgstr "Accepteer voorwaarden"
-#: .\cookbook\forms.py:434
+#: .\cookbook\forms.py:451
msgid ""
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
"g. low values mean more typos are ignored)."
@@ -275,7 +275,7 @@ msgstr ""
"Bepaalt hoe 'fuzzy' een zoekopdracht is als het trigram vergelijken gebruikt "
"(lage waarden betekenen bijvoorbeeld dat meer typefouten genegeerd worden)."
-#: .\cookbook\forms.py:444
+#: .\cookbook\forms.py:461
msgid ""
"Select type method of search. Click here for "
"full description of choices."
@@ -283,7 +283,7 @@ msgstr ""
"Selecteer zoekmethode. Klik hier voor een "
"beschrijving van de keuzes."
-#: .\cookbook\forms.py:445
+#: .\cookbook\forms.py:462
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
@@ -291,7 +291,7 @@ msgstr ""
"Gebruik 'fuzzy' koppelen bij eenheden, etiketten en ingrediënten bij "
"bewerken en importeren van recepten."
-#: .\cookbook\forms.py:447
+#: .\cookbook\forms.py:464
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
@@ -300,7 +300,7 @@ msgstr ""
"deze optie kan de zoekkwaliteit afhankelijk van de taal, zowel verbeteren "
"als verslechteren"
-#: .\cookbook\forms.py:449
+#: .\cookbook\forms.py:466
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
@@ -308,7 +308,7 @@ msgstr ""
"Velden doorzoeken op gedeelde overeenkomsten. (zoeken op 'Appel' vindt "
"'appel', 'aardappel' en 'appelsap')"
-#: .\cookbook\forms.py:451
+#: .\cookbook\forms.py:468
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
@@ -316,7 +316,7 @@ msgstr ""
"Velden doorzoeken op overeenkomsten aan het begin van het woord. (zoeken op "
"'sa' vindt 'salade' en 'sandwich')"
-#: .\cookbook\forms.py:453
+#: .\cookbook\forms.py:470
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."
@@ -324,7 +324,7 @@ msgstr ""
"Velden 'fuzzy' doorzoeken. (zoeken op 'recetp' vindt ook 'recept') Noot: "
"deze optie conflicteert met de zoekmethoden 'web' en 'raw'."
-#: .\cookbook\forms.py:455
+#: .\cookbook\forms.py:472
msgid ""
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
"only function with fulltext fields."
@@ -332,35 +332,35 @@ msgstr ""
"Velden doorzoeken op volledige tekst. Noot: Web, Zin en Raw zoekmethoden "
"werken alleen met volledige tekstvelden."
-#: .\cookbook\forms.py:459
+#: .\cookbook\forms.py:476
msgid "Search Method"
msgstr "Zoekmethode"
-#: .\cookbook\forms.py:460
+#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
msgstr "'Fuzzy' zoekopdrachten"
-#: .\cookbook\forms.py:461
+#: .\cookbook\forms.py:478
msgid "Ignore Accent"
msgstr "Negeer accent"
-#: .\cookbook\forms.py:462
+#: .\cookbook\forms.py:479
msgid "Partial Match"
msgstr "Gedeeltelijke overeenkomst"
-#: .\cookbook\forms.py:463
+#: .\cookbook\forms.py:480
msgid "Starts With"
msgstr "Begint met"
-#: .\cookbook\forms.py:464
+#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
msgstr "'Fuzzy' zoeken"
-#: .\cookbook\forms.py:465
+#: .\cookbook\forms.py:482
msgid "Full Text"
msgstr "Volledige tekst"
-#: .\cookbook\forms.py:490
+#: .\cookbook\forms.py:507
msgid ""
"Users will see all items you add to your shopping list. They must add you "
"to see items on their list."
@@ -368,7 +368,7 @@ msgstr ""
"Gebruikers zien alle items die je op je boodschappenlijst zet. Ze moeten "
"jou toevoegen om items op hun lijst te zien."
-#: .\cookbook\forms.py:496
+#: .\cookbook\forms.py:513
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
@@ -376,7 +376,7 @@ msgstr ""
"Als een maaltijdplan aan de boodschappenlijst toegevoegd wordt (handmatig of "
"automatisch), neem dan alle recepten op."
-#: .\cookbook\forms.py:497
+#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
@@ -384,94 +384,94 @@ msgstr ""
"Als een maaltijdplan aan de boodschappenlijst toegevoegd wordt (handmatig of "
"automatisch), sluit ingrediënten die op voorraad zijn dan uit."
-#: .\cookbook\forms.py:498
+#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
msgstr "Standaard aantal uren om een boodschappenlijst item te vertragen."
-#: .\cookbook\forms.py:499
+#: .\cookbook\forms.py:516
msgid "Filter shopping list to only include supermarket categories."
msgstr "Filter boodschappenlijst om alleen supermarktcategorieën te bevatten."
-#: .\cookbook\forms.py:500
+#: .\cookbook\forms.py:517
msgid "Days of recent shopping list entries to display."
msgstr "Dagen van recente boodschappenlijst items weer te geven."
-#: .\cookbook\forms.py:501
+#: .\cookbook\forms.py:518
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr ""
"Markeer eten 'Op voorraad' wanneer het van het boodschappenlijstje is "
"afgevinkt."
-#: .\cookbook\forms.py:502
+#: .\cookbook\forms.py:519
msgid "Delimiter to use for CSV exports."
msgstr "Scheidingsteken te gebruiken voor CSV exports."
-#: .\cookbook\forms.py:503
+#: .\cookbook\forms.py:520
msgid "Prefix to add when copying list to the clipboard."
msgstr ""
"Toe te voegen Voorvoegsel bij het kopiëren van een lijst naar het klembord."
-#: .\cookbook\forms.py:507
+#: .\cookbook\forms.py:524
msgid "Share Shopping List"
msgstr "Deel boodschappenlijst"
-#: .\cookbook\forms.py:508
+#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr "Autosync"
-#: .\cookbook\forms.py:509
+#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr "Voeg maaltijdplan automatisch toe"
-#: .\cookbook\forms.py:510
+#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
msgstr "Sluit op voorraad uit"
-#: .\cookbook\forms.py:511
+#: .\cookbook\forms.py:528
msgid "Include Related"
msgstr "Neem gerelateerde op"
-#: .\cookbook\forms.py:512
+#: .\cookbook\forms.py:529
msgid "Default Delay Hours"
msgstr "Standaard vertraging in uren"
-#: .\cookbook\forms.py:513
+#: .\cookbook\forms.py:530
msgid "Filter to Supermarket"
msgstr "Filter op supermarkt"
-#: .\cookbook\forms.py:514
+#: .\cookbook\forms.py:531
msgid "Recent Days"
msgstr "Afgelopen dagen"
-#: .\cookbook\forms.py:515
+#: .\cookbook\forms.py:532
msgid "CSV Delimiter"
msgstr "CSV scheidingsteken"
-#: .\cookbook\forms.py:516
+#: .\cookbook\forms.py:533
msgid "List Prefix"
msgstr "Lijst voorvoegsel"
-#: .\cookbook\forms.py:517
+#: .\cookbook\forms.py:534
msgid "Auto On Hand"
msgstr "Auto op voorraad"
-#: .\cookbook\forms.py:527
+#: .\cookbook\forms.py:544
msgid "Reset Food Inheritance"
msgstr "Herstel Ingrediënt overname"
-#: .\cookbook\forms.py:528
+#: .\cookbook\forms.py:545
msgid "Reset all food to inherit the fields configured."
msgstr "Herstel alle ingrediënten om de geconfigureerde velden over te nemen."
-#: .\cookbook\forms.py:540
+#: .\cookbook\forms.py:557
msgid "Fields on food that should be inherited by default."
msgstr "Velden van ingrediënten die standaard overgenomen moeten worden."
-#: .\cookbook\forms.py:541
+#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
msgstr "Toon recepten teller bij zoekfilters"
-#: .\cookbook\forms.py:542
+#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr "Gebruik de meervoudsvorm voor eenheden en voedsel in deze ruimte."
diff --git a/cookbook/locale/pt/LC_MESSAGES/django.po b/cookbook/locale/pt/LC_MESSAGES/django.po
index b4108bbaa..192d465c1 100644
--- a/cookbook/locale/pt/LC_MESSAGES/django.po
+++ b/cookbook/locale/pt/LC_MESSAGES/django.po
@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-26 07:46+0200\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 /"
"remote.php/webdav/é adicionado automaticamente). "
-#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157
+#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr "Armazenamento"
-#: .\cookbook\forms.py:267
+#: .\cookbook\forms.py:284
msgid "Active"
msgstr "Ativo"
-#: .\cookbook\forms.py:273
+#: .\cookbook\forms.py:290
msgid "Search String"
msgstr "Procurar"
-#: .\cookbook\forms.py:300
+#: .\cookbook\forms.py:317
msgid "File ID"
msgstr "ID the ficheiro"
-#: .\cookbook\forms.py:322
+#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr "É necessário inserir uma receita ou um título."
-#: .\cookbook\forms.py:335
+#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"É possível escolher os utilizadores com quem partilhar receitas por defeitos "
"nas definições."
-#: .\cookbook\forms.py:336
+#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the docs here"
@@ -243,15 +243,15 @@ msgstr ""
"É possível utilizar markdown para editar este campo. Documentação disponível aqui"
-#: .\cookbook\forms.py:362
+#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr "Número máximo de utilizadores alcançado."
-#: .\cookbook\forms.py:368
+#: .\cookbook\forms.py:385
msgid "Email address already taken!"
msgstr "Endereço email já utilizado!"
-#: .\cookbook\forms.py:376
+#: .\cookbook\forms.py:393
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
@@ -259,15 +259,15 @@ msgstr ""
"Um endereço de email não é obrigatório mas se fornecido será enviada uma "
"mensagem ao utilizador."
-#: .\cookbook\forms.py:391
+#: .\cookbook\forms.py:408
msgid "Name already taken."
msgstr "Nome já existente."
-#: .\cookbook\forms.py:402
+#: .\cookbook\forms.py:419
msgid "Accept Terms and Privacy"
msgstr "Aceitar Termos e Condições"
-#: .\cookbook\forms.py:434
+#: .\cookbook\forms.py:451
msgid ""
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
"g. low values mean more typos are ignored)."
@@ -276,7 +276,7 @@ msgstr ""
"de semelhança de trigrama (valores mais baixos significam que mais erros são "
"ignorados)."
-#: .\cookbook\forms.py:444
+#: .\cookbook\forms.py:461
#, fuzzy
#| msgid ""
#| "Select type method of search. Click here "
@@ -288,7 +288,7 @@ msgstr ""
"Selecionar o método de pesquisa. Uma descrição completa das opções pode ser "
"encontrada aqui."
-#: .\cookbook\forms.py:445
+#: .\cookbook\forms.py:462
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
@@ -296,7 +296,7 @@ msgstr ""
"Utilizar correspondência difusa em unidades, palavras-chave e ingredientes "
"ao editar e importar receitas."
-#: .\cookbook\forms.py:447
+#: .\cookbook\forms.py:464
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
@@ -304,171 +304,171 @@ msgstr ""
"Campos de pesquisa que ignoram pontuação. Esta opção pode aumentar ou "
"diminuir a qualidade de pesquisa dependendo da língua em uso"
-#: .\cookbook\forms.py:449
+#: .\cookbook\forms.py:466
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
msgstr ""
-#: .\cookbook\forms.py:451
+#: .\cookbook\forms.py:468
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
msgstr ""
-#: .\cookbook\forms.py:453
+#: .\cookbook\forms.py:470
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:455
+#: .\cookbook\forms.py:472
msgid ""
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
"only function with fulltext fields."
msgstr ""
-#: .\cookbook\forms.py:459
+#: .\cookbook\forms.py:476
#, fuzzy
#| msgid "Search"
msgid "Search Method"
msgstr "Procurar"
-#: .\cookbook\forms.py:460
+#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
msgstr ""
-#: .\cookbook\forms.py:461
+#: .\cookbook\forms.py:478
msgid "Ignore Accent"
msgstr ""
-#: .\cookbook\forms.py:462
+#: .\cookbook\forms.py:479
msgid "Partial Match"
msgstr ""
-#: .\cookbook\forms.py:463
+#: .\cookbook\forms.py:480
msgid "Starts With"
msgstr ""
-#: .\cookbook\forms.py:464
+#: .\cookbook\forms.py:481
#, fuzzy
#| msgid "Search"
msgid "Fuzzy Search"
msgstr "Procurar"
-#: .\cookbook\forms.py:465
+#: .\cookbook\forms.py:482
#, fuzzy
#| msgid "Text"
msgid "Full Text"
msgstr "Texto"
-#: .\cookbook\forms.py:490
+#: .\cookbook\forms.py:507
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:496
+#: .\cookbook\forms.py:513
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
msgstr ""
-#: .\cookbook\forms.py:497
+#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
msgstr ""
-#: .\cookbook\forms.py:498
+#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
msgstr ""
-#: .\cookbook\forms.py:499
+#: .\cookbook\forms.py:516
msgid "Filter shopping list to only include supermarket categories."
msgstr ""
-#: .\cookbook\forms.py:500
+#: .\cookbook\forms.py:517
msgid "Days of recent shopping list entries to display."
msgstr ""
-#: .\cookbook\forms.py:501
+#: .\cookbook\forms.py:518
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr ""
-#: .\cookbook\forms.py:502
+#: .\cookbook\forms.py:519
msgid "Delimiter to use for CSV exports."
msgstr ""
-#: .\cookbook\forms.py:503
+#: .\cookbook\forms.py:520
msgid "Prefix to add when copying list to the clipboard."
msgstr ""
-#: .\cookbook\forms.py:507
+#: .\cookbook\forms.py:524
#, fuzzy
#| msgid "Shopping"
msgid "Share Shopping List"
msgstr "Compras"
-#: .\cookbook\forms.py:508
+#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr ""
-#: .\cookbook\forms.py:509
+#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr ""
-#: .\cookbook\forms.py:510
+#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
msgstr ""
-#: .\cookbook\forms.py:511
+#: .\cookbook\forms.py:528
msgid "Include Related"
msgstr ""
-#: .\cookbook\forms.py:512
+#: .\cookbook\forms.py:529
msgid "Default Delay Hours"
msgstr ""
-#: .\cookbook\forms.py:513
+#: .\cookbook\forms.py:530
msgid "Filter to Supermarket"
msgstr ""
-#: .\cookbook\forms.py:514
+#: .\cookbook\forms.py:531
msgid "Recent Days"
msgstr ""
-#: .\cookbook\forms.py:515
+#: .\cookbook\forms.py:532
msgid "CSV Delimiter"
msgstr ""
-#: .\cookbook\forms.py:516
+#: .\cookbook\forms.py:533
msgid "List Prefix"
msgstr ""
-#: .\cookbook\forms.py:517
+#: .\cookbook\forms.py:534
msgid "Auto On Hand"
msgstr ""
-#: .\cookbook\forms.py:527
+#: .\cookbook\forms.py:544
msgid "Reset Food Inheritance"
msgstr ""
-#: .\cookbook\forms.py:528
+#: .\cookbook\forms.py:545
msgid "Reset all food to inherit the fields configured."
msgstr ""
-#: .\cookbook\forms.py:540
+#: .\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."
-#: .\cookbook\forms.py:541
+#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
msgstr "Mostrar receitas recentes na página de pesquisa"
-#: .\cookbook\forms.py:542
+#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""
diff --git a/cookbook/locale/rn/LC_MESSAGES/django.po b/cookbook/locale/rn/LC_MESSAGES/django.po
index 3ddfac8a0..ad65d22b3 100644
--- a/cookbook/locale/rn/LC_MESSAGES/django.po
+++ b/cookbook/locale/rn/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-26 07:46+0200\n"
+"POT-Creation-Date: 2023-05-18 14:28+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME /remote."
"php/webdav/ is added automatically)"
msgstr ""
-#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157
+#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr ""
-#: .\cookbook\forms.py:267
+#: .\cookbook\forms.py:284
msgid "Active"
msgstr ""
-#: .\cookbook\forms.py:273
+#: .\cookbook\forms.py:290
msgid "Search String"
msgstr ""
-#: .\cookbook\forms.py:300
+#: .\cookbook\forms.py:317
msgid "File ID"
msgstr ""
-#: .\cookbook\forms.py:322
+#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr ""
-#: .\cookbook\forms.py:335
+#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr ""
-#: .\cookbook\forms.py:336
+#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the docs here"
msgstr ""
-#: .\cookbook\forms.py:362
+#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr ""
-#: .\cookbook\forms.py:368
+#: .\cookbook\forms.py:385
msgid "Email address already taken!"
msgstr ""
-#: .\cookbook\forms.py:376
+#: .\cookbook\forms.py:393
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
msgstr ""
-#: .\cookbook\forms.py:391
+#: .\cookbook\forms.py:408
msgid "Name already taken."
msgstr ""
-#: .\cookbook\forms.py:402
+#: .\cookbook\forms.py:419
msgid "Accept Terms and Privacy"
msgstr ""
-#: .\cookbook\forms.py:434
+#: .\cookbook\forms.py:451
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:444
+#: .\cookbook\forms.py:461
msgid ""
"Select type method of search. Click here for "
"full description of choices."
msgstr ""
-#: .\cookbook\forms.py:445
+#: .\cookbook\forms.py:462
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
msgstr ""
-#: .\cookbook\forms.py:447
+#: .\cookbook\forms.py:464
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
msgstr ""
-#: .\cookbook\forms.py:449
+#: .\cookbook\forms.py:466
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
msgstr ""
-#: .\cookbook\forms.py:451
+#: .\cookbook\forms.py:468
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
msgstr ""
-#: .\cookbook\forms.py:453
+#: .\cookbook\forms.py:470
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:455
+#: .\cookbook\forms.py:472
msgid ""
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
"only function with fulltext fields."
msgstr ""
-#: .\cookbook\forms.py:459
+#: .\cookbook\forms.py:476
msgid "Search Method"
msgstr ""
-#: .\cookbook\forms.py:460
+#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
msgstr ""
-#: .\cookbook\forms.py:461
+#: .\cookbook\forms.py:478
msgid "Ignore Accent"
msgstr ""
-#: .\cookbook\forms.py:462
+#: .\cookbook\forms.py:479
msgid "Partial Match"
msgstr ""
-#: .\cookbook\forms.py:463
+#: .\cookbook\forms.py:480
msgid "Starts With"
msgstr ""
-#: .\cookbook\forms.py:464
+#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
msgstr ""
-#: .\cookbook\forms.py:465
+#: .\cookbook\forms.py:482
msgid "Full Text"
msgstr ""
-#: .\cookbook\forms.py:490
+#: .\cookbook\forms.py:507
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:496
+#: .\cookbook\forms.py:513
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
msgstr ""
-#: .\cookbook\forms.py:497
+#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
msgstr ""
-#: .\cookbook\forms.py:498
+#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
msgstr ""
-#: .\cookbook\forms.py:499
+#: .\cookbook\forms.py:516
msgid "Filter shopping list to only include supermarket categories."
msgstr ""
-#: .\cookbook\forms.py:500
+#: .\cookbook\forms.py:517
msgid "Days of recent shopping list entries to display."
msgstr ""
-#: .\cookbook\forms.py:501
+#: .\cookbook\forms.py:518
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr ""
-#: .\cookbook\forms.py:502
+#: .\cookbook\forms.py:519
msgid "Delimiter to use for CSV exports."
msgstr ""
-#: .\cookbook\forms.py:503
+#: .\cookbook\forms.py:520
msgid "Prefix to add when copying list to the clipboard."
msgstr ""
-#: .\cookbook\forms.py:507
+#: .\cookbook\forms.py:524
msgid "Share Shopping List"
msgstr ""
-#: .\cookbook\forms.py:508
+#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr ""
-#: .\cookbook\forms.py:509
+#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr ""
-#: .\cookbook\forms.py:510
+#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
msgstr ""
-#: .\cookbook\forms.py:511
+#: .\cookbook\forms.py:528
msgid "Include Related"
msgstr ""
-#: .\cookbook\forms.py:512
+#: .\cookbook\forms.py:529
msgid "Default Delay Hours"
msgstr ""
-#: .\cookbook\forms.py:513
+#: .\cookbook\forms.py:530
msgid "Filter to Supermarket"
msgstr ""
-#: .\cookbook\forms.py:514
+#: .\cookbook\forms.py:531
msgid "Recent Days"
msgstr ""
-#: .\cookbook\forms.py:515
+#: .\cookbook\forms.py:532
msgid "CSV Delimiter"
msgstr ""
-#: .\cookbook\forms.py:516
+#: .\cookbook\forms.py:533
msgid "List Prefix"
msgstr ""
-#: .\cookbook\forms.py:517
+#: .\cookbook\forms.py:534
msgid "Auto On Hand"
msgstr ""
-#: .\cookbook\forms.py:527
+#: .\cookbook\forms.py:544
msgid "Reset Food Inheritance"
msgstr ""
-#: .\cookbook\forms.py:528
+#: .\cookbook\forms.py:545
msgid "Reset all food to inherit the fields configured."
msgstr ""
-#: .\cookbook\forms.py:540
+#: .\cookbook\forms.py:557
msgid "Fields on food that should be inherited by default."
msgstr ""
-#: .\cookbook\forms.py:541
+#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
msgstr ""
-#: .\cookbook\forms.py:542
+#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""
diff --git a/cookbook/locale/ro/LC_MESSAGES/django.mo b/cookbook/locale/ro/LC_MESSAGES/django.mo
index 5d9c9507c..980a56264 100644
Binary files a/cookbook/locale/ro/LC_MESSAGES/django.mo and b/cookbook/locale/ro/LC_MESSAGES/django.mo differ
diff --git a/cookbook/locale/ru/LC_MESSAGES/django.mo b/cookbook/locale/ru/LC_MESSAGES/django.mo
index eea0334b2..3e7acced3 100644
Binary files a/cookbook/locale/ru/LC_MESSAGES/django.mo and b/cookbook/locale/ru/LC_MESSAGES/django.mo differ
diff --git a/cookbook/locale/tr/LC_MESSAGES/django.po b/cookbook/locale/tr/LC_MESSAGES/django.po
index 7d24fb255..f90083aae 100644
--- a/cookbook/locale/tr/LC_MESSAGES/django.po
+++ b/cookbook/locale/tr/LC_MESSAGES/django.po
@@ -10,7 +10,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-26 07:46+0200\n"
+"POT-Creation-Date: 2023-05-18 14:28+0200\n"
"PO-Revision-Date: 2022-11-06 22:09+0000\n"
"Last-Translator: Gorkem /remote.php/webdav/ 会自"
"动添加)"
-#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157
+#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr "存储"
-#: .\cookbook\forms.py:267
+#: .\cookbook\forms.py:284
msgid "Active"
msgstr "活跃"
-#: .\cookbook\forms.py:273
+#: .\cookbook\forms.py:290
msgid "Search String"
msgstr "搜索字符串"
-#: .\cookbook\forms.py:300
+#: .\cookbook\forms.py:317
msgid "File ID"
msgstr "文件编号"
-#: .\cookbook\forms.py:322
+#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr "你必须至少提供一份菜谱或一个标题。"
-#: .\cookbook\forms.py:335
+#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr "你可以在设置中列出默认用户来分享菜谱。"
-#: .\cookbook\forms.py:336
+#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the docs here"
msgstr ""
"可以使用 Markdown 设置此字段格式。查看文档"
-#: .\cookbook\forms.py:362
+#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr "已达到该空间的最大用户数。"
-#: .\cookbook\forms.py:368
+#: .\cookbook\forms.py:385
msgid "Email address already taken!"
msgstr "电子邮件地址已被注册!"
-#: .\cookbook\forms.py:376
+#: .\cookbook\forms.py:393
msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
msgstr "电子邮件地址不是必需的,但如果存在,邀请链接将被发送给用户。"
-#: .\cookbook\forms.py:391
+#: .\cookbook\forms.py:408
msgid "Name already taken."
msgstr "名字已被占用。"
-#: .\cookbook\forms.py:402
+#: .\cookbook\forms.py:419
msgid "Accept Terms and Privacy"
msgstr "接受条款及隐私政策"
-#: .\cookbook\forms.py:434
+#: .\cookbook\forms.py:451
msgid ""
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
"g. low values mean more typos are ignored)."
@@ -254,7 +254,7 @@ msgstr ""
"确定使用三元图相似性匹配时搜索的模糊程度(例如,较低的值意味着忽略更多的打字"
"错误)。"
-#: .\cookbook\forms.py:444
+#: .\cookbook\forms.py:461
msgid ""
"Select type method of search. Click here for "
"full description of choices."
@@ -262,31 +262,31 @@ msgstr ""
"选择搜索类型方法。 点击此处 查看选项的完整说"
"明。"
-#: .\cookbook\forms.py:445
+#: .\cookbook\forms.py:462
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
msgstr "编辑和导入菜谱时,对单位、关键词和食材使用模糊匹配。"
-#: .\cookbook\forms.py:447
+#: .\cookbook\forms.py:464
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
msgstr "忽略搜索字段的重音。此选项会因语言差异导致搜索质量产生变化"
-#: .\cookbook\forms.py:449
+#: .\cookbook\forms.py:466
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
msgstr "用于搜索部分匹配的字段。(如搜索“Pie”会返回“pie”、“piece”和“soapie”)"
-#: .\cookbook\forms.py:451
+#: .\cookbook\forms.py:468
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
msgstr "用于搜索开头匹配的字段。(如搜索“sa”会返回“salad”和“sandwich”)"
-#: .\cookbook\forms.py:453
+#: .\cookbook\forms.py:470
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."
@@ -294,41 +294,41 @@ msgstr ""
"“模糊”搜索字段。(例如搜索“recpie”将会找到“recipe”。)注意:此选项将"
"与“web”和“raw”搜索方法冲突。"
-#: .\cookbook\forms.py:455
+#: .\cookbook\forms.py:472
msgid ""
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods "
"only function with fulltext fields."
msgstr "全文搜索字段。“web”、“phrase”和“raw”搜索方法仅适用于全文字段。"
-#: .\cookbook\forms.py:459
+#: .\cookbook\forms.py:476
msgid "Search Method"
msgstr "搜索方法"
-#: .\cookbook\forms.py:460
+#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
msgstr "模糊查找"
-#: .\cookbook\forms.py:461
+#: .\cookbook\forms.py:478
msgid "Ignore Accent"
msgstr "忽略重音"
-#: .\cookbook\forms.py:462
+#: .\cookbook\forms.py:479
msgid "Partial Match"
msgstr "部分匹配"
-#: .\cookbook\forms.py:463
+#: .\cookbook\forms.py:480
msgid "Starts With"
msgstr "起始于"
-#: .\cookbook\forms.py:464
+#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
msgstr "模糊搜索"
-#: .\cookbook\forms.py:465
+#: .\cookbook\forms.py:482
msgid "Full Text"
msgstr "全文"
-#: .\cookbook\forms.py:490
+#: .\cookbook\forms.py:507
msgid ""
"Users will see all items you add to your shopping list. They must add you "
"to see items on their list."
@@ -336,103 +336,103 @@ msgstr ""
"用户将看到你添加到购物清单中的所有商品。他们必须将你添加到列表才能看到他们清"
"单上的项目。"
-#: .\cookbook\forms.py:496
+#: .\cookbook\forms.py:513
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
msgstr "将膳食计划(手动或自动)添加到购物清单时,包括所有相关食谱。"
-#: .\cookbook\forms.py:497
+#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
msgstr "将膳食计划(手动或自动)添加到购物清单时,排除现有食材。"
-#: .\cookbook\forms.py:498
+#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
msgstr "延迟购物清单条目的默认小时数。"
-#: .\cookbook\forms.py:499
+#: .\cookbook\forms.py:516
msgid "Filter shopping list to only include supermarket categories."
msgstr "筛选购物清单仅包含超市分类。"
-#: .\cookbook\forms.py:500
+#: .\cookbook\forms.py:517
msgid "Days of recent shopping list entries to display."
msgstr "显示最近几天的购物清单列表。"
-#: .\cookbook\forms.py:501
+#: .\cookbook\forms.py:518
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr "在核对购物清单时,将食物标记为“入手”。"
-#: .\cookbook\forms.py:502
+#: .\cookbook\forms.py:519
msgid "Delimiter to use for CSV exports."
msgstr "用于 CSV 导出的分隔符。"
-#: .\cookbook\forms.py:503
+#: .\cookbook\forms.py:520
msgid "Prefix to add when copying list to the clipboard."
msgstr "将清单复制到剪贴板时要添加的前缀。"
-#: .\cookbook\forms.py:507
+#: .\cookbook\forms.py:524
msgid "Share Shopping List"
msgstr "分享购物清单"
-#: .\cookbook\forms.py:508
+#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr "自动同步"
-#: .\cookbook\forms.py:509
+#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr "自动添加膳食计划"
-#: .\cookbook\forms.py:510
+#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
msgstr "排除现有"
-#: .\cookbook\forms.py:511
+#: .\cookbook\forms.py:528
msgid "Include Related"
msgstr "包括相关"
-#: .\cookbook\forms.py:512
+#: .\cookbook\forms.py:529
msgid "Default Delay Hours"
msgstr "默认延迟时间"
-#: .\cookbook\forms.py:513
+#: .\cookbook\forms.py:530
msgid "Filter to Supermarket"
msgstr "按超市筛选"
-#: .\cookbook\forms.py:514
+#: .\cookbook\forms.py:531
msgid "Recent Days"
msgstr "最近几天"
-#: .\cookbook\forms.py:515
+#: .\cookbook\forms.py:532
msgid "CSV Delimiter"
msgstr "CSV 分隔符"
-#: .\cookbook\forms.py:516
+#: .\cookbook\forms.py:533
msgid "List Prefix"
msgstr "清单前缀"
-#: .\cookbook\forms.py:517
+#: .\cookbook\forms.py:534
msgid "Auto On Hand"
msgstr "自动入手"
-#: .\cookbook\forms.py:527
+#: .\cookbook\forms.py:544
msgid "Reset Food Inheritance"
msgstr "重置食物材料"
-#: .\cookbook\forms.py:528
+#: .\cookbook\forms.py:545
msgid "Reset all food to inherit the fields configured."
msgstr "重置所有食物以继承配置的字段。"
-#: .\cookbook\forms.py:540
+#: .\cookbook\forms.py:557
msgid "Fields on food that should be inherited by default."
msgstr "默认情况下应继承的食物上的字段。"
-#: .\cookbook\forms.py:541
+#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
msgstr "显示搜索筛选器上的食谱计数"
-#: .\cookbook\forms.py:542
+#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr "在此空间内使用复数形式表示单位和食物。"
diff --git a/cookbook/migrations/0189_property_propertytype_unitconversion_food_fdc_id_and_more.py b/cookbook/migrations/0189_property_propertytype_unitconversion_food_fdc_id_and_more.py
new file mode 100644
index 000000000..710b5b08c
--- /dev/null
+++ b/cookbook/migrations/0189_property_propertytype_unitconversion_food_fdc_id_and_more.py
@@ -0,0 +1,163 @@
+# Generated by Django 4.1.9 on 2023-05-25 13:05
+
+import cookbook.models
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django_prometheus.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('cookbook', '0188_space_no_sharing_limit'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Property',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('property_amount', models.DecimalField(decimal_places=4, default=0, max_digits=32)),
+ ],
+ bases=(models.Model, cookbook.models.PermissionModelMixin),
+ ),
+ migrations.CreateModel(
+ name='PropertyType',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=128)),
+ ('unit', models.CharField(blank=True, max_length=64, null=True)),
+ ('icon', models.CharField(blank=True, max_length=16, null=True)),
+ ('description', models.CharField(blank=True, max_length=512, null=True)),
+ ('category', models.CharField(blank=True, choices=[('NUTRITION', 'Nutrition'), ('ALLERGEN', 'Allergen'), ('PRICE', 'Price'), ('GOAL', 'Goal'), ('OTHER', 'Other')], max_length=64, null=True)),
+ ('open_data_slug', models.CharField(blank=True, default=None, max_length=128, null=True)),
+ ],
+ bases=(models.Model, cookbook.models.PermissionModelMixin),
+ ),
+ migrations.CreateModel(
+ name='UnitConversion',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('base_amount', models.DecimalField(decimal_places=16, default=0, max_digits=32)),
+ ('converted_amount', models.DecimalField(decimal_places=16, default=0, max_digits=32)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('open_data_slug', models.CharField(blank=True, default=None, max_length=128, null=True)),
+ ],
+ bases=(django_prometheus.models.ExportModelOperationsMixin('unit_conversion'), models.Model, cookbook.models.PermissionModelMixin),
+ ),
+ migrations.AddField(
+ model_name='food',
+ name='fdc_id',
+ field=models.CharField(blank=True, default=None, max_length=128, null=True),
+ ),
+ migrations.AddField(
+ model_name='food',
+ name='open_data_slug',
+ field=models.CharField(blank=True, default=None, max_length=128, null=True),
+ ),
+ migrations.AddField(
+ model_name='food',
+ name='preferred_shopping_unit',
+ field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='preferred_shopping_unit', to='cookbook.unit'),
+ ),
+ migrations.AddField(
+ model_name='food',
+ name='preferred_unit',
+ field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='preferred_unit', to='cookbook.unit'),
+ ),
+ migrations.AddField(
+ model_name='food',
+ name='properties_food_amount',
+ field=models.IntegerField(blank=True, default=100),
+ ),
+ migrations.AddField(
+ model_name='food',
+ name='properties_food_unit',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.unit'),
+ ),
+ migrations.AddField(
+ model_name='supermarket',
+ name='open_data_slug',
+ field=models.CharField(blank=True, default=None, max_length=128, null=True),
+ ),
+ migrations.AddField(
+ model_name='supermarketcategory',
+ name='open_data_slug',
+ field=models.CharField(blank=True, default=None, max_length=128, null=True),
+ ),
+ migrations.AddField(
+ model_name='unit',
+ name='base_unit',
+ field=models.TextField(blank=True, default=None, max_length=256, null=True),
+ ),
+ migrations.AddField(
+ model_name='unit',
+ name='open_data_slug',
+ field=models.CharField(blank=True, default=None, max_length=128, null=True),
+ ),
+ migrations.AddConstraint(
+ model_name='supermarketcategoryrelation',
+ constraint=models.UniqueConstraint(fields=('supermarket', 'category'), name='unique_sm_category_relation'),
+ ),
+ migrations.AddField(
+ model_name='unitconversion',
+ name='base_unit',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unit_conversion_base_relation', to='cookbook.unit'),
+ ),
+ migrations.AddField(
+ model_name='unitconversion',
+ name='converted_unit',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unit_conversion_converted_relation', to='cookbook.unit'),
+ ),
+ migrations.AddField(
+ model_name='unitconversion',
+ name='created_by',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AddField(
+ model_name='unitconversion',
+ name='food',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.food'),
+ ),
+ migrations.AddField(
+ model_name='unitconversion',
+ name='space',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
+ ),
+ migrations.AddField(
+ model_name='propertytype',
+ name='space',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
+ ),
+ migrations.AddField(
+ model_name='property',
+ name='property_type',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.propertytype'),
+ ),
+ migrations.AddField(
+ model_name='property',
+ name='space',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
+ ),
+ migrations.AddField(
+ model_name='food',
+ name='properties',
+ field=models.ManyToManyField(blank=True, to='cookbook.property'),
+ ),
+ migrations.AddField(
+ model_name='recipe',
+ name='properties',
+ field=models.ManyToManyField(blank=True, to='cookbook.property'),
+ ),
+ migrations.AddConstraint(
+ model_name='unitconversion',
+ constraint=models.UniqueConstraint(fields=('space', 'base_unit', 'converted_unit', 'food'), name='f_unique_conversion_per_space'),
+ ),
+ migrations.AddConstraint(
+ model_name='propertytype',
+ constraint=models.UniqueConstraint(fields=('space', 'name'), name='property_type_unique_name_per_space'),
+ ),
+ ]
diff --git a/cookbook/migrations/0190_auto_20230525_1506.py b/cookbook/migrations/0190_auto_20230525_1506.py
new file mode 100644
index 000000000..af34cdd6c
--- /dev/null
+++ b/cookbook/migrations/0190_auto_20230525_1506.py
@@ -0,0 +1,38 @@
+# Generated by Django 4.1.9 on 2023-05-25 13:06
+
+from django.db import migrations
+from django_scopes import scopes_disabled
+from gettext import gettext as _
+
+def migrate_old_nutrition_data(apps, schema_editor):
+ print('Transforming nutrition information, this might take a while on large databases')
+ with scopes_disabled():
+ PropertyType = apps.get_model('cookbook', 'PropertyType')
+ RecipeProperty = apps.get_model('cookbook', 'Property')
+ Recipe = apps.get_model('cookbook', 'Recipe')
+ Space = apps.get_model('cookbook', 'Space')
+
+ # TODO respect space
+ for s in Space.objects.all():
+ property_fat = PropertyType.objects.get_or_create(name=_('Fat'), unit=_('g'), space=s, )[0]
+ property_carbohydrates = PropertyType.objects.get_or_create(name=_('Carbohydrates'), unit=_('g'), space=s, )[0]
+ property_proteins = PropertyType.objects.get_or_create(name=_('Proteins'), unit=_('g'), space=s, )[0]
+ property_calories = PropertyType.objects.get_or_create(name=_('Calories'), unit=_('kcal'), space=s, )[0]
+
+ for r in Recipe.objects.filter(nutrition__isnull=False, space=s).all():
+ rp_fat = RecipeProperty.objects.create(property_type=property_fat, property_amount=r.nutrition.fats, space=s)
+ rp_carbohydrates = RecipeProperty.objects.create(property_type=property_carbohydrates, property_amount=r.nutrition.carbohydrates, space=s)
+ rp_proteins = RecipeProperty.objects.create(property_type=property_proteins, property_amount=r.nutrition.proteins, space=s)
+ rp_calories = RecipeProperty.objects.create(property_type=property_calories, property_amount=r.nutrition.calories, space=s)
+ r.properties.add(rp_fat, rp_carbohydrates, rp_proteins, rp_calories)
+ r.nutrition = None
+ r.save()
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cookbook', '0189_property_propertytype_unitconversion_food_fdc_id_and_more'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate_old_nutrition_data)
+ ]
diff --git a/cookbook/migrations/0191_foodproperty_property_import_food_id_and_more.py b/cookbook/migrations/0191_foodproperty_property_import_food_id_and_more.py
new file mode 100644
index 000000000..5b7d682bf
--- /dev/null
+++ b/cookbook/migrations/0191_foodproperty_property_import_food_id_and_more.py
@@ -0,0 +1,49 @@
+# Generated by Django 4.1.9 on 2023-06-20 13:07
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('cookbook', '0190_auto_20230525_1506'),
+ ]
+
+ operations = [
+ migrations.SeparateDatabaseAndState(
+ database_operations=[
+ migrations.RunSQL(
+ sql="ALTER TABLE cookbook_food_properties RENAME TO cookbook_foodproperty",
+ reverse_sql="ALTER TABLE cookbook_foodproperty RENAME TO cookbook_food_properties",
+ ),
+ ],
+ state_operations=[
+ migrations.CreateModel(
+ name='FoodProperty',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('food', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.food')),
+ ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.property')),
+ ],
+ ),
+ migrations.AlterField(
+ model_name='food',
+ name='properties',
+ field=models.ManyToManyField(blank=True, through='cookbook.FoodProperty', to='cookbook.property'),
+ ),
+ ]
+ ),
+ migrations.AddConstraint(
+ model_name='foodproperty',
+ constraint=models.UniqueConstraint(fields=('food', 'property'), name='property_unique_food'),
+ ),
+ migrations.AddField(
+ model_name='property',
+ name='import_food_id',
+ field=models.IntegerField(blank=True, null=True),
+ ),
+ migrations.AddConstraint(
+ model_name='property',
+ constraint=models.UniqueConstraint(fields=('space', 'property_type', 'import_food_id'), name='property_unique_import_food_per_space'),
+ ),
+ ]
diff --git a/cookbook/migrations/0192_food_food_unique_open_data_slug_per_space_and_more.py b/cookbook/migrations/0192_food_food_unique_open_data_slug_per_space_and_more.py
new file mode 100644
index 000000000..09865486c
--- /dev/null
+++ b/cookbook/migrations/0192_food_food_unique_open_data_slug_per_space_and_more.py
@@ -0,0 +1,37 @@
+# Generated by Django 4.1.9 on 2023-06-20 13:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cookbook', '0191_foodproperty_property_import_food_id_and_more'),
+ ]
+
+ operations = [
+ migrations.AddConstraint(
+ model_name='food',
+ constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='food_unique_open_data_slug_per_space'),
+ ),
+ migrations.AddConstraint(
+ model_name='propertytype',
+ constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='property_type_unique_open_data_slug_per_space'),
+ ),
+ migrations.AddConstraint(
+ model_name='supermarket',
+ constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='supermarket_unique_open_data_slug_per_space'),
+ ),
+ migrations.AddConstraint(
+ model_name='supermarketcategory',
+ constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='supermarket_category_unique_open_data_slug_per_space'),
+ ),
+ migrations.AddConstraint(
+ model_name='unit',
+ constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='unit_unique_open_data_slug_per_space'),
+ ),
+ migrations.AddConstraint(
+ model_name='unitconversion',
+ constraint=models.UniqueConstraint(fields=('space', 'open_data_slug'), name='unit_conversion_unique_open_data_slug_per_space'),
+ ),
+ ]
diff --git a/cookbook/migrations/0193_space_internal_note.py b/cookbook/migrations/0193_space_internal_note.py
new file mode 100644
index 000000000..d29d4f75d
--- /dev/null
+++ b/cookbook/migrations/0193_space_internal_note.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.9 on 2023-06-21 13:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cookbook', '0192_food_food_unique_open_data_slug_per_space_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='space',
+ name='internal_note',
+ field=models.TextField(blank=True, null=True),
+ ),
+ ]
diff --git a/cookbook/models.py b/cookbook/models.py
index 1732750b9..46af7e786 100644
--- a/cookbook/models.py
+++ b/cookbook/models.py
@@ -82,31 +82,34 @@ class TreeManager(MP_NodeManager):
# model.Manager get_or_create() is not compatible with MP_Tree
def get_or_create(self, *args, **kwargs):
kwargs['name'] = kwargs['name'].strip()
-
- if obj := self.filter(name__iexact=kwargs['name'], space=kwargs['space']).first():
- return obj, False
+ if hasattr(self, 'space'):
+ if obj := self.filter(name__iexact=kwargs['name'], space=kwargs['space']).first():
+ return obj, False
else:
- with scopes_disabled():
- try:
- defaults = kwargs.pop('defaults', None)
- if defaults:
- kwargs = {**kwargs, **defaults}
- # ManyToMany fields can't be set this way, so pop them out to save for later
- fields = [field.name for field in self.model._meta.get_fields() if issubclass(type(field), ManyToManyField)]
- many_to_many = {field: kwargs.pop(field) for field in list(kwargs) if field in fields}
- obj = self.model.add_root(**kwargs)
- for field in many_to_many:
- field_model = getattr(obj, field).model
- for related_obj in many_to_many[field]:
- if isinstance(related_obj, User):
- getattr(obj, field).add(field_model.objects.get(id=related_obj.id))
- else:
- getattr(obj, field).add(field_model.objects.get(**dict(related_obj)))
- return obj, True
- except IntegrityError as e:
- if 'Key (path)' in e.args[0]:
- self.model.fix_tree(fix_paths=True)
- return self.model.add_root(**kwargs), True
+ if obj := self.filter(name__iexact=kwargs['name']).first():
+ return obj, False
+
+ with scopes_disabled():
+ try:
+ defaults = kwargs.pop('defaults', None)
+ if defaults:
+ kwargs = {**kwargs, **defaults}
+ # ManyToMany fields can't be set this way, so pop them out to save for later
+ fields = [field.name for field in self.model._meta.get_fields() if issubclass(type(field), ManyToManyField)]
+ many_to_many = {field: kwargs.pop(field) for field in list(kwargs) if field in fields}
+ obj = self.model.add_root(**kwargs)
+ for field in many_to_many:
+ field_model = getattr(obj, field).model
+ for related_obj in many_to_many[field]:
+ if isinstance(related_obj, User):
+ getattr(obj, field).add(field_model.objects.get(id=related_obj.id))
+ else:
+ getattr(obj, field).add(field_model.objects.get(**dict(related_obj)))
+ return obj, True
+ except IntegrityError as e:
+ if 'Key (path)' in e.args[0]:
+ self.model.fix_tree(fix_paths=True)
+ return self.model.add_root(**kwargs), True
class TreeModel(MP_Node):
@@ -267,6 +270,8 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
show_facet_count = models.BooleanField(default=False)
+ internal_note = models.TextField(blank=True, null=True)
+
def safe_delete(self):
"""
Safely deletes a space by deleting all objects belonging to the space first and then deleting the space itself
@@ -454,6 +459,7 @@ class Sync(models.Model, PermissionModelMixin):
class SupermarketCategory(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
description = models.TextField(blank=True, null=True)
+ open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@@ -463,7 +469,8 @@ class SupermarketCategory(models.Model, PermissionModelMixin):
class Meta:
constraints = [
- models.UniqueConstraint(fields=['space', 'name'], name='smc_unique_name_per_space')
+ models.UniqueConstraint(fields=['space', 'name'], name='smc_unique_name_per_space'),
+ models.UniqueConstraint(fields=['space', 'open_data_slug'], name='supermarket_category_unique_open_data_slug_per_space')
]
@@ -471,6 +478,7 @@ class Supermarket(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
description = models.TextField(blank=True, null=True)
categories = models.ManyToManyField(SupermarketCategory, through='SupermarketCategoryRelation')
+ open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@@ -480,7 +488,8 @@ class Supermarket(models.Model, PermissionModelMixin):
class Meta:
constraints = [
- models.UniqueConstraint(fields=['space', 'name'], name='sm_unique_name_per_space')
+ models.UniqueConstraint(fields=['space', 'name'], name='sm_unique_name_per_space'),
+ models.UniqueConstraint(fields=['space', 'open_data_slug'], name='supermarket_unique_open_data_slug_per_space')
]
@@ -496,6 +505,9 @@ class SupermarketCategoryRelation(models.Model, PermissionModelMixin):
return 'supermarket', 'space'
class Meta:
+ constraints = [
+ models.UniqueConstraint(fields=['supermarket', 'category'], name='unique_sm_category_relation')
+ ]
ordering = ('order',)
@@ -534,6 +546,8 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
plural_name = models.CharField(max_length=128, null=True, blank=True, default=None)
description = models.TextField(blank=True, null=True)
+ base_unit = models.TextField(max_length=256, null=True, blank=True, default=None)
+ open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@@ -543,7 +557,8 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
class Meta:
constraints = [
- models.UniqueConstraint(fields=['space', 'name'], name='u_unique_name_per_space')
+ models.UniqueConstraint(fields=['space', 'name'], name='u_unique_name_per_space'),
+ models.UniqueConstraint(fields=['space', 'open_data_slug'], name='unit_unique_open_data_slug_per_space')
]
@@ -569,6 +584,15 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
substitute_children = models.BooleanField(default=False)
child_inherit_fields = models.ManyToManyField(FoodInheritField, blank=True, related_name='child_inherit')
+ properties = models.ManyToManyField("Property", blank=True, through='FoodProperty')
+ properties_food_amount = models.IntegerField(default=100, blank=True)
+ properties_food_unit = models.ForeignKey(Unit, on_delete=models.PROTECT, blank=True, null=True)
+
+ preferred_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_unit')
+ preferred_shopping_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_shopping_unit')
+ fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None)
+
+ open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space', _manager_class=TreeManager)
@@ -642,7 +666,8 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
class Meta:
constraints = [
- models.UniqueConstraint(fields=['space', 'name'], name='f_unique_name_per_space')
+ models.UniqueConstraint(fields=['space', 'name'], name='f_unique_name_per_space'),
+ models.UniqueConstraint(fields=['space', 'open_data_slug'], name='food_unique_open_data_slug_per_space')
]
indexes = (
Index(fields=['id']),
@@ -650,6 +675,32 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
)
+class UnitConversion(ExportModelOperationsMixin('unit_conversion'), models.Model, PermissionModelMixin):
+ base_amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
+ base_unit = models.ForeignKey('Unit', on_delete=models.CASCADE, related_name='unit_conversion_base_relation')
+ converted_amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
+ converted_unit = models.ForeignKey('Unit', on_delete=models.CASCADE, related_name='unit_conversion_converted_relation')
+
+ food = models.ForeignKey('Food', on_delete=models.CASCADE, null=True, blank=True)
+
+ created_by = models.ForeignKey(User, on_delete=models.PROTECT)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
+ space = models.ForeignKey(Space, on_delete=models.CASCADE)
+ objects = ScopedManager(space='space')
+
+ def __str__(self):
+ return f'{self.base_amount} {self.base_unit} -> {self.converted_amount} {self.converted_unit} {self.food}'
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(fields=['space', 'base_unit', 'converted_unit', 'food'], name='f_unique_conversion_per_space'),
+ models.UniqueConstraint(fields=['space', 'open_data_slug'], name='unit_conversion_unique_open_data_slug_per_space')
+ ]
+
+
class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, PermissionModelMixin):
# delete method on Food and Unit checks if they are part of a Recipe, if it is raises a ProtectedError instead of cascading the delete
food = models.ForeignKey(Food, on_delete=models.CASCADE, null=True, blank=True)
@@ -663,8 +714,6 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
order = models.IntegerField(default=0)
original_text = models.CharField(max_length=512, null=True, blank=True, default=None)
- original_text = models.CharField(max_length=512, null=True, blank=True, default=None)
-
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@@ -720,6 +769,64 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
indexes = (GinIndex(fields=["search_vector"]),)
+class PropertyType(models.Model, PermissionModelMixin):
+ NUTRITION = 'NUTRITION'
+ ALLERGEN = 'ALLERGEN'
+ PRICE = 'PRICE'
+ GOAL = 'GOAL'
+ OTHER = 'OTHER'
+
+ 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)
+ 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)
+ open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
+
+ # TODO show if empty property?
+ # TODO formatting property?
+
+ space = models.ForeignKey(Space, on_delete=models.CASCADE)
+ objects = ScopedManager(space='space')
+
+ def __str__(self):
+ return f'{self.name}'
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(fields=['space', 'name'], name='property_type_unique_name_per_space'),
+ models.UniqueConstraint(fields=['space', 'open_data_slug'], name='property_type_unique_open_data_slug_per_space')
+ ]
+
+
+class Property(models.Model, PermissionModelMixin):
+ property_amount = models.DecimalField(default=0, decimal_places=4, max_digits=32)
+ property_type = models.ForeignKey(PropertyType, on_delete=models.PROTECT)
+
+ import_food_id = models.IntegerField(null=True, blank=True) # field to hold food id when importing properties from the open data project
+
+ space = models.ForeignKey(Space, on_delete=models.CASCADE)
+ objects = ScopedManager(space='space')
+
+ def __str__(self):
+ return f'{self.property_amount} {self.property_type.unit} {self.property_type.name}'
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(fields=['space', 'property_type', 'import_food_id'], name='property_unique_import_food_per_space')
+ ]
+
+
+class FoodProperty(models.Model):
+ food = models.ForeignKey(Food, on_delete=models.CASCADE)
+ property = models.ForeignKey(Property, on_delete=models.CASCADE)
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(fields=['food', 'property'], name='property_unique_food')
+ ]
+
+
class NutritionInformation(models.Model, PermissionModelMixin):
fats = models.DecimalField(default=0, decimal_places=16, max_digits=32)
carbohydrates = models.DecimalField(
@@ -736,14 +843,6 @@ class NutritionInformation(models.Model, PermissionModelMixin):
return f'Nutrition {self.pk}'
-# class NutritionType(models.Model, PermissionModelMixin):
-# name = models.CharField(max_length=128)
-# icon = models.CharField(max_length=16, blank=True, null=True)
-# description = models.CharField(max_length=512, blank=True, null=True)
-#
-# space = models.ForeignKey(Space, on_delete=models.CASCADE)
-# objects = ScopedManager(space='space')
-
class RecipeManager(models.Manager.from_queryset(models.QuerySet)):
def get_queryset(self):
return super(RecipeManager, self).get_queryset().annotate(rating=Avg('cooklog__rating')).annotate(last_cooked=Max('cooklog__created_at'))
@@ -766,6 +865,7 @@ class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModel
waiting_time = models.IntegerField(default=0)
internal = models.BooleanField(default=False)
nutrition = models.ForeignKey(NutritionInformation, blank=True, null=True, on_delete=models.CASCADE)
+ properties = models.ManyToManyField(Property, blank=True)
show_ingredient_overview = models.BooleanField(default=True)
private = models.BooleanField(default=False)
shared = models.ManyToManyField(User, blank=True, related_name='recipe_shared_with')
diff --git a/cookbook/serializer.py b/cookbook/serializer.py
index 44bc14223..54ef43875 100644
--- a/cookbook/serializer.py
+++ b/cookbook/serializer.py
@@ -7,6 +7,7 @@ from html import escape
from smtplib import SMTPException
from django.contrib.auth.models import Group, User, AnonymousUser
+from django.core.cache import caches
from django.core.mail import send_mail
from django.db.models import Avg, Q, QuerySet, Sum
from django.http import BadHeaderError
@@ -21,15 +22,18 @@ 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.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)
+ SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, Property,
+ PropertyType, Property)
from cookbook.templatetags.custom_tags import markdown
from recipes.settings import AWS_ENABLED, MEDIA_URL
@@ -102,15 +106,21 @@ class CustomOnHandField(serializers.Field):
return instance
def to_representation(self, obj):
- shared_users = None
- if request := self.context.get('request', None):
- shared_users = getattr(request, '_shared_users', None)
- if shared_users is None:
+ if not self.context["request"].user.is_authenticated:
+ return []
+ shared_users = []
+ if c := caches['default'].get(f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
+ shared_users = c
+ else:
try:
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
self.context['request'].user.id]
+ caches['default'].set(
+ f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}',
+ shared_users, timeout=5 * 60)
+ # TODO ugly hack that improves API performance significantly, should be done properly
except AttributeError: # Anonymous users (using share links) don't have shared users
- shared_users = []
+ pass
return obj.onhand_users.filter(id__in=shared_users).exists()
def to_internal_value(self, data):
@@ -276,10 +286,13 @@ class SpaceSerializer(WritableNestedModelSerializer):
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',
- 'image', 'use_plural',)
- read_only_fields = ('id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',)
+ 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',
+ 'image', 'use_plural',)
+ read_only_fields = (
+ 'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
+ 'demo',)
class UserSpaceSerializer(WritableNestedModelSerializer):
@@ -440,7 +453,8 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
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=name, plural_name=plural_name, space=space,
+ defaults=validated_data)
return obj
def update(self, instance, validated_data):
@@ -451,7 +465,7 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
class Meta:
model = Unit
- fields = ('id', 'name', 'plural_name', 'description', 'numrecipe', 'image')
+ fields = ('id', 'name', 'plural_name', 'description', 'numrecipe', 'image', 'open_data_slug')
read_only_fields = ('id', 'numrecipe', 'image')
@@ -484,7 +498,37 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer):
class Meta:
model = Supermarket
- fields = ('id', 'name', 'description', 'category_to_supermarket')
+ fields = ('id', 'name', 'description', 'category_to_supermarket', 'open_data_slug')
+
+
+class PropertyTypeSerializer(serializers.ModelSerializer):
+ def create(self, validated_data):
+ validated_data['space'] = self.context['request'].space
+
+ if property_type := PropertyType.objects.filter(Q(name=validated_data['name'])).first():
+ return property_type
+
+ return super().create(validated_data)
+
+ class Meta:
+ model = PropertyType
+ fields = ('id', 'name', 'icon', 'unit', 'description', 'open_data_slug')
+
+
+class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
+ property_type = PropertyTypeSerializer()
+ property_amount = CustomDecimalField()
+
+ # TODO prevent updates
+
+ def create(self, validated_data):
+ validated_data['space'] = self.context['request'].space
+ return super().create(validated_data)
+
+ class Meta:
+ model = Property
+ fields = ('id', 'property_amount', 'property_type')
+ read_only_fields = ('id',)
class RecipeSimpleSerializer(WritableNestedModelSerializer):
@@ -523,19 +567,29 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand')
substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False)
+ properties = PropertySerializer(many=True, allow_null=True, required=False)
+ properties_food_unit = UnitSerializer(allow_null=True, required=False)
+
recipe_filter = 'steps__ingredients__food'
images = ['recipe__image']
def get_substitute_onhand(self, obj):
- shared_users = None
- if request := self.context.get('request', None):
- shared_users = getattr(request, '_shared_users', None)
- if shared_users is None:
+ if not self.context["request"].user.is_authenticated:
+ return []
+ shared_users = []
+ if c := caches['default'].get(
+ f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}', None):
+ shared_users = c
+ else:
try:
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [
self.context['request'].user.id]
- except AttributeError:
- shared_users = []
+ caches['default'].set(
+ f'shopping_shared_users_{self.context["request"].space.id}_{self.context["request"].user.id}',
+ shared_users, timeout=5 * 60)
+ # TODO ugly hack that improves API performance significantly, should be done properly
+ except AttributeError: # Anonymous users (using share links) don't have shared users
+ pass
filter = Q(id__in=obj.substitute.all())
if obj.substitute_siblings:
filter |= Q(path__startswith=obj.path[:Food.steplen * (obj.depth - 1)], depth=obj.depth)
@@ -547,7 +601,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
# return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0
def create(self, validated_data):
- name = validated_data.pop('name').strip()
+ name = validated_data['name'].strip()
if plural_name := validated_data.pop('plural_name', None):
plural_name = plural_name.strip()
@@ -579,7 +633,11 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
else:
validated_data['onhand_users'] = list(set(onhand_users) - set(shared_users))
- obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space, defaults=validated_data)
+ if properties_food_unit := validated_data.pop('properties_food_unit', None):
+ properties_food_unit = Unit.objects.filter(name=properties_food_unit['name']).first()
+
+ obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space, properties_food_unit=properties_food_unit,
+ defaults=validated_data)
return obj
def update(self, instance, validated_data):
@@ -606,9 +664,11 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
class Meta:
model = Food
fields = (
- 'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category',
+ 'id', 'name', 'plural_name', 'description', 'shopping', 'recipe',
+ 'properties', 'properties_food_amount', 'properties_food_unit',
+ 'food_onhand', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
- 'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields'
+ 'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug',
)
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
@@ -618,9 +678,24 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
unit = UnitSerializer(allow_null=True)
used_in_recipes = serializers.SerializerMethodField('get_used_in_recipes')
amount = CustomDecimalField()
+ conversions = serializers.SerializerMethodField('get_conversions')
def get_used_in_recipes(self, obj):
- return list(Recipe.objects.filter(steps__ingredients=obj.id).values('id', 'name'))
+ used_in = []
+ for s in obj.step_set.all():
+ for r in s.recipe_set.all():
+ used_in.append({'id': r.id, 'name': r.name})
+ return used_in
+
+ def get_conversions(self, obj):
+ if obj.unit and obj.food:
+ uch = UnitConversionHelper(self.context['request'].space)
+ conversions = []
+ for c in uch.get_conversions(obj):
+ conversions.append({'food': c.food.name, 'unit': c.unit.name, 'amount': c.amount}) # TODO do formatting in helper
+ return conversions
+ else:
+ return []
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
@@ -633,10 +708,11 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
class Meta:
model = Ingredient
fields = (
- 'id', 'food', 'unit', 'amount', 'note', 'order',
+ 'id', 'food', 'unit', 'amount', 'conversions', 'note', 'order',
'is_header', 'no_amount', 'original_text', 'used_in_recipes',
'always_use_plural_unit', 'always_use_plural_food',
)
+ read_only_fields = ['conversions', ]
class IngredientSerializer(IngredientSimpleSerializer):
@@ -688,6 +764,30 @@ class StepRecipeSerializer(WritableNestedModelSerializer):
)
+class UnitConversionSerializer(WritableNestedModelSerializer):
+ name = serializers.SerializerMethodField('get_conversion_name')
+ base_unit = UnitSerializer()
+ converted_unit = UnitSerializer()
+ food = FoodSerializer(allow_null=True, required=False)
+ base_amount = CustomDecimalField()
+ converted_amount = CustomDecimalField()
+
+ def get_conversion_name(self, obj):
+ text = f'{round(obj.base_amount)} {obj.base_unit} '
+ if obj.food:
+ text += f' {obj.food}'
+ 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)
+
+ class Meta:
+ model = UnitConversion
+ fields = ('id', 'name', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'food', 'open_data_slug')
+
+
class NutritionInformationSerializer(serializers.ModelSerializer):
carbohydrates = CustomDecimalField()
fats = CustomDecimalField()
@@ -738,21 +838,28 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
class RecipeSerializer(RecipeBaseSerializer):
nutrition = NutritionInformationSerializer(allow_null=True, required=False)
+ properties = PropertySerializer(many=True, required=False)
steps = StepSerializer(many=True)
keywords = KeywordSerializer(many=True)
shared = UserSerializer(many=True, required=False)
rating = CustomDecimalField(required=False, allow_null=True, read_only=True)
last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
+ food_properties = serializers.SerializerMethodField('get_food_properties')
+
+ def get_food_properties(self, obj):
+ fph = FoodPropertyHelper(obj.space) # initialize with object space since recipes might be viewed anonymously
+ return fph.calculate_recipe_properties(obj)
class Meta:
model = Recipe
fields = (
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url',
- 'internal', 'show_ingredient_overview', 'nutrition', 'servings', 'file_path', 'servings_text', 'rating', 'last_cooked',
+ 'internal', 'show_ingredient_overview', 'nutrition', 'properties', 'food_properties', 'servings', 'file_path', 'servings_text', 'rating',
+ 'last_cooked',
'private', 'shared',
)
- read_only_fields = ['image', 'created_by', 'created_at']
+ read_only_fields = ['image', 'created_by', 'created_at', 'food_properties']
def validate(self, data):
above_limit, msg = above_space_limit(self.context['request'].space)
@@ -1089,13 +1196,19 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
if obj.email:
try:
- if InviteLink.objects.filter(space=self.context['request'].space, created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20:
- message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.context['request'].user.get_user_display_name())
- message += _(' to join their Tandoor Recipes space ') + escape(self.context['request'].space.name) + '.\n\n'
- message += _('Click the following link to activate your account: ') + self.context['request'].build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n'
- message += _('If the link does not work use the following code to manually join the space: ') + str(obj.uuid) + '\n\n'
+ if InviteLink.objects.filter(space=self.context['request'].space,
+ created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20:
+ message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(
+ self.context['request'].user.get_user_display_name())
+ message += _(' to join their Tandoor Recipes space ') + escape(
+ self.context['request'].space.name) + '.\n\n'
+ message += _('Click the following link to activate your account: ') + self.context[
+ 'request'].build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n'
+ message += _('If the link does not work use the following code to manually join the space: ') + str(
+ obj.uuid) + '\n\n'
message += _('The invitation is valid until ') + str(obj.valid_until) + '\n\n'
- message += _('Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/'
+ message += _(
+ 'Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub ') + 'https://github.com/vabene1111/recipes/'
send_mail(
_('Tandoor Recipes Invite'),
@@ -1204,7 +1317,8 @@ class IngredientExportSerializer(WritableNestedModelSerializer):
class Meta:
model = Ingredient
- fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount', 'always_use_plural_unit', 'always_use_plural_food')
+ fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount', 'always_use_plural_unit',
+ 'always_use_plural_food')
class StepExportSerializer(WritableNestedModelSerializer):
diff --git a/cookbook/signals.py b/cookbook/signals.py
index 624ffa536..f50f0701f 100644
--- a/cookbook/signals.py
+++ b/cookbook/signals.py
@@ -4,15 +4,17 @@ from functools import wraps
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.postgres.search import SearchVector
+from django.core.cache import caches
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import translation
from django_scopes import scope, scopes_disabled
+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)
+ ShoppingListEntry, Step, UserPreference, SearchPreference, SearchFields, Unit, PropertyType)
SQLITE = True
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
@@ -149,3 +151,15 @@ def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs
print("MEAL_AUTO_ADD Created SLR")
except AttributeError:
pass
+
+
+@receiver(post_save, sender=Unit)
+def clear_unit_cache(sender, instance=None, created=False, **kwargs):
+ if instance:
+ caches['default'].delete(CacheHelper(instance.space).BASE_UNITS_CACHE_KEY)
+
+
+@receiver(post_save, sender=PropertyType)
+def clear_property_type_cache(sender, instance=None, created=False, **kwargs):
+ if instance:
+ caches['default'].delete(CacheHelper(instance.space).PROPERTY_TYPE_CACHE_KEY)
diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html
index 7036ef40a..0b0d12b24 100644
--- a/cookbook/templates/base.html
+++ b/cookbook/templates/base.html
@@ -9,6 +9,7 @@
{% endblock %}
+
@@ -49,7 +50,7 @@
@@ -117,6 +118,10 @@
{% trans 'Books' %}