Merge branch 'TandoorRecipes:develop' into Auto-Planner

This commit is contained in:
AquaticLava
2023-06-21 19:16:49 -06:00
committed by GitHub
86 changed files with 24920 additions and 15094 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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',)

View File

@@ -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)

View File

@@ -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'

View File

@@ -41,6 +41,11 @@ def get_filetype(name):
# 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):
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)

View File

@@ -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'))

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -51,6 +51,12 @@ class NextcloudCookbook(Integration):
ingredients_added = False
for s in recipe_json['recipeInstructions']:
instruction_text = ''
if 'text' in s:
step = Step.objects.create(
instruction=s['text'], name=s['name'], space=self.request.space,
)
else:
step = Step.objects.create(
instruction=s, space=self.request.space,
)
@@ -102,7 +108,6 @@ class NextcloudCookbook(Integration):
m = min % 60
return f'PT{h}H{m}M0S'
def get_file_from_recipe(self, recipe):
export = {}
@@ -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):

View File

@@ -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,6 +68,7 @@ class Plantoeat(Integration):
if image_url:
try:
if validators.url(image_url, public=True):
response = requests.get(image_url)
self.import_recipe_image(recipe, BytesIO(response.content))
except Exception as e:

View File

@@ -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)
try:
if file['recipeYield'] != '':
recipe.servings = int(file['recipeYield'])
recipe.servings = parse_servings(file['recipeYield'])
recipe.servings_text = parse_servings_text(file['recipeYield'])
if file['totalTime'] != '':
recipe.waiting_time = int(file['totalTime']) - int(file['timePrep'])
try:
if 'totalTime' in file and file['totalTime'] != '':
recipe.working_time = parse_time(file['totalTime'])
if file['prepTime'] != '':
recipe.working_time = int(file['timePrep'])
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 time ', str(e))
recipe.save()
except Exception as e:
print('failed to parse yield or time ', str(e))
ingredient_parser = IngredientParser(self.request, True)
ingredients_added = False

View File

@@ -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)
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 <li>'s with text
if recipe_xml.find('remark').find('line') is not None:
@@ -50,6 +53,8 @@ class Rezeptsuitede(Integration):
for ingredient in recipe_xml.find('part').findall('ingredient'):
f = ingredient_parser.get_food(ingredient.attrib['item'])
u = ingredient_parser.get_unit(ingredient.attrib['unit'])
amount = 0
if ingredient.attrib['qty'].strip() != '':
amount, unit, note = ingredient_parser.parse_amount(ingredient.attrib['qty'])
ingredient_step.ingredients.add(Ingredient.objects.create(food=f, unit=u, amount=amount, space=self.request.space, ))

View File

@@ -12,7 +12,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 <noxonad@proton.me>\n"
"Language-Team: Catalan <http://translate.tandoor.dev/projects/tandoor/"
@@ -116,7 +116,7 @@ msgstr "Nombre de decimals dels ingredients."
msgid "If you want to be able to create and see comments underneath recipes."
msgstr "Si vols poder crear i veure comentaris a sota de les receptes."
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -133,7 +133,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr "Barra de navegació s'enganxi a la part superior de la pàgina."
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr ""
"Afegeix automàticament els ingredients del pla d'àpats a la llista de la "
@@ -155,11 +155,11 @@ msgstr ""
"Tots dos camps són opcionals. Si no se'n dóna cap, es mostrarà el nom "
"d'usuari"
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr "Nom"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr "Paraules clau"
@@ -171,7 +171,7 @@ msgstr "Temps de preparació en minuts"
msgid "Waiting time (cooking/baking) in minutes"
msgstr "Temps d'espera (cocció/fornejat) en minuts"
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr "Ruta"
@@ -183,7 +183,7 @@ msgstr "UID Emmagatzematge"
msgid "Default"
msgstr "Per defecte"
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
@@ -191,21 +191,21 @@ msgstr ""
"Per evitar duplicats, s'ignoren les receptes amb el mateix nom que les "
"existents. Marqueu aquesta casella per importar-ho tot."
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr "Afegir el teu comentari: "
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
"Deixeu-lo buit per a Dropbox i introduïu la contrasenya de l'aplicació per a "
"nextcloud."
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr "Deixeu-lo buit per a nextcloud i introduïu el token API per a Dropbox."
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
@@ -213,33 +213,33 @@ msgstr ""
"Deixeu-lo buit per a Dropbox i introduïu només l'URL base per a Nextcloud "
"(<code>/remote.php/webdav/</code> 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 <a href=\"/docs/markdown/"
"\">docs here</a>"
@@ -247,15 +247,15 @@ msgstr ""
"Podeu utilitzar el marcador per donar format a aquest camp. Consulteu els <a "
"href=\"/docs/markdown/\">documents aquí </a>"
#: .\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 <a href=\"/docs/search/\">here</a> for "
"full description of choices."
@@ -288,7 +288,7 @@ msgstr ""
"Seleccioneu el tipus de mètode de cerca. Feu clic <a href=\"/docs/search/"
"\">aquí</a> 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 ""

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"PO-Revision-Date: 2022-10-17 11:33+0000\n"
"Last-Translator: Sokratis Potamias <sokratespot@gmail.com>\n"
"PO-Revision-Date: 2023-05-31 17:19+0000\n"
"Last-Translator: sweeney <sweeneytodd91@protonmail.com>\n"
"Language-Team: Greek <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/el/>\n"
"Language: el\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.10.1\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
#: .\cookbook\templates\stats.html:28
@@ -26,7 +26,7 @@ msgstr "Συστατικά"
#: .\cookbook\forms.py:53
msgid "Default unit"
msgstr ""
msgstr "Προεπιλεγμένη μονάδα μέτρησης"
#: .\cookbook\forms.py:54
msgid "Use fractions"
@@ -34,7 +34,7 @@ msgstr ""
#: .\cookbook\forms.py:55
msgid "Use KJ"
msgstr ""
msgstr "Χρήση KiloJoule(KJ)"
#: .\cookbook\forms.py:56
msgid "Theme"
@@ -42,7 +42,7 @@ msgstr "Θέμα"
#: .\cookbook\forms.py:57
msgid "Navbar color"
msgstr ""
msgstr "Χρώμα μπάρας πλοήγησης"
#: .\cookbook\forms.py:58
msgid "Sticky navbar"
@@ -50,19 +50,19 @@ msgstr ""
#: .\cookbook\forms.py:59
msgid "Default page"
msgstr ""
msgstr "Προεπιλεγμένη σελίδα"
#: .\cookbook\forms.py:60
msgid "Show recent recipes"
msgstr ""
msgstr "Προβολή πρόσφατων συνταγών"
#: .\cookbook\forms.py:61
msgid "Search style"
msgstr ""
msgstr "Τρόπος αναζήτησης"
#: .\cookbook\forms.py:62
msgid "Plan sharing"
msgstr ""
msgstr "Κοινοποίηση προγράμματος"
#: .\cookbook\forms.py:63
msgid "Ingredient decimal places"
@@ -70,7 +70,7 @@ msgstr ""
#: .\cookbook\forms.py:64
msgid "Shopping list auto sync period"
msgstr ""
msgstr "Χρονική περίοδος αυτόματου συγχρονισμού λίστας αγορών"
#: .\cookbook\forms.py:65 .\cookbook\templates\recipe_view.html:21
#: .\cookbook\templates\stats.html:47
@@ -79,17 +79,21 @@ msgstr "Σχόλια"
#: .\cookbook\forms.py:66
msgid "Left-handed mode"
msgstr ""
msgstr "Έκδοση για αριστερόχειρες"
#: .\cookbook\forms.py:70
msgid ""
"Color of the top navigation bar. Not all colors work with all themes, just "
"try them out!"
msgstr ""
"Χρώμα της πάνω μπάρας πλοήγησης. Δεν δουλεύουν όλα τα χρώματα με όλα τα "
"θέματα, απλά δοκιμάστε τα!"
#: .\cookbook\forms.py:72
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
msgstr ""
"Προεπιλεγμένη μονάδα μέτρησης που θα χρησιμοποιείται όταν προστίθεται ένα "
"υλικό σε μια συνταγή."
#: .\cookbook\forms.py:74
msgid ""
@@ -104,18 +108,20 @@ msgstr ""
#: .\cookbook\forms.py:77
msgid "Users with whom newly created meal plans should be shared by default."
msgstr ""
"Χρήστες με του οποίους η κοινοποίηση του προγραμματισμού των γευμάτων θα "
"γίνεται από προεπιλογή."
#: .\cookbook\forms.py:78
msgid "Users with whom to share shopping lists."
msgstr ""
msgstr "Χρήστες με του οποίους θα γίνει κοινοποίηση των λιστών αγορών."
#: .\cookbook\forms.py:80
msgid "Show recently viewed recipes on search page."
msgstr ""
msgstr "Προβολή των προσφάτως προβεβλημένων συνταγών στη σελίδα αναζήτησης."
#: .\cookbook\forms.py:81
msgid "Number of decimals to round ingredients."
msgstr ""
msgstr "Αριθμός των δεκαδικών στα οποία θα γίνεται στρογγυλοποίηση."
#: .\cookbook\forms.py:82
msgid "If you want to be able to create and see comments underneath recipes."
@@ -153,20 +159,20 @@ msgstr ""
#: .\cookbook\forms.py:128 .\cookbook\forms.py:301
msgid "Name"
msgstr ""
msgstr "Όνομα"
#: .\cookbook\forms.py:129 .\cookbook\forms.py:302
#: .\cookbook\templates\stats.html:24 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr ""
msgstr "Λέξεις κλειδιά"
#: .\cookbook\forms.py:130
msgid "Preparation time in minutes"
msgstr ""
msgstr "Χρόνος προετοιμασίας σε λεπτά"
#: .\cookbook\forms.py:131
msgid "Waiting time (cooking/baking) in minutes"
msgstr ""
msgstr "Χρόνος αναμονής (μαγείρεμα/ ψήσιμο) σε λεπτά"
#: .\cookbook\forms.py:132 .\cookbook\forms.py:270 .\cookbook\forms.py:303
msgid "Path"
@@ -188,15 +194,18 @@ msgstr ""
#: .\cookbook\forms.py:200
msgid "Add your comment: "
msgstr ""
msgstr "Προσθήκη σχολίου: "
#: .\cookbook\forms.py:215
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
"Για dropbox παρακαλώ αφήστε το κενό και πληκτρολογήστε το password για "
"nextcloud."
#: .\cookbook\forms.py:222
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr ""
"Για nextcloud αφήστε το κενό και για dropbox πληκτρολογήστε το api token."
#: .\cookbook\forms.py:231
msgid ""
@@ -240,7 +249,7 @@ msgstr ""
#: .\cookbook\forms.py:372
msgid "Email address already taken!"
msgstr ""
msgstr "Αυτή η διεύθυνση email δεν είναι διαθέσιμη!"
#: .\cookbook\forms.py:380
msgid ""
@@ -306,7 +315,7 @@ msgstr ""
#: .\cookbook\forms.py:463
msgid "Search Method"
msgstr ""
msgstr "Μέθοδος αναζήτησης"
#: .\cookbook\forms.py:464
msgid "Fuzzy Lookups"
@@ -330,19 +339,24 @@ msgstr ""
#: .\cookbook\forms.py:469
msgid "Full Text"
msgstr ""
msgstr "Πλήρες κείμενο"
#: .\cookbook\forms.py:494
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:500
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
msgstr ""
"Όταν προστίθεται ένα πρόγραμμα γευμάτων στη λίστα αγορών (χειροκίνητα ή "
"αυτόματα), να συμπεριλαμβάνονται όλες οι σχετικές συνταγές."
#: .\cookbook\forms.py:501
msgid ""
@@ -353,18 +367,23 @@ msgstr ""
#: .\cookbook\forms.py:502
msgid "Default number of hours to delay a shopping list entry."
msgstr ""
"Προεπιλεγμένος αριθμός ωρών για την καθυστέρηση μιας εγγραφής στη λίστα "
"αγορών."
#: .\cookbook\forms.py:503
msgid "Filter shopping list to only include supermarket categories."
msgstr ""
"Φιλτράρισμα λίστας αγορών ώστε να περιλαμβάνει μόνο τις κατηγορίες του "
"supermarker."
#: .\cookbook\forms.py:504
msgid "Days of recent shopping list entries to display."
msgstr ""
msgstr "Αριθμός ημερών για τη προβολή των πρόσφατων εγγραφών της λίστας αγορών."
#: .\cookbook\forms.py:505
msgid "Mark food 'On Hand' when checked off shopping list."
msgstr ""
"Χαρακτηρισμός ενός τροφίμου ως 'Διαθέσιμο' όταν τσεκαριστεί στη λίστα αγορών."
#: .\cookbook\forms.py:506
msgid "Delimiter to use for CSV exports."
@@ -376,27 +395,27 @@ msgstr ""
#: .\cookbook\forms.py:511
msgid "Share Shopping List"
msgstr ""
msgstr "Κοινοποίηση λίστας αγορών"
#: .\cookbook\forms.py:512
msgid "Autosync"
msgstr ""
msgstr "Αυτόματος συγχρονισμός"
#: .\cookbook\forms.py:513
msgid "Auto Add Meal Plan"
msgstr ""
msgstr "Αυτόματη προσθήκη προγραμματισμού γευμάτων"
#: .\cookbook\forms.py:514
msgid "Exclude On Hand"
msgstr ""
msgstr "Αποκλεισμός διαθέσιμων"
#: .\cookbook\forms.py:515
msgid "Include Related"
msgstr ""
msgstr "Συμπερίληψη σχετικών"
#: .\cookbook\forms.py:516
msgid "Default Delay Hours"
msgstr ""
msgstr "Προεπιλεγμένες ώρες καθυστέρησης"
#: .\cookbook\forms.py:517
msgid "Filter to Supermarket"
@@ -404,7 +423,7 @@ msgstr ""
#: .\cookbook\forms.py:518
msgid "Recent Days"
msgstr ""
msgstr "Πρόσφατες ημέρες"
#: .\cookbook\forms.py:519
msgid "CSV Delimiter"
@@ -439,11 +458,13 @@ msgid ""
"In order to prevent spam, the requested email was not send. Please wait a "
"few minutes and try again."
msgstr ""
"Για να αποκλείσουμε πιθανά spam, το email που ζητήθηκε δεν στάλθηκε. "
"Παρακαλώ περιμένετε λίγα λεπτά και δοκιμάστε ξανά."
#: .\cookbook\helper\permission_helper.py:149
#: .\cookbook\helper\permission_helper.py:172 .\cookbook\views\views.py:152
msgid "You are not logged in and therefore cannot view this page!"
msgstr ""
msgstr "Δεν μπορείτε να δείτε αυτή τη σελίδα γιατί δεν είστε συνδεδεμένος!"
#: .\cookbook\helper\permission_helper.py:153
#: .\cookbook\helper\permission_helper.py:159
@@ -455,7 +476,7 @@ msgstr ""
#: .\cookbook\views\views.py:163 .\cookbook\views\views.py:170
#: .\cookbook\views\views.py:249
msgid "You do not have the required permissions to view this page!"
msgstr ""
msgstr "Δεν έχετε τα απαιτούμενα δικαιώματα να δείτε αυτή τη σελίδα!"
#: .\cookbook\helper\permission_helper.py:177
#: .\cookbook\helper\permission_helper.py:200
@@ -463,14 +484,15 @@ msgstr ""
#: .\cookbook\helper\permission_helper.py:237
msgid "You cannot interact with this object as it is not owned by you!"
msgstr ""
"Δεν μπορείτε να αλληλεπιδράστε με αυτό το αντικείμενο γιατί δεν σας ανήκει!"
#: .\cookbook\helper\permission_helper.py:321
msgid "You have reached the maximum number of recipes for your space."
msgstr ""
msgstr "Έχετε υπερβεί τον μέγιστο αριθμό συνταγών για τον χώρο σας."
#: .\cookbook\helper\permission_helper.py:333
msgid "You have more users than allowed in your space."
msgstr ""
msgstr "Έχετε περισσότερους χρήστες από το επιτρεπόμενο στον χώρο σας."
#: .\cookbook\helper\recipe_search.py:565
msgid "One of queryset or hash_key must be provided"
@@ -519,33 +541,33 @@ msgstr ""
#: .\cookbook\integration\paprika.py:46
msgid "Notes"
msgstr ""
msgstr "Σημειώσεις"
#: .\cookbook\integration\paprika.py:49
msgid "Nutritional Information"
msgstr ""
msgstr "Διατροφικές πληροφορίες"
#: .\cookbook\integration\paprika.py:53
msgid "Source"
msgstr ""
msgstr "Πηγή"
#: .\cookbook\integration\saffron.py:23
msgid "Servings"
msgstr ""
msgstr "Μερίδες"
#: .\cookbook\integration\saffron.py:25
msgid "Waiting time"
msgstr ""
msgstr "Χρόνος αναμονής"
#: .\cookbook\integration\saffron.py:27
msgid "Preparation Time"
msgstr ""
msgstr "Χρόνος προετοιμασίας"
#: .\cookbook\integration\saffron.py:29
#: .\cookbook\templates\forms\ingredients.html:7
#: .\cookbook\templates\index.html:7
msgid "Cookbook"
msgstr ""
msgstr "Βιβλίο συνταγών"
#: .\cookbook\integration\saffron.py:31
msgid "Section"
@@ -569,19 +591,19 @@ msgstr ""
#: .\cookbook\migrations\0047_auto_20200602_1133.py:14
msgid "Breakfast"
msgstr ""
msgstr "Πρωινό"
#: .\cookbook\migrations\0047_auto_20200602_1133.py:19
msgid "Lunch"
msgstr ""
msgstr "Μεσημεριανό"
#: .\cookbook\migrations\0047_auto_20200602_1133.py:24
msgid "Dinner"
msgstr ""
msgstr "Βραδινό"
#: .\cookbook\migrations\0047_auto_20200602_1133.py:29
msgid "Other"
msgstr ""
msgstr "Άλλο"
#: .\cookbook\models.py:251
msgid ""

View File

@@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -102,7 +102,7 @@ msgstr ""
msgid "If you want to be able to create and see comments underneath recipes."
msgstr ""
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -114,7 +114,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr ""
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr ""
@@ -132,11 +132,11 @@ msgid ""
"instead"
msgstr ""
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr ""
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr ""
@@ -148,7 +148,7 @@ msgstr ""
msgid "Waiting time (cooking/baking) in minutes"
msgstr ""
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr ""
@@ -160,261 +160,261 @@ msgstr ""
msgid "Default"
msgstr ""
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
msgstr ""
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr ""
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr ""
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> 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 <a href=\"/docs/markdown/"
"\">docs here</a>"
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 <a href=\"/docs/search/\">here</a> 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 ""

View File

@@ -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 <apu24@drexel.edu>\n"
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
"PO-Revision-Date: 2023-05-26 16:19+0000\n"
"Last-Translator: Luis Cacho <luiscachog@gmail.com>\n"
"Language-Team: Spanish <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/es/>\n"
"Language: es\n"
@@ -117,7 +117,7 @@ msgstr "Número de decimales para redondear los ingredientes."
msgid "If you want to be able to create and see comments underneath recipes."
msgstr "Si desea poder crear y ver comentarios debajo de las recetas."
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -135,7 +135,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr "Hace la barra de navegación fija en la parte superior de la página."
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr ""
"Añadir de manera automática los ingredientes del plan a la lista de la "
@@ -157,11 +157,11 @@ msgstr ""
"Ambos campos son opcionales. Si no se proporciona ninguno, se mostrará el "
"nombre de usuario en su lugar"
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr "Nombre"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr "Palabras clave"
@@ -173,7 +173,7 @@ msgstr "Tiempo de preparación en minutos"
msgid "Waiting time (cooking/baking) in minutes"
msgstr "Tiempo de espera (cocinar/hornear) en minutos"
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr "Ruta"
@@ -185,7 +185,7 @@ msgstr "UID de almacenamiento"
msgid "Default"
msgstr "Por defecto"
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
@@ -193,22 +193,22 @@ msgstr ""
"Para evitar duplicados, las recetas con el mismo nombre serán ignoradas. "
"Marca esta opción para importar todas las recetas."
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr "Añada su comentario: "
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
"Déjelo vacío para Dropbox e ingrese la contraseña de la aplicación para "
"nextcloud."
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr ""
"Déjelo en blanco para nextcloud e ingrese el token de api para dropbox."
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
@@ -216,33 +216,33 @@ msgstr ""
"Dejar vació para Dropbox e introducir sólo la URL base para Nextcloud "
"(<code>/remote.php/webdav/</code> 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 <a href=\"/docs/markdown/"
"\">docs here</a>"
@@ -250,15 +250,15 @@ msgstr ""
"Puede utilizar Markdown para formatear este campo. Vea la <a href=\"/docs/"
"markdown/\">documentación aqui</a>"
#: .\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 <a href=\"/docs/search/\">here</a> for "
"full description of choices."
@@ -291,7 +291,7 @@ msgstr ""
"Selecciona el tipo de búsqueda. Haz click <a href=\"/docs/search/\">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"

View File

@@ -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 <noxonad@proton.me>\n"
"Language-Team: French <http://translate.tandoor.dev/projects/tandoor/recipes-"
@@ -121,7 +121,7 @@ msgstr ""
"Si vous souhaitez pouvoir créer et consulter des commentaires en dessous des "
"recettes."
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -139,7 +139,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr "Épingler la barre de navigation en haut de la page."
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr ""
"Ajouter les ingrédients du menu de la semaine à la liste de courses "
@@ -161,11 +161,11 @@ msgstr ""
"Les deux champs sont facultatifs. Si aucun nest rempli, le nom "
"dutilisateur sera affiché à la place"
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr "Nom"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr "Mots-clés"
@@ -177,7 +177,7 @@ msgstr "Temps de préparation en minutes"
msgid "Waiting time (cooking/baking) in minutes"
msgstr "Temps dattente (cuisson) en minutes"
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr "Chemin"
@@ -189,7 +189,7 @@ msgstr "UID de stockage"
msgid "Default"
msgstr "Par défaut"
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
@@ -197,22 +197,22 @@ msgstr ""
"Pour éviter les doublons, les recettes de même nom seront ignorées. Cocher "
"cette case pour tout importer."
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr "Ajoutez votre commentaire : "
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
"Laissez vide pour Dropbox et renseignez votre mot de passe dapplication "
"pour Nextcloud."
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr ""
"Laissez vide pour Nextcloud et renseignez votre jeton dAPI pour Dropbox."
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
@@ -220,33 +220,33 @@ msgstr ""
"Laisser vide pour Dropbox et saisissez seulement lURL de base pour "
"Nextcloud (<code>/remote.php/webdav/</code> 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 <a href=\"/docs/markdown/"
"\">docs here</a>"
@@ -254,15 +254,15 @@ msgstr ""
"Vous pouvez utiliser du markdown pour mettre en forme ce champ. Voir la <a "
"href=\"/docs/markdown/\">documentation ici</a>"
#: .\cookbook\forms.py:362
#: .\cookbook\forms.py:379
msgid "Maximum number of users for this space reached."
msgstr "Nombre maximum dutilisateurs 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 nest pas requise mais si elle est renseignée, le lien "
"dinvitation sera envoyé à lutilisateur."
#: .\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 dutilisation"
#: .\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 <a href=\"/docs/search/\">here</a> for "
"full description of choices."
@@ -295,7 +295,7 @@ msgstr ""
"Sélectionner la méthode de recherche. Cliquer <a href=\"/docs/search/"
"\">ici</a> 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 limportation 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 quavec 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 lajout dun 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 lajout dun 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 ninclure 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 laliment comme disponible lorsquil 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."

View File

@@ -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 <noxonad@proton.me>\n"
"Language-Team: Hungarian <http://translate.tandoor.dev/projects/tandoor/"
@@ -116,7 +116,7 @@ msgstr ""
"Ha azt szeretné, hogy hozzászólásokat tudjon létrehozni és látni a receptek "
"alatt."
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -133,7 +133,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr "A navigációs sávot az oldal tetejére rögzíti."
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr ""
"Automatikusan hozzáadja az étkezési terv hozzávalóit a bevásárlólistához."
@@ -154,11 +154,11 @@ msgstr ""
"Mindkét mező opcionális. Ha egyiket sem adjuk meg, akkor a felhasználónév "
"jelenik meg helyette"
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr "Név"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr "Kulcsszavak"
@@ -170,7 +170,7 @@ msgstr "Előkészítési idő percben"
msgid "Waiting time (cooking/baking) in minutes"
msgstr "Várakozási idő (sütés/főzés) percben"
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr "Elérési útvonal"
@@ -182,7 +182,7 @@ msgstr "Tárhely UID"
msgid "Default"
msgstr "Alapértelmezett"
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
@@ -191,23 +191,23 @@ msgstr ""
"recepteket a rendszer figyelmen kívül hagyja. Jelölje be ezt a négyzetet, ha "
"mindent importálni szeretne."
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr "Add hozzá a kommented: "
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
"A dropbox esetében hagyja üresen, a nextcloud esetében pedig adja meg az "
"alkalmazás jelszavát."
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr ""
"A nextcloud esetében hagyja üresen, a dropbox esetében pedig adja meg az api "
"tokent."
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
@@ -215,33 +215,33 @@ msgstr ""
"Hagyja üresen a dropbox esetén, és csak a nextcloud alap url-jét adja meg "
"(<code>/remote.php/webdav/</code> 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 <a href=\"/docs/markdown/"
"\">docs here</a>"
@@ -249,15 +249,15 @@ msgstr ""
"A mező formázásához használhatja a markdown formátumot. Lásd a <a href=\"/"
"docs/markdown/\">dokumentációt itt</a>"
#: .\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 <a href=\"/docs/search/\">here</a> "
@@ -294,7 +294,7 @@ msgstr ""
"Válassza ki a keresés típusát. Kattintson <a href=\"/docs/search/\">ide</a> "
"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 ""

View File

@@ -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 <olivercervera@yahoo.it>\n"
"Language-Team: Italian <http://translate.tandoor.dev/projects/tandoor/"
@@ -116,7 +116,7 @@ msgid "If you want to be able to create and see comments underneath recipes."
msgstr ""
"Se vuoi essere in grado di creare e vedere i commenti sotto le ricette."
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -134,7 +134,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr "Fissa la barra di navigazione nella parte superiore della pagina."
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr ""
"Aggiungi automaticamente gli ingredienti del piano alimentare alla lista "
@@ -156,11 +156,11 @@ msgstr ""
"Entrambi i campi sono facoltativi. Se non viene fornito, verrà visualizzato "
"il nome utente"
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr "Nome"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr "Parole chiave"
@@ -172,7 +172,7 @@ msgstr "Tempo di preparazione in minuti"
msgid "Waiting time (cooking/baking) in minutes"
msgstr "Tempo di attesa (cottura) in minuti"
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr "Percorso"
@@ -184,7 +184,7 @@ msgstr "UID di archiviazione"
msgid "Default"
msgstr "Predefinito"
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
@@ -192,20 +192,20 @@ msgstr ""
"Per prevenire duplicati, vengono ignorate le ricette che hanno lo stesso "
"nome di quelle esistenti. Metti la spunta per importare tutto."
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr "Aggiungi il tuo commento: "
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
"Lascia vuoto per dropbox e inserisci la password dell'app per nextcloud."
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr "Lascia vuoto per nextcloud e inserisci l'api token per dropbox."
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
@@ -213,33 +213,33 @@ msgstr ""
"Lascia vuoto per dropbox e inserisci solo l'url base per nextcloud (<code>/"
"remote.php/webdav/</code> è 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 <a href=\"/docs/markdown/"
"\">docs here</a>"
@@ -247,15 +247,15 @@ msgstr ""
"Puoi usare markdown per formattare questo campo. Guarda la <a href=\"/docs/"
"markdown/\">documentazione qui</a>"
#: .\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 <a href=\"/docs/search/\">here</a> for "
"full description of choices."
@@ -288,7 +288,7 @@ msgstr ""
"Seleziona il metodo di ricerca. Clicca <a href=\"/docs/search/\">qui</a> "
"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 "

View File

@@ -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 <joachim.weber@gmx.de>\n"
"Language-Team: Latvian <http://translate.tandoor.dev/projects/tandoor/"
@@ -125,7 +125,7 @@ msgid "If you want to be able to create and see comments underneath recipes."
msgstr ""
"Ja vēlaties, lai jūs varētu izveidot un redzēt komentārus zem receptēm."
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -143,7 +143,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr ""
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr ""
@@ -163,11 +163,11 @@ msgstr ""
"Abi lauki nav obligāti. Ja neviens nav norādīts, tā vietā tiks parādīts "
"lietotājvārds"
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr "Vārds"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr "Atslēgvārdi"
@@ -179,7 +179,7 @@ msgstr "Pagatavošanas laiks minūtēs"
msgid "Waiting time (cooking/baking) in minutes"
msgstr "Gaidīšanas laiks (vārīšana / cepšana) minūtēs"
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr "Ceļš"
@@ -191,25 +191,25 @@ msgstr "Krātuves UID"
msgid "Default"
msgstr ""
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
msgstr ""
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr "Pievienot komentāru: "
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr "Atstājiet tukšu Dropbox un ievadiet lietotnes paroli Nextcloud."
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr "Atstājiet tukšu Nextcloud un ievadiet API tokenu Dropbox."
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
@@ -217,33 +217,33 @@ msgstr ""
"Atstājiet tukšu Dropbox un ievadiet tikai Nextcloud bāzes URL (<kods> /"
"remote.php/webdav/ </code> tiek pievienots automātiski)"
#: .\cookbook\forms.py:265 .\cookbook\views\edit.py:157
#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
msgstr "Krātuve"
#: .\cookbook\forms.py:267
#: .\cookbook\forms.py:284
msgid "Active"
msgstr ""
#: .\cookbook\forms.py:273
#: .\cookbook\forms.py:290
msgid "Search String"
msgstr "Meklēšanas virkne"
#: .\cookbook\forms.py:300
#: .\cookbook\forms.py:317
msgid "File ID"
msgstr "Faila ID"
#: .\cookbook\forms.py:322
#: .\cookbook\forms.py:339
msgid "You must provide at least a recipe or a title."
msgstr "Jums jānorāda vismaz recepte vai nosaukums."
#: .\cookbook\forms.py:335
#: .\cookbook\forms.py:352
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"Iestatījumos varat uzskaitīt noklusējuma lietotājus, ar kuriem koplietot "
"receptes."
#: .\cookbook\forms.py:336
#: .\cookbook\forms.py:353
msgid ""
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
"\">docs here</a>"
@@ -251,219 +251,219 @@ msgstr ""
"Lai formatētu šo lauku, varat izmantot Markdown. Skatiet <a href=\"/docs/"
"markdown/\"> dokumentus šeit </a>"
#: .\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 <a href=\"/docs/search/\">here</a> 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
#, fuzzy
#| msgid "Search"
msgid "Search Method"
msgstr "Meklēt"
#: .\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 "Meklēt"
#: .\cookbook\forms.py:465
#: .\cookbook\forms.py:482
#, fuzzy
#| msgid "Text"
msgid "Full Text"
msgstr "Teskts"
#: .\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 List"
msgid "Share Shopping List"
msgstr "Iepirkumu saraksts"
#: .\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 "Saraksta prefikss"
#: .\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 "Ēdiens, kas būtu jāaizstāj."
#: .\cookbook\forms.py:541
#: .\cookbook\forms.py:558
#, fuzzy
#| msgid "Show recently viewed recipes on search page."
msgid "Show recipe counts on search filters"
msgstr "Parādīt nesen skatītās receptes meklēšanas lapā."
#: .\cookbook\forms.py:542
#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""

View File

@@ -12,7 +12,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-02-27 13:55+0000\n"
"Last-Translator: Jesse <jesse.kamps@pm.me>\n"
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/recipes-"
@@ -116,7 +116,7 @@ msgstr "Aantal decimalen om ingrediënten op af te ronden."
msgid "If you want to be able to create and see comments underneath recipes."
msgstr "Als je opmerkingen onder recepten wil kunnen maken en zien."
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -133,7 +133,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr "Zet de navbar vast aan de bovenkant van de pagina."
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr "Zet maaltijdplan ingrediënten automatisch op boodschappenlijst."
@@ -153,11 +153,11 @@ msgstr ""
"Beide velden zijn optioneel. Indien niks is opgegeven wordt de "
"gebruikersnaam weergegeven"
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr "Naam"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr "Etiketten"
@@ -169,7 +169,7 @@ msgstr "Voorbereidingstijd in minuten"
msgid "Waiting time (cooking/baking) in minutes"
msgstr "Wacht tijd in minuten (koken en bakken)"
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr "Pad"
@@ -181,7 +181,7 @@ msgstr "Opslag UID"
msgid "Default"
msgstr "Standaard waarde"
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
@@ -189,19 +189,19 @@ msgstr ""
"Om dubbelingen te voorkomen worden recepten met dezelfde naam als een "
"bestaand recept genegeerd. Vink aan om alles te importeren."
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr "Voeg een opmerking toe: "
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr "Laat leeg voor dropbox en vul het app wachtwoord in voor nextcloud."
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr "Laat leeg voor nextcloud en vul de api token in voor dropbox."
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
@@ -209,33 +209,33 @@ msgstr ""
"Laat leeg voor dropbox en vul enkel de base url voor nextcloud in. (<code>/"
"remote.php/webdav/</code> 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 <a href=\"/docs/markdown/"
"\">docs here</a>"
@@ -243,15 +243,15 @@ msgstr ""
"Je kunt markdown gebruiken om dit veld te op te maken. Bekijk de <a href=\"/"
"docs/markdown/\">documentatie hier</a>"
#: .\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 <a href=\"/docs/search/\">here</a> for "
"full description of choices."
@@ -283,7 +283,7 @@ msgstr ""
"Selecteer zoekmethode. Klik <a href=\"/docs/search/\">hier</a> 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."

View File

@@ -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 <joachim.weber@gmx.de>\n"
"Language-Team: Portuguese <http://translate.tandoor.dev/projects/tandoor/"
@@ -114,7 +114,7 @@ msgstr "Número de casas decimais para arredondamentos."
msgid "If you want to be able to create and see comments underneath recipes."
msgstr "Ativar a funcionalidade comentar receitas."
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -131,7 +131,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr "Mantém a barra de navegação no topo da página."
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr ""
@@ -151,11 +151,11 @@ msgstr ""
"Ambos os campos são opcionais. Se nenhum for preenchido o nome de utilizador "
"será apresentado"
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr "Nome"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr "Palavras-chave"
@@ -167,7 +167,7 @@ msgstr "Tempo de preparação em minutos"
msgid "Waiting time (cooking/baking) in minutes"
msgstr "Tempo de espera (cozedura) em minutos"
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr "Caminho"
@@ -179,7 +179,7 @@ msgstr "UID de armazenamento"
msgid "Default"
msgstr "Predefinição"
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
@@ -187,21 +187,21 @@ msgstr ""
"Para evitar repetições, receitas com o mesmo nome de receitas já existentes "
"são ignoradas. Marque esta caixa para importar tudo."
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr "Adicionar comentário: "
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
"Deixar vazio para Dropbox e inserir palavra-passe de aplicação para "
"Nextcloud."
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr "Deixar vazio para Nextcloud e inserir token api para Dropbox."
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
@@ -209,33 +209,33 @@ msgstr ""
"Deixar vazio para Dropbox e inserir apenas url base para Nextcloud (<code>/"
"remote.php/webdav/</code>é 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 <a href=\"/docs/markdown/"
"\">docs here</a>"
@@ -243,15 +243,15 @@ msgstr ""
"É possível utilizar markdown para editar este campo. Documentação <a href=\"/"
"docs/markdown/\">disponível aqui</a>"
#: .\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 <a href=\"/docs/search/\">here</a> "
@@ -288,7 +288,7 @@ msgstr ""
"Selecionar o método de pesquisa. Uma descrição completa das opções pode ser "
"encontrada <a href=\"/docs/search/\">aqui</a>."
#: .\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 ""

View File

@@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -102,7 +102,7 @@ msgstr ""
msgid "If you want to be able to create and see comments underneath recipes."
msgstr ""
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -114,7 +114,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr ""
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr ""
@@ -132,11 +132,11 @@ msgid ""
"instead"
msgstr ""
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr ""
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr ""
@@ -148,7 +148,7 @@ msgstr ""
msgid "Waiting time (cooking/baking) in minutes"
msgstr ""
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr ""
@@ -160,261 +160,261 @@ msgstr ""
msgid "Default"
msgstr ""
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
msgstr ""
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr ""
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr ""
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> 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 <a href=\"/docs/markdown/"
"\">docs here</a>"
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 <a href=\"/docs/search/\">here</a> 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 ""

View File

@@ -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 <g.kalipcilar@gmail.com>\n"
"Language-Team: Turkish <http://translate.tandoor.dev/projects/tandoor/"
@@ -110,7 +110,7 @@ msgstr "Malzeme birimleri için yuvarlanma basamağı."
msgid "If you want to be able to create and see comments underneath recipes."
msgstr "Tariflerin altında yorumlar oluşturup görebilmek istiyorsanız."
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -127,7 +127,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr ""
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr ""
@@ -145,11 +145,11 @@ msgid ""
"instead"
msgstr ""
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr "İsim"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr ""
@@ -161,7 +161,7 @@ msgstr ""
msgid "Waiting time (cooking/baking) in minutes"
msgstr ""
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr ""
@@ -173,263 +173,263 @@ msgstr ""
msgid "Default"
msgstr "Varsayılan"
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
msgstr ""
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr ""
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr ""
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> 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 "Aktif"
#: .\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 <a href=\"/docs/markdown/"
"\">docs here</a>"
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 <a href=\"/docs/search/\">here</a> 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
#, fuzzy
#| msgid "Show recently viewed recipes on search page."
msgid "Show recipe counts on search filters"
msgstr "Son görüntülenen tarifleri arama sayfasında göster."
#: .\cookbook\forms.py:542
#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""

View File

@@ -7,7 +7,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-02-26 13:15+0000\n"
"Last-Translator: 吕楪 <thy@irithys.com>\n"
"Language-Team: Chinese (Simplified) <http://translate.tandoor.dev/projects/"
@@ -104,7 +104,7 @@ msgstr "四舍五入食材的小数点数量。"
msgid "If you want to be able to create and see comments underneath recipes."
msgstr "如果你希望能够在菜谱下面创建并看到评论。"
#: .\cookbook\forms.py:79 .\cookbook\forms.py:492
#: .\cookbook\forms.py:79 .\cookbook\forms.py:509
msgid ""
"Setting to 0 will disable auto sync. When viewing a shopping list the list "
"is updated every set seconds to sync changes someone else might have made. "
@@ -119,7 +119,7 @@ msgstr ""
msgid "Makes the navbar stick to the top of the page."
msgstr "使导航栏悬浮在页面的顶部。"
#: .\cookbook\forms.py:83 .\cookbook\forms.py:495
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr "自动将膳食计划食材添加到购物清单中。"
@@ -137,11 +137,11 @@ msgid ""
"instead"
msgstr "这两个字段都是可选的。如果没有给出,将显示用户名"
#: .\cookbook\forms.py:123 .\cookbook\forms.py:297
#: .\cookbook\forms.py:123 .\cookbook\forms.py:314
msgid "Name"
msgstr "名字"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:298 .\cookbook\views\lists.py:88
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr "关键词"
@@ -153,7 +153,7 @@ msgstr "准备时间(分钟)"
msgid "Waiting time (cooking/baking) in minutes"
msgstr "等候(烹饪、烘焙等)时间(分钟)"
#: .\cookbook\forms.py:127 .\cookbook\forms.py:266 .\cookbook\forms.py:299
#: .\cookbook\forms.py:127 .\cookbook\forms.py:283 .\cookbook\forms.py:316
msgid "Path"
msgstr "路径"
@@ -165,7 +165,7 @@ msgstr "存储 UID"
msgid "Default"
msgstr "默认"
#: .\cookbook\forms.py:173
#: .\cookbook\forms.py:190
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
@@ -173,19 +173,19 @@ msgstr ""
"为防止重复,忽略与现有同名的菜谱。选中此框可导入所有内容(危险操作,请先备"
"份)。"
#: .\cookbook\forms.py:196
#: .\cookbook\forms.py:213
msgid "Add your comment: "
msgstr "发表评论: "
#: .\cookbook\forms.py:211
#: .\cookbook\forms.py:228
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr "Dropbox 留空并输入 Nextcloud 应用密码。"
#: .\cookbook\forms.py:218
#: .\cookbook\forms.py:235
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr "Nextcloud 留空并输入 Dropbox API 令牌。"
#: .\cookbook\forms.py:227
#: .\cookbook\forms.py:244
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
@@ -193,60 +193,60 @@ msgstr ""
"Dropbox 留空并输入基础 Nextcloud 网址(<code>/remote.php/webdav/</code> 会自"
"动添加)"
#: .\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 <a href=\"/docs/markdown/"
"\">docs here</a>"
msgstr ""
"可以使用 Markdown 设置此字段格式。<a href=\"/docs/markdown/\">查看文档</a>"
#: .\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 <a href=\"/docs/search/\">here</a> for "
"full description of choices."
@@ -262,31 +262,31 @@ msgstr ""
"选择搜索类型方法。 <a href=\"/docs/search/\">点击此处</a> 查看选项的完整说"
"明。"
#: .\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 "在此空间内使用复数形式表示单位和食物。"

View File

@@ -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'),
),
]

View File

@@ -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)
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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),
),
]

View File

@@ -82,10 +82,13 @@ 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 hasattr(self, 'space'):
if obj := self.filter(name__iexact=kwargs['name'], space=kwargs['space']).first():
return obj, False
else:
if obj := self.filter(name__iexact=kwargs['name']).first():
return obj, False
with scopes_disabled():
try:
defaults = kwargs.pop('defaults', None)
@@ -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')

View File

@@ -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',
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',)
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):

View File

@@ -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)

View File

@@ -9,6 +9,7 @@
{% endblock %}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex,nofollow"/>
<link rel="shortcut icon" type="image/x-icon" href="{% static 'assets/favicon.svg' %}">
@@ -117,6 +118,10 @@
<a class="nav-link" href="{% url 'view_books' %}"><i
class="fas fa-fw fa-book-open"></i> {% trans 'Books' %}</a>
</li>
{% plugin_main_nav_templates as plugin_main_nav_templates %}
{% for pn in plugin_main_nav_templates %}
{% include pn %}
{% endfor %}
</ul>
<ul class="navbar-nav ml-auto">
@@ -269,6 +274,33 @@
</div>
</a>
</div>
<div class="col-4">
<a href="{% url 'list_property_type' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-database fa-2x"></i>
</div>
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
{% trans 'Properties' %}
</div>
</div>
</a>
</div>
</div>
<div class="row m-0 mt-2 mt-md-0">
<div class="col-4">
<a href="{% url 'list_unit_conversion' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-exchange-alt fa-2x"></i>
</div>
<div class="card-body text-break text-center p-0 no-gutters text-muted menu-dropdown-text">
{% trans 'Unit Conversions' %}
</div>
</div>
</a>
</div>
</div>
</div>
</li>
@@ -322,6 +354,12 @@
<a class="dropdown-item" href="{% url 'view_space_overview' %}"><i
class="fas fa-list"></i> {% trans 'Overview' %}</a>
{% endif %}
{% plugin_dropdown_nav_templates as plugin_dropdown_nav_templates %}
{% for pn in plugin_dropdown_nav_templates %}
<div class="dropdown-divider"></div>
{% include pn %}
{% endfor %}
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'docs_markdown' %}"><i
class="fab fa-markdown fa-fw"></i> {% trans 'Markdown Guide' %}</a>
@@ -348,6 +386,7 @@
</div>
</nav>
{% message_of_the_day request as message_of_the_day %}
{% if message_of_the_day %}
<div class="bg-info" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">

View File

@@ -16,7 +16,7 @@ from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.helper.mdx_urlize import UrlizeExtension
from cookbook.models import Space, get_model_name
from recipes import settings
from recipes.settings import STATIC_URL
from recipes.settings import STATIC_URL, PLUGINS
register = template.Library()
@@ -132,6 +132,22 @@ def is_debug():
def markdown_link():
return f"{_('You can use markdown to format this field. See the ')}<a target='_blank' href='{reverse('docs_markdown')}'>{_('docs here')}</a>"
@register.simple_tag
def plugin_dropdown_nav_templates():
templates = []
for p in PLUGINS:
if p['nav_dropdown']:
templates.append(p['nav_dropdown'])
return templates
@register.simple_tag
def plugin_main_nav_templates():
templates = []
for p in PLUGINS:
if p['nav_main']:
templates.append(p['nav_main'])
return templates
@register.simple_tag
def bookmarklet(request):

View File

@@ -2,6 +2,7 @@ import json
import pytest
from django.contrib import auth
from django.core.cache import caches
from django.urls import reverse
from django_scopes import scope, scopes_disabled
from pytest_factoryboy import LazyFixture, register
@@ -28,7 +29,6 @@ if (Food.node_order_by):
else:
node_location = 'last-child'
register(FoodFactory, 'obj_1', space=LazyFixture('space_1'))
register(FoodFactory, 'obj_2', space=LazyFixture('space_1'))
register(FoodFactory, 'obj_3', space=LazyFixture('space_2'))
@@ -554,6 +554,7 @@ def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
assert (getattr(obj_tree_1, field) == new_val) == inherit
assert (getattr(child, field) == new_val) == inherit
# TODO add test_inherit with child_inherit
@@ -613,11 +614,9 @@ def test_reset_inherit_no_food_instances(obj_tree_1, space_1, field):
parent.reset_inheritance(space=space_1)
def test_onhand(obj_1, u1_s1, u2_s1):
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
'food_onhand'] == False
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
'food_onhand'] == False
def test_onhand(obj_1, u1_s1, u2_s1, space_1):
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] is False
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] is False
u1_s1.patch(
reverse(
@@ -627,13 +626,12 @@ def test_onhand(obj_1, u1_s1, u2_s1):
{'food_onhand': True},
content_type='application/json'
)
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
'food_onhand'] == True
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
'food_onhand'] == False
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] is True
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] is False
user1 = auth.get_user(u1_s1)
user2 = auth.get_user(u2_s1)
user1.userpreference.shopping_share.add(user2)
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)[
'food_onhand'] == True
caches['default'].set(f'shopping_shared_users_{space_1.id}_{user2.id}', None)
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] is True

View File

@@ -0,0 +1,116 @@
import json
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food, MealType, PropertyType, Property
LIST_URL = 'api:property-list'
DETAIL_URL = 'api:property-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1):
pt = PropertyType.objects.get_or_create(name='test_1', space=space_1)[0]
return Property.objects.get_or_create(property_amount=100, property_type=pt, space=space_1)[0]
@pytest.fixture
def obj_2(space_1, u1_s1):
pt = PropertyType.objects.get_or_create(name='test_2', space=space_1)[0]
return Property.objects.get_or_create(property_amount=100, property_type=pt, space=space_1)[0]
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
['g1_s2', 403],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'property_amount': 200},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['property_amount'] == 200
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2, space_1):
with scopes_disabled():
pt = PropertyType.objects.get_or_create(name='test_1', space=space_1)[0]
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'property_amount': 100, 'property_type': {'id': pt.id, 'name': pt.name}},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['property_amount'] == 100
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert MealType.objects.count() == 0

View File

@@ -0,0 +1,132 @@
import json
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food, MealType, PropertyType
LIST_URL = 'api:propertytype-list'
DETAIL_URL = 'api:propertytype-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1):
return PropertyType.objects.get_or_create(name='test_1', space=space_1)[0]
@pytest.fixture
def obj_2(space_1, u1_s1):
return PropertyType.objects.get_or_create(name='test_2', space=space_1)[0]
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
['g1_s2', 403],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'name': 'new'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['name'] == 'new'
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'name': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['name'] == 'test'
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
def test_add_duplicate(u1_s1, u1_s2, obj_1):
r = u1_s1.post(
reverse(LIST_URL),
{'name': obj_1.name},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == 201
assert response['id'] == obj_1.id
r = u1_s2.post(
reverse(LIST_URL),
{'name': obj_1.name},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == 201
assert response['id'] != obj_1.id
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert MealType.objects.count() == 0

View File

@@ -0,0 +1,163 @@
import json
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import Food, MealType, UnitConversion
from cookbook.tests.conftest import get_random_food, get_random_unit
LIST_URL = 'api:unitconversion-list'
DETAIL_URL = 'api:unitconversion-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1):
return UnitConversion.objects.get_or_create(
food=get_random_food(space_1, u1_s1),
base_amount=100,
base_unit=get_random_unit(space_1, u1_s1),
converted_amount=100,
converted_unit=get_random_unit(space_1, u1_s1),
created_by=auth.get_user(u1_s1),
space=space_1
)[0]
@pytest.fixture
def obj_2(space_1, u1_s1):
return UnitConversion.objects.get_or_create(
food=get_random_food(space_1, u1_s1),
base_amount=100,
base_unit=get_random_unit(space_1, u1_s1),
converted_amount=100,
converted_unit=get_random_unit(space_1, u1_s1),
created_by=auth.get_user(u1_s1),
space=space_1
)[0]
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
obj_1.space = space_2
obj_1.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 1
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
['g1_s2', 403],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, obj_1):
c = request.getfixturevalue(arg[0])
r = c.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'base_amount': 1000},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == arg[1]
if r.status_code == 200:
assert response['base_amount'] == 1000
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, u1_s2, space_1, u1_s1):
with scopes_disabled():
c = request.getfixturevalue(arg[0])
random_unit_1 = get_random_unit(space_1, u1_s1)
random_unit_2 = get_random_unit(space_1, u1_s1)
random_food_1 = get_random_unit(space_1, u1_s1)
r = c.post(
reverse(LIST_URL),
{
'food': {'id': random_food_1.id, 'name': random_food_1.name},
'base_amount': 100,
'base_unit': {'id': random_unit_1.id, 'name': random_unit_1.name},
'converted_amount': 100,
'converted_unit': {'id': random_unit_2.id, 'name': random_unit_2.name}
},
content_type='application/json'
)
response = json.loads(r.content)
print(response)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['base_amount'] == 100
r = c.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 200
r = u1_s2.get(reverse(DETAIL_URL, args={response['id']}))
assert r.status_code == 404
# TODO make name in space unique
# def test_add_duplicate(u1_s1, u1_s2, obj_1):
# r = u1_s1.post(
# reverse(LIST_URL),
# {'name': obj_1.name},
# content_type='application/json'
# )
# response = json.loads(r.content)
# assert r.status_code == 201
# assert response['id'] == obj_1.id
#
# r = u1_s2.post(
# reverse(LIST_URL),
# {'name': obj_1.name},
# content_type='application/json'
# )
# response = json.loads(r.content)
# assert r.status_code == 201
# assert response['id'] != obj_1.id
def test_delete(u1_s1, u1_s2, obj_1):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert MealType.objects.count() == 0

View File

@@ -13,6 +13,8 @@ from cookbook.tests.factories import SpaceFactory, UserFactory
register(SpaceFactory, 'space_1')
register(SpaceFactory, 'space_2')
# register(FoodFactory, space=LazyFixture('space_2'))
# TODO refactor clients to be factories
@@ -169,7 +171,6 @@ def dict_compare(d1, d2, details=False):
def transpose(text, number=2):
# select random token
tokens = text.split()
positions = list(i for i, e in enumerate(tokens) if len(e) > 1)
@@ -212,6 +213,14 @@ def ext_recipe_1_s1(space_1, u1_s1):
return r
def get_random_food(space_1, u1_s1):
return Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0]
def get_random_unit(space_1, u1_s1):
return Unit.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0]
# ---------------------- USER FIXTURES -----------------------
# maybe better with factories but this is very explict so ...

View File

@@ -0,0 +1,129 @@
from django.contrib import auth
from django.core.cache import caches
from django_scopes import scopes_disabled
from decimal import Decimal
from cookbook.helper.cache_helper import CacheHelper
from cookbook.helper.property_helper import FoodPropertyHelper
from cookbook.models import Unit, Food, PropertyType, Property, Recipe, Step, UnitConversion, Property
def test_food_property(space_1, space_2, u1_s1):
with scopes_disabled():
unit_gram = Unit.objects.create(name='gram', base_unit='g', space=space_1)
unit_kg = Unit.objects.create(name='kg', base_unit='kg', space=space_1)
unit_pcs = Unit.objects.create(name='pcs', base_unit='', space=space_1)
unit_floz1 = Unit.objects.create(name='fl. oz 1', base_unit='imperial_fluid_ounce', space=space_1) # US and UK use different volume systems (US vs imperial)
unit_floz2 = Unit.objects.create(name='fl. oz 2', base_unit='fluid_ounce', space=space_1)
unit_fantasy = Unit.objects.create(name='Fantasy Unit', base_unit='', space=space_1)
food_1 = Food.objects.create(name='food_1', space=space_1, properties_food_unit=unit_gram, properties_food_amount=100)
food_2 = Food.objects.create(name='food_2', space=space_1, properties_food_unit=unit_gram, properties_food_amount=100)
property_fat = PropertyType.objects.create(name='property_fat', space=space_1)
property_calories = PropertyType.objects.create(name='property_calories', space=space_1)
property_nuts = PropertyType.objects.create(name='property_nuts', space=space_1)
property_price = PropertyType.objects.create(name='property_price', space=space_1)
food_1_property_fat = Property.objects.create(property_amount=50, property_type=property_fat, space=space_1)
food_1_property_nuts = Property.objects.create(property_amount=1, property_type=property_nuts, space=space_1)
food_1_property_price = Property.objects.create(property_amount=7.50, property_type=property_price, space=space_1)
food_1.properties.add(food_1_property_fat, food_1_property_nuts, food_1_property_price)
food_2_property_fat = Property.objects.create(property_amount=25, property_type=property_fat, space=space_1)
food_2_property_nuts = Property.objects.create(property_amount=0, property_type=property_nuts, space=space_1)
food_2_property_price = Property.objects.create(property_amount=2.50, property_type=property_price, space=space_1)
food_2.properties.add(food_2_property_fat, food_2_property_nuts, food_2_property_price)
print('\n----------- TEST PROPERTY - PROPERTY CALCULATION MULTI STEP IDENTICAL UNIT ---------------')
recipe_1 = Recipe.objects.create(name='recipe_1', waiting_time=0, working_time=0, space=space_1, created_by=auth.get_user(u1_s1))
step_1 = Step.objects.create(instruction='instruction_step_1', space=space_1)
step_1.ingredients.create(amount=500, unit=unit_gram, food=food_1, space=space_1)
step_1.ingredients.create(amount=1000, unit=unit_gram, food=food_2, space=space_1)
recipe_1.steps.add(step_1)
step_2 = Step.objects.create(instruction='instruction_step_1', space=space_1)
step_2.ingredients.create(amount=50, unit=unit_gram, food=food_1, space=space_1)
recipe_1.steps.add(step_2)
property_values = FoodPropertyHelper(space_1).calculate_recipe_properties(recipe_1)
assert property_values[property_fat.id]['name'] == property_fat.name
assert abs(property_values[property_fat.id]['total_value'] - Decimal(525)) < 0.0001
assert abs(property_values[property_fat.id]['food_values'][food_1.id]['value'] - Decimal(275)) < 0.0001
assert abs(property_values[property_fat.id]['food_values'][food_2.id]['value'] - Decimal(250)) < 0.0001
print('\n----------- TEST PROPERTY - PROPERTY CALCULATION NO POSSIBLE CONVERSION ---------------')
recipe_2 = Recipe.objects.create(name='recipe_2', waiting_time=0, working_time=0, space=space_1, created_by=auth.get_user(u1_s1))
step_1 = Step.objects.create(instruction='instruction_step_1', space=space_1)
step_1.ingredients.create(amount=5, unit=unit_pcs, food=food_1, space=space_1)
step_1.ingredients.create(amount=10, unit=unit_pcs, food=food_2, space=space_1)
recipe_2.steps.add(step_1)
property_values = FoodPropertyHelper(space_1).calculate_recipe_properties(recipe_2)
assert property_values[property_fat.id]['name'] == property_fat.name
assert abs(property_values[property_fat.id]['total_value']) < 0.0001
assert abs(property_values[property_fat.id]['food_values'][food_1.id]['value']) < 0.0001
print('\n----------- TEST PROPERTY - PROPERTY CALCULATION UNIT CONVERSION ---------------')
uc1 = UnitConversion.objects.create(
base_amount=100,
base_unit=unit_gram,
converted_amount=1,
converted_unit=unit_pcs,
space=space_1,
created_by=auth.get_user(u1_s1),
)
property_values = FoodPropertyHelper(space_1).calculate_recipe_properties(recipe_2)
assert property_values[property_fat.id]['name'] == property_fat.name
assert abs(property_values[property_fat.id]['total_value'] - Decimal(500)) < 0.0001
assert abs(property_values[property_fat.id]['food_values'][food_1.id]['value'] - Decimal(250)) < 0.0001
assert abs(property_values[property_fat.id]['food_values'][food_2.id]['value'] - Decimal(250)) < 0.0001
print('\n----------- TEST PROPERTY - PROPERTY CALCULATION UNIT CONVERSION MULTIPLE ---------------')
uc1.delete()
uc1 = UnitConversion.objects.create(
base_amount=0.1,
base_unit=unit_kg,
converted_amount=1,
converted_unit=unit_pcs,
space=space_1,
created_by=auth.get_user(u1_s1),
)
property_values = FoodPropertyHelper(space_1).calculate_recipe_properties(recipe_2)
assert property_values[property_fat.id]['name'] == property_fat.name
assert abs(property_values[property_fat.id]['total_value'] - Decimal(500)) < 0.0001
assert abs(property_values[property_fat.id]['food_values'][food_1.id]['value'] - Decimal(250)) < 0.0001
assert abs(property_values[property_fat.id]['food_values'][food_2.id]['value'] - Decimal(250)) < 0.0001
print('\n----------- TEST PROPERTY - MISSING FOOD REFERENCE AMOUNT ---------------')
food_1.properties_food_unit = None
food_1.save()
food_2.properties_food_amount = 0
food_2.save()
property_values = FoodPropertyHelper(space_1).calculate_recipe_properties(recipe_1)
assert property_values[property_fat.id]['name'] == property_fat.name
assert property_values[property_fat.id]['total_value'] == 0
print('\n----------- TEST PROPERTY - SPACE SEPARATION ---------------')
property_fat.space = space_2
property_fat.save()
caches['default'].delete(CacheHelper(space_1).PROPERTY_TYPE_CACHE_KEY) # clear cache as objects won't change space in reality
property_values = FoodPropertyHelper(space_1).calculate_recipe_properties(recipe_2)
assert property_fat.id not in property_values

View File

@@ -0,0 +1,187 @@
from _decimal import Decimal
from django.contrib import auth
from django_scopes import scopes_disabled
from cookbook.helper.unit_conversion_helper import UnitConversionHelper, ConversionException
from cookbook.models import Unit, Food, Ingredient, UnitConversion
def test_base_converter(space_1):
uch = UnitConversionHelper(space_1)
assert abs(uch.convert_from_to('g', 'kg', 1234) - Decimal(1.234)) < 0.0001
assert abs(uch.convert_from_to('kg', 'pound', 2) - Decimal(4.40924)) < 0.00001
assert abs(uch.convert_from_to('kg', 'g', 1) - Decimal(1000)) < 0.00001
assert abs(uch.convert_from_to('imperial_gallon', 'gallon', 1000) - Decimal(1200.95104)) < 0.00001
assert abs(uch.convert_from_to('tbsp', 'ml', 20) - Decimal(295.73549)) < 0.00001
try:
assert uch.convert_from_to('kg', 'tbsp', 2) == 1234
assert False
except ConversionException:
assert True
try:
assert uch.convert_from_to('kg', 'g2', 2) == 1234
assert False
except ConversionException:
assert True
def test_unit_conversions(space_1, space_2, u1_s1):
with scopes_disabled():
uch = UnitConversionHelper(space_1)
uch_space_2 = UnitConversionHelper(space_2)
unit_gram = Unit.objects.create(name='gram', base_unit='g', space=space_1)
unit_kg = Unit.objects.create(name='kg', base_unit='kg', space=space_1)
unit_pcs = Unit.objects.create(name='pcs', base_unit='', space=space_1)
unit_floz1 = Unit.objects.create(name='fl. oz 1', base_unit='imperial_fluid_ounce', space=space_1) # US and UK use different volume systems (US vs imperial)
unit_floz2 = Unit.objects.create(name='fl. oz 2', base_unit='fluid_ounce', space=space_1)
unit_fantasy = Unit.objects.create(name='Fantasy Unit', base_unit='', space=space_1)
food_1 = Food.objects.create(name='Test Food 1', space=space_1)
food_2 = Food.objects.create(name='Test Food 2', space=space_1)
print('\n----------- TEST BASE CONVERSIONS - GRAM ---------------')
ingredient_food_1_gram = Ingredient.objects.create(
food=food_1,
unit=unit_gram,
amount=100,
space=space_1,
)
conversions = uch.get_conversions(ingredient_food_1_gram)
print(conversions)
assert len(conversions) == 2
assert next(x for x in conversions if x.unit == unit_kg) is not None
assert abs(next(x for x in conversions if x.unit == unit_kg).amount - Decimal(0.1)) < 0.0001
print('\n----------- TEST BASE CONVERSIONS - VOLUMES ---------------')
ingredient_food_1_floz1 = Ingredient.objects.create(
food=food_1,
unit=unit_floz1,
amount=100,
space=space_1,
)
conversions = uch.get_conversions(ingredient_food_1_floz1)
assert len(conversions) == 2
assert next(x for x in conversions if x.unit == unit_floz2) is not None
assert abs(next(x for x in conversions if x.unit == unit_floz2).amount - Decimal(96.07599404038842)) < 0.001 # TODO validate value
print(conversions)
unit_pint = Unit.objects.create(name='pint', base_unit='pint', space=space_1)
conversions = uch.get_conversions(ingredient_food_1_floz1)
assert len(conversions) == 3
assert next(x for x in conversions if x.unit == unit_pint) is not None
assert abs(next(x for x in conversions if x.unit == unit_pint).amount - Decimal(6.004749627524276)) < 0.001 # TODO validate value
print(conversions)
print('\n----------- TEST BASE CUSTOM CONVERSION - TO CUSTOM CONVERSION ---------------')
UnitConversion.objects.create(
base_amount=1000,
base_unit=unit_gram,
converted_amount=1337,
converted_unit=unit_fantasy,
space=space_1,
created_by=auth.get_user(u1_s1),
)
conversions = uch.get_conversions(ingredient_food_1_gram)
assert len(conversions) == 3
assert next(x for x in conversions if x.unit == unit_fantasy) is not None
assert abs(next(x for x in conversions if x.unit == unit_fantasy).amount - Decimal('133.700')) < 0.001 # TODO validate value
print(conversions)
print('\n----------- TEST CUSTOM CONVERSION - NO PCS ---------------')
ingredient_food_1_pcs = Ingredient.objects.create(
food=food_1,
unit=unit_pcs,
amount=5,
space=space_1,
)
ingredient_food_2_pcs = Ingredient.objects.create(
food=food_2,
unit=unit_pcs,
amount=5,
space=space_1,
)
assert len(uch.get_conversions(ingredient_food_1_pcs)) == 1
assert len(uch.get_conversions(ingredient_food_2_pcs)) == 1
print(uch.get_conversions(ingredient_food_1_pcs))
print(uch.get_conversions(ingredient_food_2_pcs))
print('\n----------- TEST CUSTOM CONVERSION - PCS TO MULTIPLE BASE ---------------')
uc1 = UnitConversion.objects.create(
base_amount=1,
base_unit=unit_pcs,
converted_amount=200,
converted_unit=unit_gram,
food=food_1,
space=space_1,
created_by=auth.get_user(u1_s1),
)
conversions = uch.get_conversions(ingredient_food_1_pcs)
assert len(conversions) == 3
assert abs(next(x for x in conversions if x.unit == unit_gram).amount - Decimal(1000)) < 0.0001
assert abs(next(x for x in conversions if x.unit == unit_kg).amount - Decimal(1)) < 0.0001
print(conversions)
assert len(uch.get_conversions(ingredient_food_2_pcs)) == 1
print(uch.get_conversions(ingredient_food_2_pcs))
print('\n----------- TEST CUSTOM CONVERSION - CONVERT MULTI STEP ---------------')
# TODO add test for multi step conversion ... do I even do or want to support this ?
print('\n----------- TEST CUSTOM CONVERSION - REVERSE CONVERSION ---------------')
uc2 = UnitConversion.objects.create(
base_amount=200,
base_unit=unit_gram,
converted_amount=1,
converted_unit=unit_pcs,
food=food_2,
space=space_1,
created_by=auth.get_user(u1_s1),
)
conversions = uch.get_conversions(ingredient_food_1_pcs)
assert len(conversions) == 3
assert abs(next(x for x in conversions if x.unit == unit_gram).amount - Decimal(1000)) < 0.0001
assert abs(next(x for x in conversions if x.unit == unit_kg).amount - Decimal(1)) < 0.0001
print(conversions)
conversions = uch.get_conversions(ingredient_food_2_pcs)
assert len(conversions) == 3
assert abs(next(x for x in conversions if x.unit == unit_gram).amount - Decimal(1000)) < 0.0001
assert abs(next(x for x in conversions if x.unit == unit_kg).amount - Decimal(1)) < 0.0001
print(conversions)
print('\n----------- TEST SPACE SEPARATION ---------------')
uc2.space = space_2
uc2.save()
conversions = uch.get_conversions(ingredient_food_2_pcs)
assert len(conversions) == 1
print(conversions)
conversions = uch_space_2.get_conversions(ingredient_food_1_gram)
assert len(conversions) == 1
assert not any(x for x in conversions if x.unit == unit_kg)
print(conversions)
unit_kg_space_2 = Unit.objects.create(name='kg', base_unit='kg', space=space_2)
conversions = uch_space_2.get_conversions(ingredient_food_1_gram)
assert len(conversions) == 2
assert not any(x for x in conversions if x.unit == unit_kg)
assert next(x for x in conversions if x.unit == unit_kg_space_2) is not None
assert abs(next(x for x in conversions if x.unit == unit_kg_space_2).amount - Decimal(0.1)) < 0.0001
print(conversions)

View File

@@ -6,17 +6,23 @@ from rest_framework import permissions, routers
from rest_framework.schemas import get_schema_view
from cookbook.helper import dal
from recipes.settings import DEBUG
from recipes.settings import DEBUG, PLUGINS
from recipes.version import VERSION_NUMBER
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage,
Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UserFile,
get_model_name, UserSpace, Space)
get_model_name, UserSpace, Space, PropertyType, UnitConversion)
from .views import api, data, delete, edit, import_export, lists, new, telegram, views
from .views.api import CustomAuthToken
from .views.api import CustomAuthToken, ImportOpenData
router = routers.DefaultRouter()
# extend DRF default router class to allow including additional routers
class DefaultRouter(routers.DefaultRouter):
def extend(self, r):
self.registry.extend(r.registry)
router = DefaultRouter()
router.register(r'automation', api.AutomationViewSet)
router.register(r'bookmarklet-import', api.BookmarkletImportViewSet)
router.register(r'cook-log', api.CookLogViewSet)
@@ -34,6 +40,9 @@ router.register(r'meal-type', api.MealTypeViewSet)
router.register(r'recipe', api.RecipeViewSet)
router.register(r'recipe-book', api.RecipeBookViewSet)
router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
router.register(r'unit-conversion', api.UnitConversionViewSet)
router.register(r'food-property-type', api.PropertyTypeViewSet)
router.register(r'food-property', api.PropertyViewSet)
router.register(r'shopping-list', api.ShoppingListViewSet)
router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet)
router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet)
@@ -53,6 +62,13 @@ router.register(r'user-space', api.UserSpaceViewSet)
router.register(r'view-log', api.ViewLogViewSet)
router.register(r'access-token', api.AccessTokenViewSet)
for p in PLUGINS:
if c := locate(f'{p["module"]}.urls.{p["api_router_name"]}'):
try:
router.extend(c)
except AttributeError:
pass
urlpatterns = [
path('', views.index, name='index'),
path('setup/', views.setup, name='view_setup'),
@@ -119,7 +135,6 @@ urlpatterns = [
path('api/switch-active-space/<int:space_id>/', api.switch_active_space, name='api_switch_active_space'),
path('api/download-file/<int:file_id>/', api.download_file, name='api_download_file'),
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
# TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
@@ -139,6 +154,7 @@ urlpatterns = [
path('api/', include((router.urls, 'api'))),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('api-token-auth/', CustomAuthToken.as_view()),
path('api-import-open-data/', ImportOpenData.as_view(), name='api_import_open_data'),
path('offline/', views.offline, name='view_offline'),
@@ -189,7 +205,7 @@ for m in generic_models:
)
)
vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation, UserFile, Step, CustomFilter]
vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation, UserFile, Step, CustomFilter, UnitConversion, PropertyType]
for m in vue_models:
py_name = get_model_name(m)
url_name = py_name.replace('_', '-')

View File

@@ -19,6 +19,7 @@ from annoying.functions import get_object_or_None
from django.contrib import messages
from django.contrib.auth.models import Group, User
from django.contrib.postgres.search import TrigramSimilarity
from django.core.cache import caches
from django.core.exceptions import FieldError, ValidationError
from django.core.files import File
from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When, Avg, Max
@@ -44,6 +45,7 @@ from rest_framework.parsers import MultiPartParser
from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
from rest_framework.response import Response
from rest_framework.throttling import AnonRateThrottle
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from treebeard.exceptions import InvalidMoveToDescendant, InvalidPosition, PathOverflow
@@ -52,10 +54,13 @@ from cookbook.helper import recipe_url_import as helper
from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.image_processing import handle_image
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.open_data_importer import OpenDataImporter
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner,
CustomIsOwnerReadOnly, CustomIsShared,
CustomIsSpaceOwner, CustomIsUser, group_required,
is_space_owner, switch_user_active_space, above_space_limit, CustomRecipePermission, CustomUserPermission, CustomTokenHasReadWriteScope, CustomTokenHasScope, has_group_permission)
is_space_owner, switch_user_active_space, above_space_limit,
CustomRecipePermission, CustomUserPermission,
CustomTokenHasReadWriteScope, CustomTokenHasScope, has_group_permission)
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch
from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup, clean_dict
from cookbook.helper.scrapers.scrapers import text_scraper
@@ -65,7 +70,7 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte
MealType, Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog)
SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, PropertyType, Property)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
@@ -88,7 +93,8 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
SupermarketCategorySerializer, SupermarketSerializer,
SyncLogSerializer, SyncSerializer, UnitSerializer,
UserFileSerializer, UserSerializer, UserPreferenceSerializer,
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer, RecipeExportSerializer)
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer,
RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer, PropertySerializer)
from cookbook.views.import_export import get_integration
from recipes import settings
@@ -166,14 +172,17 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space).order_by(Lower('name').asc())
query = self.request.query_params.get('query', None)
if self.request.user.is_authenticated:
fuzzy = self.request.user.searchpreference.lookup or any([self.model.__name__.lower() in x for x in
self.request.user.searchpreference.trigram.values_list(
'field', flat=True)])
else:
fuzzy = True
if query is not None and query not in ["''", '']:
if fuzzy and (settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']):
if any([self.model.__name__.lower() in x for x in
self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
if fuzzy and (settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']):
if self.request.user.is_authenticated and any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name__unaccent', query))
else:
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query))
@@ -181,13 +190,12 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
else:
# TODO have this check unaccent search settings or other search preferences?
filter = Q(name__icontains=query)
if any([self.model.__name__.lower() in x for x in
self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
if self.request.user.is_authenticated:
if any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
filter |= Q(name__unaccent__icontains=query)
self.queryset = (
self.queryset
.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
self.queryset.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
default=Value(0))) # put exact matches at the top of the result set
.filter(filter).order_by('-starts', Lower('name').asc())
)
@@ -243,6 +251,9 @@ class MergeMixin(ViewSetMixin):
isTree = False
try:
if isinstance(source, Food):
source.properties.through.objects.all().delete()
for link in [field for field in source._meta.get_fields() if issubclass(type(field), ForeignObjectRel)]:
linkManager = getattr(source, link.get_accessor_name())
related = linkManager.all()
@@ -272,6 +283,7 @@ class MergeMixin(ViewSetMixin):
source.delete()
return Response(content, status=status.HTTP_200_OK)
except Exception:
traceback.print_exc()
content = {'error': True,
'msg': _(f'An error occurred attempting to merge {source.name} with {target.name}')}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
@@ -522,8 +534,20 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
pagination_class = DefaultPagination
def get_queryset(self):
self.request._shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [
shared_users = []
if c := caches['default'].get(
f'shopping_shared_users_{self.request.space.id}_{self.request.user.id}', None):
shared_users = c
else:
try:
shared_users = [x.id for x in list(self.request.user.get_shopping_share())] + [
self.request.user.id]
caches['default'].set(
f'shopping_shared_users_{self.request.space.id}_{self.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
self.queryset = super().get_queryset()
shopping_status = ShoppingListEntry.objects.filter(space=self.request.space, food=OuterRef('id'),
@@ -792,7 +816,32 @@ class RecipeViewSet(viewsets.ModelViewSet):
if self.detail: # if detail request and not list, private condition is verified by permission class
if not share: # filter for space only if not shared
self.queryset = self.queryset.filter(space=self.request.space)
self.queryset = self.queryset.filter(space=self.request.space).prefetch_related(
'keywords',
'shared',
'properties',
'properties__property_type',
'steps',
'steps__ingredients',
'steps__ingredients__step_set',
'steps__ingredients__step_set__recipe_set',
'steps__ingredients__food',
'steps__ingredients__food__properties',
'steps__ingredients__food__properties__property_type',
'steps__ingredients__food__inherit_fields',
'steps__ingredients__food__supermarket_category',
'steps__ingredients__food__onhand_users',
'steps__ingredients__food__substitute',
'steps__ingredients__food__child_inherit_fields',
'steps__ingredients__unit',
'steps__ingredients__unit__unit_conversion_base_relation',
'steps__ingredients__unit__unit_conversion_base_relation__base_unit',
'steps__ingredients__unit__unit_conversion_converted_relation',
'steps__ingredients__unit__unit_conversion_converted_relation__converted_unit',
'cooklog_set',
).select_related('nutrition')
return super().get_queryset()
self.queryset = self.queryset.filter(space=self.request.space).filter(
@@ -802,7 +851,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x
in list(self.request.GET)}
search = RecipeSearch(self.request, **params)
self.queryset = search.get_queryset(self.queryset).prefetch_related('cooklog_set')
self.queryset = search.get_queryset(self.queryset).prefetch_related('keywords', 'cooklog_set')
return self.queryset
def list(self, request, *args, **kwargs):
@@ -921,6 +970,41 @@ class RecipeViewSet(viewsets.ModelViewSet):
return Response(self.serializer_class(qs, many=True).data)
class UnitConversionViewSet(viewsets.ModelViewSet):
queryset = UnitConversion.objects
serializer_class = UnitConversionSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
query_params = [
QueryParam(name='food_id', description='ID of food to filter for', qtype='int'),
]
schema = QueryParamAutoSchema()
def get_queryset(self):
food_id = self.request.query_params.get('food_id', None)
if food_id is not None:
self.queryset = self.queryset.filter(food_id=food_id)
return self.queryset.filter(space=self.request.space)
class PropertyTypeViewSet(viewsets.ModelViewSet):
queryset = PropertyType.objects
serializer_class = PropertyTypeSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
def get_queryset(self):
return self.queryset.filter(space=self.request.space)
class PropertyViewSet(viewsets.ModelViewSet):
queryset = Property.objects
serializer_class = PropertySerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
def get_queryset(self):
return self.queryset.filter(space=self.request.space)
class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
queryset = ShoppingListRecipe.objects
serializer_class = ShoppingListRecipeSerializer
@@ -1122,10 +1206,13 @@ class CustomAuthToken(ObtainAuthToken):
context={'request': request})
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
if token := AccessToken.objects.filter(user=user, expires__gt=timezone.now(), scope__contains='read').filter(scope__contains='write').first():
if token := AccessToken.objects.filter(user=user, expires__gt=timezone.now(), scope__contains='read').filter(
scope__contains='write').first():
access_token = token
else:
access_token = AccessToken.objects.create(user=user, token=f'tda_{str(uuid.uuid4()).replace("-", "_")}', expires=(timezone.now() + timezone.timedelta(days=365 * 5)), scope='read write app')
access_token = AccessToken.objects.create(user=user, token=f'tda_{str(uuid.uuid4()).replace("-", "_")}',
expires=(timezone.now() + timezone.timedelta(days=365 * 5)),
scope='read write app')
return Response({
'id': access_token.id,
'token': access_token.token,
@@ -1153,7 +1240,8 @@ def recipe_from_source(request):
serializer = RecipeFromSourceSerializer(data=request.data)
if serializer.is_valid():
if (b_pk := serializer.validated_data.get('bookmarklet', None)) and (bookmarklet := BookmarkletImport.objects.filter(pk=b_pk).first()):
if (b_pk := serializer.validated_data.get('bookmarklet', None)) and (
bookmarklet := BookmarkletImport.objects.filter(pk=b_pk).first()):
serializer.validated_data['url'] = bookmarklet.url
serializer.validated_data['data'] = bookmarklet.html
bookmarklet.delete()
@@ -1175,13 +1263,22 @@ def recipe_from_source(request):
# 'recipe_html': '',
'recipe_images': [],
}, status=status.HTTP_200_OK)
if re.match('^(.)*/view/recipe/[0-9]+/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', url):
recipe_json = requests.get(url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1], '') + '?share=' + re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
if re.match(
'^(.)*/view/recipe/[0-9]+/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
url):
recipe_json = requests.get(
url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1],
'') + '?share=' +
re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
recipe_json = clean_dict(recipe_json, 'id')
serialized_recipe = RecipeExportSerializer(data=recipe_json, context={'request': request})
if serialized_recipe.is_valid():
recipe = serialized_recipe.save()
recipe.image = File(handle_image(request, File(io.BytesIO(requests.get(recipe_json['image']).content), name='image'), filetype=pathlib.Path(recipe_json['image']).suffix),
if validators.url(recipe_json['image'], public=True):
recipe.image = File(handle_image(request,
File(io.BytesIO(requests.get(recipe_json['image']).content),
name='image'),
filetype=pathlib.Path(recipe_json['image']).suffix),
name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}')
recipe.save()
return Response({
@@ -1323,11 +1420,44 @@ def import_files(request):
return Response({'import_id': il.pk}, status=status.HTTP_200_OK)
except NotImplementedError:
return Response({'error': True, 'msg': _('Importing is not implemented for this provider')}, status=status.HTTP_400_BAD_REQUEST)
return Response({'error': True, 'msg': _('Importing is not implemented for this provider')},
status=status.HTTP_400_BAD_REQUEST)
else:
return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST)
class ImportOpenData(APIView):
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
def get(self, request, format=None):
response = requests.get('https://raw.githubusercontent.com/TandoorRecipes/open-tandoor-data/main/build/meta.json')
metadata = json.loads(response.content)
return Response(metadata)
def post(self, request, *args, **kwargs):
# TODO validate data
print(request.data)
selected_version = request.data['selected_version']
selected_datatypes = request.data['selected_datatypes']
update_existing = str2bool(request.data['update_existing'])
use_metric = str2bool(request.data['use_metric'])
response = requests.get(f'https://raw.githubusercontent.com/TandoorRecipes/open-tandoor-data/main/build/{selected_version}.json') # TODO catch 404, timeout, ...
data = json.loads(response.content)
response_obj = {}
data_importer = OpenDataImporter(request, data, update_existing=update_existing, use_metric=use_metric)
response_obj['unit'] = len(data_importer.import_units())
response_obj['category'] = len(data_importer.import_category())
response_obj['property'] = len(data_importer.import_property())
response_obj['store'] = len(data_importer.import_supermarket())
response_obj['food'] = len(data_importer.import_food())
response_obj['conversion'] = len(data_importer.import_conversion())
return Response(response_obj)
def get_recipe_provider(recipe):
if recipe.storage.method == Storage.DROPBOX:
return Dropbox

View File

@@ -228,3 +228,33 @@ def step(request):
}
}
)
@group_required('user')
def unit_conversion(request):
# model-name is the models.js name of the model, probably ALL-CAPS
return render(
request,
'generic/model_template.html',
{
"title": _("Unit Conversions"),
"config": {
'model': "UNIT_CONVERSION", # *REQUIRED* name of the model in models.js
}
}
)
@group_required('user')
def property_type(request):
# model-name is the models.js name of the model, probably ALL-CAPS
return render(
request,
'generic/model_template.html',
{
"title": _("Property Types"),
"config": {
'model': "PROPERTY_TYPE", # *REQUIRED* name of the model in models.js
}
}
)

View File

@@ -1,14 +1,11 @@
import os
import re
import uuid
from datetime import datetime
from uuid import UUID
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.models import Group
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
@@ -18,11 +15,9 @@ from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from oauth2_provider.models import AccessToken
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm,
SpaceCreateForm, SpaceJoinForm, User,
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm)
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, SpaceCreateForm, SpaceJoinForm, User,
UserCreateForm, UserPreference)
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid, switch_user_active_space
from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference, ShareLink,
Space, ViewLog, UserSpace)

View File

@@ -239,6 +239,9 @@ RecetteTek exports are `.rtk` files which can simply be uploaded to tandoor to i
## Rezeptsuite.de
Rezeptsuite.de exports are `.xml` files which can simply be uploaded to tandoor to import all your recipes.
It appears that Reptsuite, depending on the client, might export a `.zip` file containing a `.cml` file.
If this happens just unzip the zip file and change `.cml` to `.xml` to import your recipes.
## Melarecipes
Melarecipes provides multiple export formats but only the `MelaRecipes` format can export the complete collection.

View File

@@ -40,6 +40,7 @@ nav:
- Templating: features/templating.md
- Shopping: features/shopping.md
- Authentication: features/authentication.md
- Automation: features/automation.md
- Storages and Sync: features/external_recipes.md
- Import/Export: features/import_export.md
- System:

View File

@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/2.0/ref/settings/
"""
import ast
import json
import mimetypes
import os
import re
import sys
@@ -80,6 +81,8 @@ DJANGO_TABLES2_PAGE_RANGE = 8
HCAPTCHA_SITEKEY = os.getenv('HCAPTCHA_SITEKEY', '')
HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
FDA_API_KEY = os.getenv('FDA_API_KEY', 'DEMO_KEY')
SHARING_ABUSE = bool(int(os.getenv('SHARING_ABUSE', False)))
SHARING_LIMIT = int(os.getenv('SHARING_LIMIT', 0))
@@ -144,6 +147,9 @@ try:
'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d),
'base_url': plugin_class.base_url,
'bundle_name': plugin_class.bundle_name if hasattr(plugin_class, 'bundle_name') else '',
'api_router_name': plugin_class.api_router_name if hasattr(plugin_class, 'api_router_name') else '',
'nav_main': plugin_class.nav_main if hasattr(plugin_class, 'nav_main') else '',
'nav_dropdown': plugin_class.nav_dropdown if hasattr(plugin_class, 'nav_dropdown') else '',
}
PLUGINS.append(plugin_config)
except Exception:
@@ -412,7 +418,7 @@ for p in PLUGINS:
if p['bundle_name'] != '':
WEBPACK_LOADER[p['bundle_name']] = {
'CACHE': not DEBUG,
'BUNDLE_DIR_NAME': f'{p["base_path"]}/vue/', # must end with slash
'BUNDLE_DIR_NAME': f'vue/', # must end with slash
'STATS_FILE': os.path.join(p["base_path"], 'vue', 'webpack-stats.json'),
'POLL_INTERVAL': 0.1,
'TIMEOUT': None,
@@ -445,6 +451,7 @@ LANGUAGES = [
('hu', _('Hungarian')),
('it', _('Italian')),
('lv', _('Latvian')),
('nb', _('Norwegian ')),
('pl', _('Polish')),
('ru', _('Russian')),
('es', _('Spanish')),
@@ -514,3 +521,5 @@ EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False)))
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv(
'ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix
mimetypes.add_type("text/javascript", ".js", True)

View File

@@ -1,5 +1,5 @@
Django==4.1.7
cryptography==39.0.1
Django==4.1.9
cryptography==41.0.0
django-annoying==0.10.6
django-autocomplete-light==3.9.4
django-cleanup==7.0.0
@@ -17,7 +17,7 @@ Markdown==3.4.3
Pillow==9.4.0
psycopg2-binary==2.9.5
python-dotenv==0.21.0
requests==2.28.2
requests==2.31.0
six==1.16.0
webdavclient3==3.14.6
whitenoise==6.2.0

View File

@@ -31,7 +31,7 @@
</div>
</div>
<!-- Image and misc properties -->
<!-- Image and misc -->
<div class="row pt-2">
<div class="col-md-6" style="max-height: 50vh; min-height: 30vh">
<input id="id_file_upload" ref="file_upload" type="file" hidden
@@ -99,65 +99,53 @@
</div>
</div>
<!-- Nutrition -->
<div class="row pt-2">
<div class="col-md-12">
<div class="card border-grey">
<div class="card-header" style="display: table">
<div class="row">
<div class="col-md-9 d-table">
<h5 class="d-table-cell align-middle">{{ $t("Nutrition") }}</h5>
<div class="card mt-2 mb-2">
<div class="card-body pr-2 pl-2 pr-md-5 pl-md-5 pt-3 pb-3">
<h6>{{ $t('Properties') }} <small class="text-muted"> {{$t('per_serving')}}</small></h6>
<div class="alert alert-info" role="alert">
{{ $t('recipe_property_info')}}
</div>
<div class="d-flex mt-2" v-for="p in recipe.properties" v-bind:key="p.id">
<div class="flex-fill w-50">
<generic-multiselect
@change="p.property_type = $event.val"
:initial_single_selection="p.property_type"
:label="'name'"
:model="Models.PROPERTY_TYPE"
:limit="25"
:multiple="false"
></generic-multiselect>
</div>
<div class="flex-fill w-50">
<div class="input-group">
<input type="number" class="form-control" v-model="p.property_amount">
<div class="input-group-append">
<span class="input-group-text" v-if="p.property_type !== null && p.property_type.unit !== ''">{{ p.property_type.unit }}</span>
<button class="btn btn-danger" @click="deleteProperty(p)"><i class="fa fa-trash fa-fw"></i></button>
</div>
</div>
</div>
</div>
<div class="flex-row mt-2">
<div class="flex-column w-25 offset-4">
<button class="btn btn-success btn-block" @click="addProperty()"><i class="fa fa-plus"></i></button>
</div>
</div>
</div>
<div class="col-md-3">
<button
type="button"
@click="addNutrition()"
v-if="recipe.nutrition === null"
v-b-tooltip.hover
v-bind:title="$t('Add_nutrition_recipe')"
class="btn btn-sm btn-success shadow-none float-right"
>
<i class="fas fa-plus-circle"></i>
</button>
<button
type="button"
@click="removeNutrition()"
v-if="recipe.nutrition !== null"
v-b-tooltip.hover
v-bind:title="$t('Remove_nutrition_recipe')"
class="btn btn-sm btn-danger shadow-none float-right"
>
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
</div>
<b-collapse id="id_nutrition_collapse" class="mt-2" v-model="nutrition_visible">
<div class="card-body" v-if="recipe.nutrition !== null">
<b-alert show>
There is currently only very basic support for tracking nutritional information. A
<a href="https://github.com/vabene1111/recipes/issues/896" target="_blank"
rel="noreferrer nofollow">big update</a> is planned to improve on this in many
different areas.
</b-alert>
<div class="row pt-2">
<div class="col-md-12">
<label for="id_name"> {{ $t(energy()) }}</label>
<input class="form-control" id="id_calories" v-model="recipe.nutrition.calories"/>
<label for="id_name"> {{ $t("Carbohydrates") }}</label>
<input class="form-control" id="id_carbohydrates"
v-model="recipe.nutrition.carbohydrates"/>
<label for="id_name"> {{ $t("Fats") }}</label>
<input class="form-control" id="id_fats" v-model="recipe.nutrition.fats"/>
<label for="id_name"> {{ $t("Proteins") }}</label>
<input class="form-control" id="id_proteins" v-model="recipe.nutrition.proteins"/>
</div>
</b-collapse>
</div>
<b-card-header header-tag="header" class="p-1" role="tab">
<b-button squared block v-b-toggle.additional_collapse class="text-left"
variant="outline-primary">{{ $t("additional_options") }}
@@ -1121,6 +1109,14 @@ export default {
let new_keyword = {label: tag, name: tag}
this.recipe.keywords.push(new_keyword)
},
addProperty: function () {
this.recipe.properties.push(
{'property_amount': 0, 'property_type': null}
)
},
deleteProperty: function (recipe_property) {
this.recipe.properties = this.recipe.properties.filter(p => p.id !== recipe_property.id)
},
searchKeywords: function (query) {
let apiFactory = new ApiApiFactory()

View File

@@ -1,158 +1,6 @@
<template>
<div id="app">
<template v-if="loading">
<loading-spinner></loading-spinner>
</template>
<div v-if="!loading" style="padding-bottom: 60px">
<RecipeSwitcher ref="ref_recipe_switcher" @switch="quickSwitch($event)"/>
<div class="row">
<div class="col-12" style="text-align: center">
<h3>{{ recipe.name }}</h3>
</div>
</div>
<div class="row text-center">
<div class="col col-md-12">
<recipe-rating :recipe="recipe"></recipe-rating>
<last-cooked :recipe="recipe" class="mt-2"></last-cooked>
</div>
</div>
<div class="my-auto">
<div class="col-12" style="text-align: center">
<i>{{ recipe.description }}</i>
</div>
</div>
<div style="text-align: center">
<keywords-component :recipe="recipe"></keywords-component>
</div>
<hr/>
<div class="row align-items-center">
<div class="col col-md-3">
<div class="d-flex">
<div class="my-auto mr-1">
<i class="fas fa-fw fa-user-clock fa-2x text-primary"></i>
</div>
<div class="my-auto mr-1">
<span class="text-primary"><b>{{ $t("Preparation") }}</b></span><br/>
{{ working_time }}
</div>
</div>
</div>
<div class="col col-md-3">
<div class="row d-flex">
<div class="my-auto mr-1">
<i class="far fa-fw fa-clock fa-2x text-primary"></i>
</div>
<div class="my-auto mr-1">
<span class="text-primary"><b>{{ $t("Waiting") }}</b></span><br/>
{{ waiting_time }}
</div>
</div>
</div>
<div class="col col-md-4 col-10 mt-2 mt-md-0">
<div class="d-flex">
<div class="my-auto mr-1">
<i class="fas fa-fw fa-pizza-slice fa-2x text-primary"></i>
</div>
<div class="my-auto mr-1">
<CustomInputSpinButton v-model.number="servings"/>
</div>
<div class="my-auto mr-1">
<span class="text-primary">
<b>
<template v-if="recipe.servings_text === ''">{{ $t("Servings") }}</template>
<template v-else>{{ recipe.servings_text }}</template>
</b>
</span>
</div>
</div>
</div>
<div class="col col-md-2 col-2 mt-2 mt-md-0 text-right">
<recipe-context-menu v-bind:recipe="recipe" :servings="servings"
:disabled_options="{print:false}"></recipe-context-menu>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2"
v-if="recipe && ingredient_count > 0 && (recipe.show_ingredient_overview || recipe.steps.length < 2)">
<ingredients-card
:recipe="recipe.id"
:steps="recipe.steps"
:ingredient_factor="ingredient_factor"
:servings="servings"
:header="true"
id="ingredient_container"
@checked-state-changed="updateIngredientCheckedState"
@change-servings="servings = $event"
/>
</div>
<div class="col-12 order-1 col-sm-12 order-sm-1 col-md-6 order-md-2">
<div class="row">
<div class="col-12">
<img class="img img-fluid rounded" :src="recipe.image" :alt="$t('Recipe_Image')"
v-if="recipe.image !== null" @load="onImgLoad"
:style="{ 'max-height': ingredient_height }"/>
</div>
</div>
</div>
</div>
<template v-if="!recipe.internal">
<div v-if="recipe.file_path.includes('.pdf')">
<PdfViewer :recipe="recipe"></PdfViewer>
</div>
<div
v-if="recipe.file_path.includes('.png') || recipe.file_path.includes('.jpg') || recipe.file_path.includes('.jpeg') || recipe.file_path.includes('.gif')">
<ImageViewer :recipe="recipe"></ImageViewer>
</div>
</template>
<div v-for="(s, index) in recipe.steps" v-bind:key="s.id" style="margin-top: 1vh">
<step-component
:recipe="recipe"
:step="s"
:ingredient_factor="ingredient_factor"
:index="index"
:start_time="start_time"
@update-start-time="updateStartTime"
@checked-state-changed="updateIngredientCheckedState"
></step-component>
</div>
<div v-if="recipe.source_url !== null">
<h6 class="d-print-none"><i class="fas fa-file-import"></i> {{ $t("Imported_From") }}</h6>
<span class="text-muted mt-1"><a style="overflow-wrap: break-word;"
:href="recipe.source_url">{{ recipe.source_url }}</a></span>
</div>
<div class="row" style="margin-top: 2vh; ">
<div class="col-lg-6 offset-lg-3 col-12">
<Nutrition-component :recipe="recipe" id="nutrition_container"
:ingredient_factor="ingredient_factor"></Nutrition-component>
</div>
</div>
</div>
<add-recipe-to-book :recipe="recipe"></add-recipe-to-book>
<div class="row text-center d-print-none" style="margin-top: 3vh; margin-bottom: 3vh"
v-if="share_uid !== 'None' && !loading">
<div class="col col-md-12">
<import-tandoor></import-tandoor> <br/>
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)" class="mt-3">{{ $t("Report Abuse") }}</a>
</div>
</div>
<recipe-view-component></recipe-view-component>
<bottom-navigation-bar></bottom-navigation-bar>
</div>
@@ -163,192 +11,28 @@ import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {apiLoadRecipe} from "@/utils/api"
import RecipeContextMenu from "@/components/RecipeContextMenu"
import {ResolveUrlMixin, ToastMixin, calculateHourMinuteSplit} from "@/utils/utils"
import PdfViewer from "@/components/PdfViewer"
import ImageViewer from "@/components/ImageViewer"
import moment from "moment"
import LoadingSpinner from "@/components/LoadingSpinner"
import AddRecipeToBook from "@/components/Modals/AddRecipeToBook"
import RecipeRating from "@/components/RecipeRating"
import LastCooked from "@/components/LastCooked"
import IngredientsCard from "@/components/IngredientsCard"
import StepComponent from "@/components/StepComponent"
import KeywordsComponent from "@/components/KeywordsComponent"
import NutritionComponent from "@/components/NutritionComponent"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import CustomInputSpinButton from "@/components/CustomInputSpinButton"
import {ApiApiFactory} from "@/utils/openapi/api";
import ImportTandoor from "@/components/Modals/ImportTandoor.vue";
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
Vue.prototype.moment = moment
import RecipeViewComponent from "@/components/RecipeViewComponent.vue";
Vue.use(BootstrapVue)
export default {
name: "RecipeView",
mixins: [ResolveUrlMixin, ToastMixin],
mixins: [],
components: {
ImportTandoor,
LastCooked,
RecipeRating,
PdfViewer,
ImageViewer,
IngredientsCard,
StepComponent,
RecipeContextMenu,
NutritionComponent,
KeywordsComponent,
LoadingSpinner,
AddRecipeToBook,
RecipeSwitcher,
CustomInputSpinButton,
BottomNavigationBar,
},
computed: {
ingredient_factor: function () {
return this.servings / this.recipe.servings
},
ingredient_count() {
return this.recipe?.steps.map((x) => x.ingredients).flat().length
},
working_time: function () {
return calculateHourMinuteSplit(this.recipe.working_time)
},
waiting_time: function () {
return calculateHourMinuteSplit(this.recipe.waiting_time)
},
RecipeViewComponent
},
computed: {},
data() {
return {
loading: true,
recipe: undefined,
rootrecipe: undefined,
servings: 1,
servings_cache: {},
start_time: "",
share_uid: window.SHARE_UID,
wake_lock: null,
ingredient_height: '250',
}
},
watch: {
servings(newVal, oldVal) {
this.servings_cache[this.recipe.id] = this.servings
},
return {}
},
mounted() {
this.loadRecipe(window.RECIPE_ID)
this.$i18n.locale = window.CUSTOM_LOCALE
this.requestWakeLock()
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() {
this.destroyWakeLock()
},
methods: {
requestWakeLock: async function () {
if ('wakeLock' in navigator) {
try {
this.wake_lock = await navigator.wakeLock.request('screen')
document.addEventListener('visibilitychange', this.visibilityChange)
} catch (err) {
console.log(err)
}
}
},
handleResize: function () {
if (document.getElementById('nutrition_container') !== null) {
this.ingredient_height = document.getElementById('ingredient_container').clientHeight - document.getElementById('nutrition_container').clientHeight
} else {
this.ingredient_height = document.getElementById('ingredient_container').clientHeight
}
},
destroyWakeLock: function () {
if (this.wake_lock != null) {
this.wake_lock.release()
.then(() => {
this.wake_lock = null
});
}
document.removeEventListener('visibilitychange', this.visibilityChange)
},
visibilityChange: async function () {
if (this.wake_lock != null && document.visibilityState === 'visible') {
await this.requestWakeLock()
}
},
loadRecipe: function (recipe_id) {
apiLoadRecipe(recipe_id).then((recipe) => {
let total_time = 0
for (let step of recipe.steps) {
for (let ingredient of step.ingredients) {
this.$set(ingredient, "checked", false)
}
step.time_offset = total_time
total_time += step.time
}
// set start time only if there are any steps with timers (otherwise no timers are rendered)
if (total_time > 0) {
this.start_time = moment().format("yyyy-MM-DDTHH:mm")
}
if (recipe.image === null) this.printReady()
this.recipe = this.rootrecipe = recipe
this.servings = this.servings_cache[this.rootrecipe.id] = recipe.servings
this.loading = false
setTimeout(() => {
this.handleResize()
}, 100)
})
},
updateStartTime: function (e) {
this.start_time = e
},
updateIngredientCheckedState: function (e) {
for (let step of this.recipe.steps) {
for (let ingredient of step.ingredients) {
if (ingredient.id === e.id) {
this.$set(ingredient, "checked", !ingredient.checked)
}
}
}
},
quickSwitch: function (e) {
if (e === -1) {
this.recipe = this.rootrecipe
this.servings = this.servings_cache[this.rootrecipe?.id ?? 1]
} else {
this.recipe = e
this.servings = this.servings_cache?.[e.id] ?? e.servings
}
},
printReady: function () {
const template = document.createElement("template");
template.id = "printReady";
document.body.appendChild(template);
},
onImgLoad: function () {
this.printReady()
},
},
methods: {},
}
</script>
<style>
#app > div > div {
break-inside: avoid;
}
</style>

View File

@@ -164,6 +164,15 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<h4>{{ $t('Open_Data_Import') }}</h4>
<open-data-import-component></open-data-import-component>
</div>
</div>
<div class="row mt-4">
<div class="col col-12">
<h4 class="mt-2"><i class="fas fa-trash"></i> {{ $t('Delete') }}</h4>
@@ -198,6 +207,7 @@ import GenericMultiselect from "@/components/GenericMultiselect";
import GenericModalForm from "@/components/Modals/GenericModalForm";
import axios from "axios";
import VueClipboard from 'vue-clipboard2'
import OpenDataImportComponent from "@/components/OpenDataImportComponent.vue";
Vue.use(VueClipboard)
@@ -206,7 +216,7 @@ Vue.use(BootstrapVue)
export default {
name: "SpaceManageView",
mixins: [ResolveUrlMixin, ToastMixin, ApiMixin],
components: {GenericMultiselect, GenericModalForm},
components: {GenericMultiselect, GenericModalForm, OpenDataImportComponent},
data() {
return {
ACTIVE_SPACE_ID: window.ACTIVE_SPACE_ID,

View File

@@ -1,104 +1,42 @@
<template>
<div id="app">
<div class="row" v-if="food">
<div class="col-12">
<h2>{{ food.name }}</h2>
</div>
<beta-warning></beta-warning>
<div v-if="metadata !== undefined">
{{ $t('Data_Import_Info') }}
<select class="form-control" v-model="selected_version">
<option v-for="v in metadata.versions" v-bind:key="v">{{ v }}</option>
</select>
<b-checkbox v-model="update_existing" class="mt-1">{{ $t('Update_Existing_Data') }}</b-checkbox>
<b-checkbox v-model="use_metric" class="mt-1">{{ $t('Use_Metric') }}</b-checkbox>
<div v-if="selected_version !== undefined" class="mt-3">
<table class="table">
<tr>
<th>{{ $t('Datatype') }}</th>
<th>{{ $t('Number of Objects') }}</th>
<th>{{ $t('Imported') }}</th>
</tr>
<tr v-for="d in metadata.datatypes" v-bind:key="d">
<td>{{ $t(d.charAt(0).toUpperCase() + d.slice(1)) }}</td>
<td>{{ metadata[selected_version][d] }}</td>
<td>
<template v-if="import_count !== undefined">{{ import_count[d] }}</template>
</td>
</tr>
</table>
<button class="btn btn-success" @click="doImport">{{ $t('Import') }}</button>
</div>
<div class="row">
<div class="col-12">
<b-form v-if="food">
<b-form-group :label="$t('Name')" description="">
<b-form-input v-model="food.name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Plural')" description="">
<b-form-input v-model="food.plural_name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Recipe')" :description="$t('food_recipe_help')">
<generic-multiselect
@change="food.recipe = $event.val;"
:model="Models.RECIPE"
:initial_selection="food.recipe"
label="name"
:multiple="false"
:placeholder="$t('Recipe')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('OnHand_help')">
<b-form-checkbox v-model="food.food_onhand">{{ $t('OnHand') }}</b-form-checkbox>
</b-form-group>
<b-form-group :description="$t('ignore_shopping_help')">
<b-form-checkbox v-model="food.ignore_shopping">{{ $t('Ignore_Shopping') }}</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('Shopping_Category')" :description="$t('shopping_category_help')">
<generic-multiselect
@change="food.supermarket_category = $event.val;"
:model="Models.SHOPPING_CATEGORY"
:initial_selection="food.supermarket_category"
label="name"
:multiple="false"
:placeholder="$t('Shopping_Category')"
></generic-multiselect>
</b-form-group>
<hr/>
<!-- todo add conditions if false disable dont hide -->
<b-form-group :label="$t('Substitutes')" :description="$t('substitute_help')">
<generic-multiselect
@change="food.substitute = $event.val;"
:model="Models.FOOD"
:initial_selection="food.substitute"
label="name"
:multiple="false"
:placeholder="$t('Substitutes')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('substitute_siblings_help')">
<b-form-checkbox v-model="food.substitute_siblings">{{ $t('substitute_siblings') }}</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('InheritFields')" :description="$t('InheritFields_help')">
<generic-multiselect
@change="food.inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_selection="food.inherit_fields"
label="name"
:multiple="false"
:placeholder="$t('InheritFields')"
></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('ChildInheritFields')" :description="$t('ChildInheritFields_help')">
<generic-multiselect
@change="food.child_inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_selection="food.child_inherit_fields"
label="name"
:multiple="false"
:placeholder="$t('ChildInheritFields')"
></generic-multiselect>
</b-form-group>
<!-- TODO change to a button -->
<b-form-group :description="$t('reset_children_help')">
<b-form-checkbox v-model="food.reset_inherit">{{ $t('reset_children') }}</b-form-checkbox>
</b-form-group>
<b-button variant="primary" @click="updateFood">{{ $t('Save') }}</b-button>
</b-form>
</div>
</div>
</div>
</template>
@@ -107,10 +45,9 @@ import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiApiFactory} from "@/utils/openapi/api";
import RecipeCard from "@/components/RecipeCard.vue";
import GenericMultiselect from "@/components/GenericMultiselect.vue";
import {ApiMixin, StandardToasts} from "@/utils/utils";
import {ApiMixin, resolveDjangoUrl, StandardToasts} from "@/utils/utils";
import axios from "axios";
import BetaWarning from "@/components/BetaWarning.vue";
Vue.use(BootstrapVue)
@@ -119,33 +56,39 @@ Vue.use(BootstrapVue)
export default {
name: "TestView",
mixins: [ApiMixin],
components: {
GenericMultiselect
},
components: {BetaWarning},
data() {
return {
food: undefined,
metadata: undefined,
selected_version: undefined,
update_existing: true,
use_metric: true,
import_count: undefined,
}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
let apiClient = new ApiApiFactory()
apiClient.retrieveFood('1').then((r) => {
this.food = r.data
})
axios.get(resolveDjangoUrl('api_import_open_data')).then(r => {
this.metadata = r.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
methods: {
updateFood: function () {
let apiClient = new ApiApiFactory()
apiClient.updateFood(this.food.id, this.food).then((r) => {
this.food = r.data
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
doImport: function () {
axios.post(resolveDjangoUrl('api_import_open_data'), {
'selected_version': this.selected_version,
'selected_datatypes': this.metadata.datatypes,
'update_existing': this.update_existing,
'use_metric': this.use_metric,
}).then(r => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
this.import_count = r.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)
})
}
},
},
}
</script>

View File

@@ -0,0 +1,155 @@
<template>
<div id="app">
<div class="row" v-if="food">
<div class="col-12">
<h2>{{ food.name }}</h2>
</div>
</div>
<div class="row">
<div class="col-12">
<b-form v-if="food">
<b-form-group :label="$t('Name')" description="">
<b-form-input v-model="food.name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Plural')" description="">
<b-form-input v-model="food.plural_name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Recipe')" :description="$t('food_recipe_help')">
<generic-multiselect
@change="food.recipe = $event.val;"
:model="Models.RECIPE"
:initial_selection="food.recipe"
label="name"
:multiple="false"
:placeholder="$t('Recipe')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('OnHand_help')">
<b-form-checkbox v-model="food.food_onhand">{{ $t('OnHand') }}</b-form-checkbox>
</b-form-group>
<b-form-group :description="$t('ignore_shopping_help')">
<b-form-checkbox v-model="food.ignore_shopping">{{ $t('Ignore_Shopping') }}</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('Shopping_Category')" :description="$t('shopping_category_help')">
<generic-multiselect
@change="food.supermarket_category = $event.val;"
:model="Models.SHOPPING_CATEGORY"
:initial_selection="food.supermarket_category"
label="name"
:multiple="false"
:placeholder="$t('Shopping_Category')"
></generic-multiselect>
</b-form-group>
<hr/>
<!-- todo add conditions if false disable dont hide -->
<b-form-group :label="$t('Substitutes')" :description="$t('substitute_help')">
<generic-multiselect
@change="food.substitute = $event.val;"
:model="Models.FOOD"
:initial_selection="food.substitute"
label="name"
:multiple="false"
:placeholder="$t('Substitutes')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('substitute_siblings_help')">
<b-form-checkbox v-model="food.substitute_siblings">{{ $t('substitute_siblings') }}</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('InheritFields')" :description="$t('InheritFields_help')">
<generic-multiselect
@change="food.inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_selection="food.inherit_fields"
label="name"
:multiple="false"
:placeholder="$t('InheritFields')"
></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('ChildInheritFields')" :description="$t('ChildInheritFields_help')">
<generic-multiselect
@change="food.child_inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_selection="food.child_inherit_fields"
label="name"
:multiple="false"
:placeholder="$t('ChildInheritFields')"
></generic-multiselect>
</b-form-group>
<!-- TODO change to a button -->
<b-form-group :description="$t('reset_children_help')">
<b-form-checkbox v-model="food.reset_inherit">{{ $t('reset_children') }}</b-form-checkbox>
</b-form-group>
<b-button variant="primary" @click="updateFood">{{ $t('Save') }}</b-button>
</b-form>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiApiFactory} from "@/utils/openapi/api";
import RecipeCard from "@/components/RecipeCard.vue";
import GenericMultiselect from "@/components/GenericMultiselect.vue";
import {ApiMixin, StandardToasts} from "@/utils/utils";
Vue.use(BootstrapVue)
export default {
name: "TestView",
mixins: [ApiMixin],
components: {
GenericMultiselect
},
data() {
return {
food: undefined,
}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
let apiClient = new ApiApiFactory()
apiClient.retrieveFood('1').then((r) => {
this.food = r.data
})
},
methods: {
updateFood: function () {
let apiClient = new ApiApiFactory()
apiClient.updateFood(this.food.id, this.food).then((r) => {
this.food = r.data
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}
},
}
</script>
<style>
</style>

View File

@@ -1,6 +1,6 @@
<template>
<!-- bottom button nav -->
<div class="fixed-bottom p-1 pt-2 pl-2 pr-2 border-top text-center d-lg-none" style="background: white">
<div class="fixed-bottom p-1 pt-2 pl-2 pr-2 border-top text-center d-lg-none d-print-none" style="background: white">
<div class="d-flex flex-row justify-content-around">
<div class="flex-column" v-if="show_button_1">
<slot name="button_1">

View File

@@ -0,0 +1,412 @@
<template>
<div>
<b-modal :id="id" size="xl" @hidden="cancelAction" :body-class="`pr-3 pl-3`">
<template v-slot:modal-title>
<div class="row" v-if="food">
<div class="col-12">
<h2>{{ food.name }} <small class="text-muted" v-if="food.plural_name">{{
food.plural_name
}}</small>
</h2>
</div>
</div>
</template>
<div>
<b-tabs content-class="mt-3" v-if="food">
<b-tab title="General" active>
<b-form>
<b-form-group :label="$t('Name')" description="">
<b-form-input v-model="food.name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Plural')" description="">
<b-form-input v-model="food.plural_name"></b-form-input>
</b-form-group>
<!-- Food properties -->
<h5><i class="fas fa-database"></i> {{ $t('Properties') }}</h5>
<b-form-group :label="$t('Properties Food Amount')" description=""> <!-- TODO localize -->
<b-form-input v-model="food.properties_food_amount"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Properties Food Unit')" description=""> <!-- TODO localize -->
<generic-multiselect
@change="food.properties_food_unit = $event.val;"
:model="Models.UNIT"
:initial_single_selection="food.properties_food_unit"
label="name"
:multiple="false"
:placeholder="$t('Unit')"
></generic-multiselect>
</b-form-group>
<table class="table table-bordered">
<thead>
<tr>
<th> {{ $t('Property Amount') }}</th> <!-- TODO localize -->
<th> {{ $t('Property Type') }}</th> <!-- TODO localize -->
<th></th>
<th></th>
</tr>
</thead>
<tr v-for="fp in food.properties" v-bind:key="fp.id">
<td><input v-model="fp.property_amount" type="number"> <span
v-if="fp.property_type">{{ fp.property_type.unit }}</span></td>
<td>
<generic-multiselect
@change="fp.property_type = $event.val"
:initial_single_selection="fp.property_type"
label="name" :model="Models.PROPERTY_TYPE"
:multiple="false"/>
</td>
<td> / <span>{{ food.properties_food_amount }} <span
v-if="food.properties_food_unit !== null">{{
food.properties_food_unit.name
}}</span></span>
</td>
<td>
<button class="btn btn-danger btn-small" @click="deleteProperty(fp)"><i
class="fas fa-trash-alt"></i></button>
</td>
</tr>
</table>
<div class="text-center">
<b-button-group>
<b-btn class="btn btn-success shadow-none" @click="addProperty()"><i
class="fa fa-plus"></i>
</b-btn>
<b-btn class="btn btn-secondary shadow-none" @click="addAllProperties()"><i
class="fa fa-plus"> <i class="ml-1 fas fa-list"></i></i>
</b-btn>
</b-button-group>
</div>
<b-form-group :label="$t('Shopping_Category')" :description="$t('shopping_category_help')">
<generic-multiselect
@change="food.supermarket_category = $event.val;"
:model="Models.SHOPPING_CATEGORY"
:initial_single_selection="food.supermarket_category"
label="name"
:multiple="false"
:allow_create="true"
:placeholder="$t('Shopping_Category')"
></generic-multiselect>
</b-form-group>
</b-form>
</b-tab>
<b-tab title="Conversions" @click="loadUnitConversions" v-if="this.food.id !== undefined">
<b-row v-for="uc in unit_conversions" :key="uc">
<b-col>
<span v-if="uc.id">
<b-btn class="btn btn-sm" variant="danger" @click="deleteUnitConversion(uc)"><i class="fas fa-trash-alt"></i></b-btn>
{{ uc.base_amount }}
{{ uc.base_unit.name }}
=
{{ uc.converted_amount }}
{{ uc.converted_unit.name }}
</span>
<b-form class="mt-1">
<b-input-group>
<b-input v-model="uc.base_amount" @change="uc.changed = true"></b-input>
<b-input-group-append>
<generic-multiselect
@change="uc.base_unit = $event.val; uc.changed = true"
:initial_single_selection="uc.base_unit"
label="name" :model="Models.UNIT"
:multiple="false"/>
</b-input-group-append>
</b-input-group>
<b-input-group>
<b-input v-model="uc.converted_amount" @change="uc.changed = true"></b-input>
<b-input-group-append>
<generic-multiselect
@change="uc.converted_unit = $event.val; uc.changed = true"
:initial_single_selection="uc.converted_unit"
label="name" :model="Models.UNIT"
:multiple="false"/>
</b-input-group-append>
</b-input-group>
</b-form>
</b-col>
<hr style="height: 1px"/>
</b-row>
<b-row>
<b-col class="text-center">
<b-btn variant="success" @click="addUnitConversion"><i class="fa fa-plus"></i></b-btn>
</b-col>
</b-row>
</b-tab>
<b-tab title="More">
<b-form>
<b-form-group :label="$t('Recipe')" :description="$t('food_recipe_help')">
<generic-multiselect
@change="food.recipe = $event.val;"
:model="Models.RECIPE"
:initial_single_selection="food.recipe"
label="name"
:multiple="false"
:placeholder="$t('Recipe')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('OnHand_help')">
<b-form-checkbox v-model="food.food_onhand">{{ $t('OnHand') }}</b-form-checkbox>
</b-form-group>
<b-form-group :description="$t('ignore_shopping_help')">
<b-form-checkbox v-model="food.ignore_shopping">{{
$t('Ignore_Shopping')
}}
</b-form-checkbox>
</b-form-group>
<hr/>
<!-- todo add conditions if false disable dont hide -->
<b-form-group :label="$t('Substitutes')" :description="$t('substitute_help')">
<generic-multiselect
@change="food.substitute = $event.val;"
:model="Models.FOOD"
:initial_selection="food.substitute"
label="name"
:multiple="true"
:placeholder="$t('Substitutes')"
></generic-multiselect>
</b-form-group>
<b-form-group :description="$t('substitute_siblings_help')">
<b-form-checkbox v-model="food.substitute_siblings">{{
$t('substitute_siblings')
}}
</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('InheritFields')" :description="$t('InheritFields_help')">
<generic-multiselect
@change="food.inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_selection="food.inherit_fields"
label="name"
:multiple="true"
:placeholder="$t('InheritFields')"
></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('ChildInheritFields')"
:description="$t('ChildInheritFields_help')">
<generic-multiselect
@change="food.child_inherit_fields = $event.val;"
:model="Models.FOOD_INHERIT_FIELDS"
:initial_sselection="food.child_inherit_fields"
label="name"
:multiple="true"
:placeholder="$t('ChildInheritFields')"
></generic-multiselect>
</b-form-group>
<!-- TODO change to a button -->
<b-form-group :description="$t('reset_children_help')">
<b-form-checkbox v-model="food.reset_inherit">{{
$t('reset_children')
}}
</b-form-checkbox>
</b-form-group>
</b-form>
</b-tab>
</b-tabs>
</div>
<template v-slot:modal-footer>
<b-button variant="primary" @click="updateFood">{{ $t('Save') }}</b-button>
</template>
</b-modal>
</div>
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiApiFactory} from "@/utils/openapi/api";
import GenericMultiselect from "@/components/GenericMultiselect.vue";
import {ApiMixin, formFunctions, getForm, StandardToasts} from "@/utils/utils";
Vue.use(BootstrapVue)
export default {
name: "FoodEditor",
mixins: [ApiMixin],
components: {
GenericMultiselect
},
props: {
id: {type: String, default: 'id_food_edit_modal_modal'},
show: {required: true, type: Boolean, default: false},
item1: {
type: Object,
default: undefined
},
},
watch: {
show: function () {
if (this.show) {
this.$bvModal.show(this.id)
} else {
this.$bvModal.hide(this.id)
}
},
},
data() {
return {
food: undefined,
unit_conversions: []
}
},
mounted() {
this.$bvModal.show(this.id)
this.$i18n.locale = window.CUSTOM_LOCALE
let apiClient = new ApiApiFactory()
let pf
if (this.item1.id !== undefined) {
pf = apiClient.retrieveFood(this.item1.id).then((r) => {
this.food = r.data
this.food.properties_food_unit = {name: 'g'}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
} else {
this.food = {
name: "",
plural_name: "",
description: "",
shopping: false,
recipe: null,
properties: [],
properties_food_amount: 100,
properties_food_unit: {name: 'g'},
food_onhand: false,
supermarket_category: null,
parent: null,
numchild: 0,
inherit_fields: [],
ignore_shopping: false,
substitute: [],
substitute_siblings: false,
substitute_children: false,
substitute_onhand: false,
child_inherit_fields: [],
}
}
},
methods: {
updateFood: function () {
let apiClient = new ApiApiFactory()
if (this.food.id !== undefined) {
apiClient.updateFood(this.food.id, this.food).then((r) => {
this.food = r.data
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
} else {
apiClient.createFood(this.food).then((r) => {
this.food = r.data
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}
this.unit_conversions.forEach(uc => {
if (uc.changed === true) {
if (uc.id === undefined) {
apiClient.createUnitConversion(uc).then(r => {
uc = r.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err, true)
})
} else {
apiClient.updateUnitConversion(uc.id, uc).then(r => {
uc = r.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err, true)
})
}
}
})
},
addProperty: function () {
this.food.properties.push({property_type: null, property_amount: 0})
},
addAllProperties: function () {
let apiClient = new ApiApiFactory()
apiClient.listPropertyTypes().then(r => {
r.data.forEach(x => {
this.food.properties.push({property_type: x, property_amount: 0})
})
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
deleteProperty: function (p) {
this.food.properties = this.food.properties.filter(x => x !== p)
},
cancelAction: function () {
this.$emit("hidden", "")
},
loadUnitConversions: function () {
let apiClient = new ApiApiFactory()
apiClient.listUnitConversions(this.food.id).then(r => {
this.unit_conversions = r.data
})
},
addUnitConversion: function () {
this.unit_conversions.push(
{
food: this.food,
base_amount: 1,
base_unit: null,
converted_amount: 0,
converted_unit: null,
}
)
},
deleteUnitConversion: function (uc) {
this.unit_conversions = this.unit_conversions.filter(u => u !== uc)
let apiClient = new ApiApiFactory()
apiClient.destroyUnitConversion(uc.id).then(r => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
})
}
},
}
</script>
<style>
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<span class="pl-1" v-if="recipe.last_cooked !== null">
<span class="pl-1" v-if="recipe.last_cooked !== undefined && recipe.last_cooked !== null">
<b-badge pill variant="primary" class="font-weight-normal"><i class="fas fa-utensils"></i> {{
formatDate(recipe.last_cooked)
}}</b-badge>

View File

@@ -1,6 +1,10 @@
<template>
<div>
<b-modal :id="'modal_' + id" @hidden="cancelAction">
<template v-if="form_component !== undefined">
<component :is="form_component" :id="'modal_' + id" :show="show" @hidden="cancelAction" :item1="item1"></component>
</template>
<template v-else>
<b-modal :id="'modal_' + id" @hidden="cancelAction" size="lg">
<template v-slot:modal-title>
<h4 class="d-inline">{{ form.title }}</h4>
<help-badge v-if="form.show_help" @show="show_help = true" @hide="show_help = false" :component="`GenericModal${form.title}`"/>
@@ -9,7 +13,7 @@
<p v-if="visibleCondition(f, 'instruction')">{{ f.label }}</p>
<lookup-input v-if="visibleCondition(f, 'lookup')" :form="f" :model="listModel(f.list)" @change="storeValue" :help="showHelp && f.help"/>
<checkbox-input class="mb-3" v-if="visibleCondition(f, 'checkbox')" :label="f.label" :value="f.value" :field="f.field" :help="showHelp && f.help"/>
<text-input v-if="visibleCondition(f, 'text')" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" :help="showHelp && f.help" :subtitle="f.subtitle" />
<text-input v-if="visibleCondition(f, 'text')" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" :help="showHelp && f.help" :subtitle="f.subtitle" :disabled="f.disabled"/>
<choice-input v-if="visibleCondition(f, 'choice')" :label="f.label" :value="f.value" :field="f.field" :options="f.options" :placeholder="f.placeholder"/>
<emoji-input v-if="visibleCondition(f, 'emoji')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue"/>
<file-input v-if="visibleCondition(f, 'file')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue"/>
@@ -29,6 +33,8 @@
</div>
</template>
</b-modal>
</template>
</div>
</template>
@@ -54,7 +60,18 @@ import NumberInput from "@/components/Modals/NumberInput.vue";
export default {
name: "GenericModalForm",
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput, SmallText, HelpBadge,DateInput, NumberInput },
components: {
FileInput,
CheckboxInput,
LookupInput,
TextInput,
EmojiInput,
ChoiceInput,
SmallText,
HelpBadge,
DateInput,
NumberInput
},
mixins: [ApiMixin, ToastMixin],
props: {
model: {required: true, type: Object},
@@ -77,6 +94,7 @@ export default {
},
},
show: {required: true, type: Boolean, default: false},
models: {required: false, type: Function, default: null}
},
data() {
return {
@@ -92,6 +110,10 @@ export default {
mounted() {
this.id = Math.random()
this.$root.$on("change", this.storeValue) // bootstrap modal placed at document so have to listen at root of component
if (this.models !== null) {
this.Models = this.models // override models definition file with prop
}
},
computed: {
advancedForm() {
@@ -111,6 +133,15 @@ export default {
return undefined
}
},
form_component() {
// TODO this leads webpack to create one .js file for each component in this folder because at runtime any one of them could be requested
// TODO this is not necessarily bad but maybe there are better options to do this
if (this.form.component !== undefined) {
return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.form.component}`)
} else {
return undefined
}
},
},
watch: {
show: function () {
@@ -153,6 +184,7 @@ export default {
if (this.dirty) {
this.dirty = false
this.$emit("finish-action", "cancel")
this.$emit("hidden")
}
},
storeValue: function (field, value) {
@@ -250,7 +282,10 @@ export default {
target: this.form_data.target.id,
})
.then((result) => {
this.$emit("finish-action", { target: this.form_data.target.id, target_object: this.form_data.target }) //TODO temporary workaround to not change other apis
this.$emit("finish-action", {
target: this.form_data.target.id,
target_object: this.form_data.target
}) //TODO temporary workaround to not change other apis
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_MERGE)
})
.catch((err) => {

View File

@@ -1,7 +1,7 @@
<template>
<div>
<b-form-group v-bind:label="label" class="mb-3">
<b-form-input v-model="new_value" type="text" :placeholder="placeholder"></b-form-input>
<b-form-input v-model="new_value" type="text" :placeholder="placeholder" :disabled="disabled"></b-form-input>
<em v-if="help" class="small text-muted">{{ help }}</em>
<small v-if="subtitle" class="text-muted">{{ subtitle }}</small>
</b-form-group>
@@ -18,6 +18,7 @@ export default {
placeholder: { type: String, default: "You Should Add Placeholder Text" },
help: { type: String, default: undefined },
subtitle: { type: String, default: undefined },
disabled: { type: Boolean, default: false }
},
data() {
return {

View File

@@ -0,0 +1,94 @@
<template>
<div>
<beta-warning></beta-warning>
<div v-if="metadata !== undefined">
{{ $t('Data_Import_Info') }}
<a href="https://github.com/TandoorRecipes/open-tandoor-data" target="_blank" rel="noreferrer nofollow">{{$t('Learn_More')}}</a>
<select class="form-control" v-model="selected_version">
<option v-for="v in metadata.versions" v-bind:key="v">{{ v }}</option>
</select>
<b-checkbox v-model="update_existing" class="mt-1">{{ $t('Update_Existing_Data') }}</b-checkbox>
<b-checkbox v-model="use_metric" class="mt-1">{{ $t('Use_Metric') }}</b-checkbox>
<div v-if="selected_version !== undefined" class="mt-3">
<table class="table">
<tr>
<th>{{ $t('Datatype') }}</th>
<th>{{ $t('Number of Objects') }}</th>
<th>{{ $t('Imported') }}</th>
</tr>
<tr v-for="d in metadata.datatypes" v-bind:key="d">
<td>{{ $t(d.charAt(0).toUpperCase() + d.slice(1)) }}</td>
<td>{{ metadata[selected_version][d] }}</td>
<td>
<template v-if="import_count !== undefined">{{ import_count[d] }}</template>
</td>
</tr>
</table>
<button class="btn btn-success" @click="doImport">{{ $t('Import') }}</button>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiMixin, resolveDjangoUrl, StandardToasts} from "@/utils/utils";
import axios from "axios";
import BetaWarning from "@/components/BetaWarning.vue";
Vue.use(BootstrapVue)
export default {
name: "OpenDataImportComponent",
mixins: [ApiMixin],
components: {BetaWarning},
data() {
return {
metadata: undefined,
selected_version: undefined,
update_existing: true,
use_metric: true,
import_count: undefined,
}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
axios.get(resolveDjangoUrl('api_import_open_data')).then(r => {
this.metadata = r.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
methods: {
doImport: function () {
axios.post(resolveDjangoUrl('api_import_open_data'), {
'selected_version': this.selected_version,
'selected_datatypes': this.metadata.datatypes,
'update_existing': this.update_existing,
'use_metric': this.use_metric,
}).then(r => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
this.import_count = r.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)
})
},
},
}
</script>

View File

@@ -0,0 +1,195 @@
<template>
<div>
<div class="card p-4 pb-2" v-if="recipe !== undefined">
<b-row>
<b-col>
<h5><i class="fas fa-database"></i> {{ $t('Properties') }}</h5>
</b-col>
<b-col class="text-right">
<span v-if="!show_total">{{ $t('per_serving') }} </span>
<span v-if="show_total">{{ $t('total') }} </span>
<a href="#" @click="show_total = !show_total">
<i class="fas fa-toggle-on" v-if="!show_total"></i>
<i class="fas fa-toggle-off" v-if="show_total"></i>
</a>
<div v-if="hasRecipeProperties && hasFoodProperties">
<span v-if="!show_recipe_properties">{{ $t('Food') }} </span>
<span v-if="show_recipe_properties">{{ $t('Recipe') }} </span>
<a href="#" @click="show_recipe_properties = !show_recipe_properties">
<i class="fas fa-toggle-on" v-if="!show_recipe_properties"></i>
<i class="fas fa-toggle-off" v-if="show_recipe_properties"></i>
</a>
</div>
</b-col>
</b-row>
<table class="table table-bordered table-sm">
<tr v-for="p in property_list" v-bind:key="`id_${p.id}`">
<td>
{{ p.icon }} {{ p.name }}
</td>
<td class="text-right">{{ get_amount(p.property_amount) }}</td>
<td class=""> {{ p.unit }}</td>
<td class="align-middle text-center" v-if="!show_recipe_properties">
<a href="#" @click="selected_property = p">
<i v-if="p.missing_value" class="text-warning fas fa-exclamation-triangle"></i>
<i v-if="!p.missing_value" class="text-muted fas fa-info-circle"></i>
</a>
</td>
</tr>
</table>
</div>
<b-modal id="id_modal_property_overview" :title="selected_property.name" v-model="show_modal" v-if="selected_property !== undefined"
@hidden="selected_property = undefined">
<template v-if="selected_property !== undefined">
{{ selected_property.description }}
<table class="table table-bordered">
<tr v-for="f in selected_property.food_values"
v-bind:key="`id_${selected_property.id}_food_${f.id}`">
<td><a href="#" @click="openFoodEditModal(f)">{{ f.food }}</a></td>
<td>{{ f.value }} {{ selected_property.unit }}</td>
</tr>
</table>
</template>
</b-modal>
<generic-modal-form
:model="Models.FOOD"
:action="Actions.UPDATE"
:item1="selected_food"
:show="show_food_edit_modal"
@hidden="foodEditorHidden"
>
</generic-modal-form>
</div>
</template>
<script>
import {ApiMixin, StandardToasts} from "@/utils/utils";
import GenericModalForm from "@/components/Modals/GenericModalForm.vue";
import {ApiApiFactory} from "@/utils/openapi/api";
export default {
name: "PropertyViewComponent",
mixins: [ApiMixin],
components: {GenericModalForm},
props: {
recipe: Object,
servings: Number,
},
data() {
return {
selected_property: undefined,
selected_food: undefined,
show_food_edit_modal: false,
show_total: false,
show_recipe_properties: false,
}
},
computed: {
show_modal: function () {
return this.selected_property !== undefined
},
hasRecipeProperties: function () {
return this.recipe.properties.length !== 0
},
hasFoodProperties: function () {
let has_food_properties = false
for (const [key, fp] of Object.entries(this.recipe.food_properties)) {
if (fp.total_value !== 0) {
has_food_properties = true
}
}
return has_food_properties
},
property_list: function () {
let pt_list = []
if (this.show_recipe_properties) {
this.recipe.properties.forEach(rp => {
pt_list.push(
{
'id': rp.property_type.id,
'name': rp.property_type.name,
'description': rp.property_type.description,
'icon': rp.property_type.icon,
'food_values': [],
'property_amount': rp.property_amount,
'missing_value': false,
'unit': rp.property_type.unit,
}
)
})
} else {
for (const [key, fp] of Object.entries(this.recipe.food_properties)) {
pt_list.push(
{
'id': fp.id,
'name': fp.name,
'description': fp.description,
'icon': fp.icon,
'food_values': fp.food_values,
'property_amount': fp.total_value,
'missing_value': fp.missing_value,
'unit': fp.unit,
}
)
}
}
return pt_list
}
},
mounted() {
if (this.hasRecipeProperties && !this.hasFoodProperties) {
this.show_recipe_properties = true
}
},
methods: {
get_amount: function (amount) {
if (this.show_total) {
return (amount * (this.servings / this.recipe.servings)).toLocaleString(window.CUSTOM_LOCALE, {
'maximumFractionDigits': 2,
'minimumFractionDigits': 2
})
} else {
return (amount / this.recipe.servings).toLocaleString(window.CUSTOM_LOCALE, {
'maximumFractionDigits': 2,
'minimumFractionDigits': 2
})
}
},
openFoodEditModal: function (food) {
console.log(food)
let apiClient = ApiApiFactory()
apiClient.retrieveFood(food.id).then(r => {
this.selected_food = r.data;
this.show_food_edit_modal = true
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
foodEditorHidden: function () {
this.show_food_edit_modal = false;
this.$emit("foodUpdated", "")
}
},
}
</script>
<style scoped>
</style>

View File

@@ -35,12 +35,12 @@
<div class="card-img-overlay d-flex flex-column justify-content-left float-left text-left pt-2" style="width:40%"
v-if="recipe.working_time !== 0 || recipe.waiting_time !== 0">
<b-badge pill variant="light" class="mt-1 font-weight-normal" v-if="recipe.working_time !== 0">
<b-badge pill variant="light" class="mt-1 font-weight-normal" v-if="recipe.working_time !== 0 && recipe.working_time !== undefined">
<i
class="fa fa-clock"></i> {{ working_time }}
</b-badge>
<b-badge pill variant="secondary" class="mt-1 font-weight-normal"
v-if="recipe.waiting_time !== 0">
v-if="recipe.waiting_time !== 0 && recipe.waiting_time !== undefined">
<i class="fa fa-pause"></i> {{ waiting_time }}
</b-badge>
</div>
@@ -58,7 +58,7 @@
<div class="justify-content-end">
<recipe-context-menu :recipe="recipe" class="justify-content-end float-right align-items-end pr-0"
:disabled_options="context_disabled_options"
v-if="recipe !== null"></recipe-context-menu>
v-if="recipe !== null && show_context_menu"></recipe-context-menu>
</div>
</div>
@@ -89,7 +89,7 @@
</div>
</transition>
<b-badge pill variant="info" v-if="!recipe.internal">{{ $t("External") }}</b-badge>
<b-badge pill variant="info" v-if="recipe.internal !== undefined && !recipe.internal">{{ $t("External") }}</b-badge>
</template>
</b-card-text>

View File

@@ -0,0 +1,351 @@
<template>
<div>
<template v-if="loading">
<loading-spinner></loading-spinner>
</template>
<div v-if="!loading" style="padding-bottom: 60px">
<RecipeSwitcher ref="ref_recipe_switcher" @switch="quickSwitch($event)"/>
<div class="row">
<div class="col-12" style="text-align: center">
<h3>{{ recipe.name }}</h3>
</div>
</div>
<div class="row text-center">
<div class="col col-md-12">
<recipe-rating :recipe="recipe"></recipe-rating>
<last-cooked :recipe="recipe" class="mt-2"></last-cooked>
</div>
</div>
<div class="my-auto">
<div class="col-12" style="text-align: center">
<i>{{ recipe.description }}</i>
</div>
</div>
<div style="text-align: center">
<keywords-component :recipe="recipe"></keywords-component>
</div>
<hr/>
<div class="row align-items-center">
<div class="col col-md-3">
<div class="d-flex">
<div class="my-auto mr-1">
<i class="fas fa-fw fa-user-clock fa-2x text-primary"></i>
</div>
<div class="my-auto mr-1">
<span class="text-primary"><b>{{ $t("Preparation") }}</b></span><br/>
{{ working_time }}
</div>
</div>
</div>
<div class="col col-md-3">
<div class="row d-flex">
<div class="my-auto mr-1">
<i class="far fa-fw fa-clock fa-2x text-primary"></i>
</div>
<div class="my-auto mr-1">
<span class="text-primary"><b>{{ $t("Waiting") }}</b></span><br/>
{{ waiting_time }}
</div>
</div>
</div>
<div class="col col-md-4 col-10 mt-2 mt-md-0">
<div class="d-flex">
<div class="my-auto mr-1">
<i class="fas fa-fw fa-pizza-slice fa-2x text-primary"></i>
</div>
<div class="my-auto mr-1">
<CustomInputSpinButton v-model.number="servings"/>
</div>
<div class="my-auto mr-1">
<span class="text-primary">
<b>
<template v-if="recipe.servings_text === ''">{{ $t("Servings") }}</template>
<template v-else>{{ recipe.servings_text }}</template>
</b>
</span>
</div>
</div>
</div>
<div class="col col-md-2 col-2 mt-2 mt-md-0 text-right">
<recipe-context-menu v-bind:recipe="recipe" :servings="servings"
:disabled_options="{print:false}"></recipe-context-menu>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2"
v-if="recipe && ingredient_count > 0 && (recipe.show_ingredient_overview || recipe.steps.length < 2)">
<ingredients-card
:recipe="recipe.id"
:steps="recipe.steps"
:ingredient_factor="ingredient_factor"
:servings="servings"
:header="true"
id="ingredient_container"
@checked-state-changed="updateIngredientCheckedState"
@change-servings="servings = $event"
/>
</div>
<div class="col-12 order-1 col-sm-12 order-sm-1 col-md-6 order-md-2">
<div class="row">
<div class="col-12">
<img class="img img-fluid rounded" :src="recipe.image" :alt="$t('Recipe_Image')"
v-if="recipe.image !== null" @load="onImgLoad"
:style="{ 'max-height': ingredient_height }"/>
</div>
</div>
</div>
</div>
<template v-if="!recipe.internal">
<div v-if="recipe.file_path.includes('.pdf')">
<PdfViewer :recipe="recipe"></PdfViewer>
</div>
<div
v-if="recipe.file_path.includes('.png') || recipe.file_path.includes('.jpg') || recipe.file_path.includes('.jpeg') || recipe.file_path.includes('.gif')">
<ImageViewer :recipe="recipe"></ImageViewer>
</div>
</template>
<div v-for="(s, index) in recipe.steps" v-bind:key="s.id" style="margin-top: 1vh">
<step-component
:recipe="recipe"
:step="s"
:ingredient_factor="ingredient_factor"
:index="index"
:start_time="start_time"
@update-start-time="updateStartTime"
@checked-state-changed="updateIngredientCheckedState"
></step-component>
</div>
<div v-if="recipe.source_url !== null">
<h6 class="d-print-none"><i class="fas fa-file-import"></i> {{ $t("Imported_From") }}</h6>
<span class="text-muted mt-1"><a style="overflow-wrap: break-word;"
:href="recipe.source_url">{{ recipe.source_url }}</a></span>
</div>
<div class="row" style="margin-top: 2vh; ">
<div class="col-lg-6 offset-lg-3 col-12">
<property-view-component :recipe="recipe" :servings="servings" @foodUpdated="loadRecipe(recipe.id)"></property-view-component>
</div>
</div>
</div>
<add-recipe-to-book :recipe="recipe"></add-recipe-to-book>
<div class="row text-center d-print-none" style="margin-top: 3vh; margin-bottom: 3vh"
v-if="share_uid !== 'None' && !loading">
<div class="col col-md-12">
<import-tandoor></import-tandoor> <br/>
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)" class="mt-3">{{ $t("Report Abuse") }}</a>
</div>
</div>
</div>
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {apiLoadRecipe} from "@/utils/api"
import RecipeContextMenu from "@/components/RecipeContextMenu"
import {ResolveUrlMixin, ToastMixin, calculateHourMinuteSplit} from "@/utils/utils"
import PdfViewer from "@/components/PdfViewer"
import ImageViewer from "@/components/ImageViewer"
import moment from "moment"
import LoadingSpinner from "@/components/LoadingSpinner"
import AddRecipeToBook from "@/components/Modals/AddRecipeToBook"
import RecipeRating from "@/components/RecipeRating"
import LastCooked from "@/components/LastCooked"
import IngredientsCard from "@/components/IngredientsCard"
import StepComponent from "@/components/StepComponent"
import KeywordsComponent from "@/components/KeywordsComponent"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import CustomInputSpinButton from "@/components/CustomInputSpinButton"
import {ApiApiFactory} from "@/utils/openapi/api";
import ImportTandoor from "@/components/Modals/ImportTandoor.vue";
import PropertyViewComponent from "@/components/PropertyViewComponent.vue";
Vue.prototype.moment = moment
Vue.use(BootstrapVue)
export default {
name: "RecipeView",
mixins: [ResolveUrlMixin, ToastMixin],
components: {
ImportTandoor,
LastCooked,
RecipeRating,
PdfViewer,
ImageViewer,
IngredientsCard,
StepComponent,
RecipeContextMenu,
KeywordsComponent,
LoadingSpinner,
AddRecipeToBook,
RecipeSwitcher,
CustomInputSpinButton,
PropertyViewComponent,
},
computed: {
ingredient_factor: function () {
return this.servings / this.recipe.servings
},
ingredient_count() {
return this.recipe?.steps.map((x) => x.ingredients).flat().length
},
working_time: function () {
return calculateHourMinuteSplit(this.recipe.working_time)
},
waiting_time: function () {
return calculateHourMinuteSplit(this.recipe.waiting_time)
},
},
data() {
return {
loading: true,
recipe: undefined,
rootrecipe: undefined,
servings: 1,
servings_cache: {},
start_time: "",
share_uid: window.SHARE_UID,
wake_lock: null,
ingredient_height: '250',
}
},
watch: {
servings(newVal, oldVal) {
this.servings_cache[this.recipe.id] = this.servings
},
},
mounted() {
this.loadRecipe(window.RECIPE_ID)
this.$i18n.locale = window.CUSTOM_LOCALE
this.requestWakeLock()
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() {
this.destroyWakeLock()
},
methods: {
requestWakeLock: async function () {
if ('wakeLock' in navigator) {
try {
this.wake_lock = await navigator.wakeLock.request('screen')
document.addEventListener('visibilitychange', this.visibilityChange)
} catch (err) {
console.log(err)
}
}
},
handleResize: function () {
if (document.getElementById('nutrition_container') !== null) {
this.ingredient_height = document.getElementById('ingredient_container').clientHeight - document.getElementById('nutrition_container').clientHeight
} else {
this.ingredient_height = document.getElementById('ingredient_container').clientHeight
}
},
destroyWakeLock: function () {
if (this.wake_lock != null) {
this.wake_lock.release()
.then(() => {
this.wake_lock = null
});
}
document.removeEventListener('visibilitychange', this.visibilityChange)
},
visibilityChange: async function () {
if (this.wake_lock != null && document.visibilityState === 'visible') {
await this.requestWakeLock()
}
},
loadRecipe: function (recipe_id) {
apiLoadRecipe(recipe_id).then((recipe) => {
let total_time = 0
for (let step of recipe.steps) {
for (let ingredient of step.ingredients) {
this.$set(ingredient, "checked", false)
}
step.time_offset = total_time
total_time += step.time
}
// set start time only if there are any steps with timers (otherwise no timers are rendered)
if (total_time > 0) {
this.start_time = moment().format("yyyy-MM-DDTHH:mm")
}
if (recipe.image === null) this.printReady()
this.recipe = this.rootrecipe = recipe
this.servings = this.servings_cache[this.rootrecipe.id] = recipe.servings
this.loading = false
setTimeout(() => {
this.handleResize()
}, 100)
})
},
updateStartTime: function (e) {
this.start_time = e
},
updateIngredientCheckedState: function (e) {
for (let step of this.recipe.steps) {
for (let ingredient of step.ingredients) {
if (ingredient.id === e.id) {
this.$set(ingredient, "checked", !ingredient.checked)
}
}
}
},
quickSwitch: function (e) {
if (e === -1) {
this.recipe = this.rootrecipe
this.servings = this.servings_cache[this.rootrecipe?.id ?? 1]
} else {
this.recipe = e
this.servings = this.servings_cache?.[e.id] ?? e.servings
}
},
printReady: function () {
const template = document.createElement("template");
template.id = "printReady";
document.body.appendChild(template);
},
onImgLoad: function () {
this.printReady()
},
},
}
</script>
<style>
#app > div > div {
break-inside: avoid;
}
</style>

View File

@@ -31,22 +31,22 @@
"Step_start_time": "",
"Sort_by_new": "",
"Table_of_Contents": "",
"Recipes_per_page": "",
"Recipes_per_page": "Receptů na stránku",
"Show_as_header": "",
"Hide_as_header": "",
"Add_nutrition_recipe": "",
"Remove_nutrition_recipe": "",
"Add_nutrition_recipe": "Přidat nutriční hodnoty",
"Remove_nutrition_recipe": "Smazat nutriční hodnoty",
"Copy_template_reference": "",
"Save_and_View": "",
"Save_and_View": "Uložit & Zobrazit",
"Manage_Books": "",
"Meal_Plan": "",
"Meal_Plan": "Jídelníček",
"Select_Book": "",
"Select_File": "",
"Select_File": "Vybrat soubor",
"Recipe_Image": "",
"Import_finished": "",
"View_Recipes": "",
"Import_finished": "Import dokončen",
"View_Recipes": "Zobrazit recepty",
"Log_Cooking": "",
"New_Recipe": "",
"New_Recipe": "Nový recept",
"Url_Import": "",
"Reset_Search": "",
"Recently_Viewed": "",
@@ -54,21 +54,21 @@
"New_Keyword": "",
"Delete_Keyword": "",
"Edit_Keyword": "",
"Edit_Recipe": "",
"Edit_Recipe": "Upravit recept",
"Move_Keyword": "",
"Merge_Keyword": "",
"Hide_Keywords": "",
"Hide_Recipes": "",
"Move_Up": "",
"Move_Down": "",
"Step_Name": "",
"Move_Up": "Nahoru",
"Move_Down": "Dolů",
"Step_Name": "Název kroku",
"Step_Type": "",
"Make_Header": "",
"Make_Ingredient": "",
"Amount": "",
"Enable_Amount": "",
"Disable_Amount": "",
"Ingredient Editor": "",
"Amount": "Množství",
"Enable_Amount": "Zobrazit množství",
"Disable_Amount": "Skrýt množství",
"Ingredient Editor": "Editace ingrediencí",
"Description_Replace": "",
"Instruction_Replace": "",
"Auto_Sort": "",
@@ -79,8 +79,8 @@
"Add_Step": "",
"Keywords": "",
"Books": "",
"Proteins": "",
"Fats": "",
"Proteins": "Proteiny",
"Fats": "Tuky",
"Carbohydrates": "",
"Calories": "",
"Energy": "",
@@ -333,7 +333,7 @@
"Foods": "",
"Account": "",
"Cosmetic": "",
"API": "",
"API": "API",
"enable_expert": "",
"expert_mode": "",
"simple_mode": "",
@@ -353,8 +353,8 @@
"Custom Filter": "",
"shared_with": "",
"sort_by": "",
"asc": "",
"desc": "",
"asc": "Vzestupně",
"desc": "Sestupně",
"date_viewed": "",
"last_cooked": "",
"times_cooked": "",
@@ -362,29 +362,29 @@
"show_sortby": "",
"search_rank": "",
"make_now": "",
"recipe_filter": "",
"recipe_filter": "Filtrovat recepty",
"book_filter_help": "",
"review_shopping": "",
"view_recipe": "",
"view_recipe": "Zobrazit recept",
"copy_to_new": "",
"recipe_name": "",
"recipe_name": "Název receptu",
"paste_ingredients_placeholder": "",
"paste_ingredients": "",
"ingredient_list": "",
"explain": "",
"filter": "",
"Website": "",
"App": "",
"filter": "Filtr",
"Website": "Web",
"App": "Aplikace",
"Message": "",
"Bookmarklet": "",
"Sticky_Nav": "",
"Sticky_Nav_Help": "",
"Nav_Color": "",
"Nav_Color_Help": "",
"Use_Kj": "",
"Comments_setting": "",
"click_image_import": "",
"no_more_images_found": "",
"Use_Kj": "Používat kJ místo kcal",
"Comments_setting": "Zobrazit komentáře",
"click_image_import": "Vyberte obrázek, který chcete přiřadit k tomuto receptu",
"no_more_images_found": "Žádné další obrázky na zadaném odkazu.",
"import_duplicates": "",
"paste_json": "",
"Click_To_Edit": "",
@@ -407,9 +407,9 @@
"InheritFields_help": "",
"show_ingredient_overview": "",
"Ingredient Overview": "",
"last_viewed": "",
"created_on": "",
"updatedon": "",
"last_viewed": "Naposledy zobrazeno",
"created_on": "Vytvořeno",
"updatedon": "Upraveno",
"Imported_From": "",
"advanced_search_settings": "",
"nothing_planned_today": "",
@@ -418,13 +418,13 @@
"Pinned": "",
"Imported": "",
"Quick actions": "",
"Ratings": "",
"Ratings": "Hodnocení",
"Internal": "",
"Units": "",
"Units": "Jednotky",
"Manage_Emails": "",
"Change_Password": "",
"Change_Password": "Změna hesla",
"Social_Authentication": "",
"Random Recipes": "",
"Random Recipes": "Náhodné recepty",
"parameter_count": "",
"select_keyword": "",
"add_keyword": "",
@@ -436,10 +436,10 @@
"empty_list": "",
"Select": "",
"Supermarkets": "",
"User": "",
"Username": "",
"First_name": "",
"Last_name": "",
"User": "Uživatel",
"Username": "Uživatelské jméno",
"First_name": "Jméno",
"Last_name": "Příjmení",
"Keyword": "",
"Advanced": "",
"Page": "",
@@ -452,15 +452,15 @@
"Create Food": "",
"create_food_desc": "",
"additional_options": "",
"Importer_Help": "",
"Documentation": "",
"Select_App_To_Import": "",
"Import_Supported": "",
"Export_Supported": "",
"Import_Not_Yet_Supported": "",
"Export_Not_Yet_Supported": "",
"Import_Result_Info": "",
"Recipes_In_Import": "",
"Importer_Help": "Nápověda k importu z této aplikace:",
"Documentation": "Dokumentace",
"Select_App_To_Import": "Vyberte aplikaci, ze které chcete importovat",
"Import_Supported": "Import podporován",
"Export_Supported": "Export podporován",
"Import_Not_Yet_Supported": "Import není zatím podporován",
"Export_Not_Yet_Supported": "Export není zatím podporován",
"Import_Result_Info": "{imported} z {total} receptů naimportováno",
"Recipes_In_Import": "Receptů v importním souboru",
"Toggle": "",
"Import_Error": "",
"Warning_Delete_Supermarket_Category": "",
@@ -477,6 +477,6 @@
"Use_Plural_Food_Always": "",
"Use_Plural_Food_Simple": "",
"plural_usage_info": "",
"Create Recipe": "",
"Import Recipe": ""
"Create Recipe": "Vytvořit recept",
"Import Recipe": "Importovat recept"
}

View File

@@ -481,5 +481,22 @@
"Amount": "Menge",
"Original_Text": "Originaler Text",
"Import Recipe": "Rezept importieren",
"Create Recipe": "Rezept erstellen"
"Create Recipe": "Rezept erstellen",
"recipe_property_info": "Sie können auch Eigenschaften zu Lebensmitteln hinzufügen, um sie automatisch auf der Grundlage Ihres Rezepts zu berechnen!",
"per_serving": "pro Portion",
"open_data_help_text": "Das Tandoor Open Data Projekt bietet von der Gemeinschaft bereitgestellte Daten für Tandoor. Dieses Feld wird beim Importieren automatisch ausgefüllt und ermöglicht künftige Aktualisierungen.",
"Open_Data_Import": "Datenimport öffnen",
"Update_Existing_Data": "Vorhandene Daten aktualisieren",
"Data_Import_Info": "Verbessern Sie Ihren Space, indem Sie eine von der Community kuratierte Liste von Lebensmitteln, Einheiten und mehr importieren, um Ihre Rezeptsammlung zu verbessern.",
"Learn_More": "Mehr erfahren",
"Use_Metric": "Metrische Einheiten verwenden",
"converted_unit": "Umgerechnete Einheit",
"converted_amount": "Umgerechneter Betrag",
"base_unit": "Basiseinheit",
"base_amount": "Grundbetrag",
"Datatype": "Datentyp",
"Number of Objects": "Anzahl von Objekten",
"Property": "Eigenschaft",
"Conversion": "Umrechnung",
"Properties": "Eigenschaften"
}

View File

@@ -14,6 +14,7 @@
"success_moving_resource": "Successfully moved a resource!",
"success_merging_resource": "Successfully merged a resource!",
"file_upload_disabled": "File upload is not enabled for your space.",
"recipe_property_info": "You can also add properties to foods to calculate them automatically based on your recipe!",
"warning_space_delete": "You can delete your space including all recipes, shopping lists, meal plans and whatever else you have created. This cannot be undone! Are you sure you want to do this ?",
"food_inherit_info": "Fields on food that should be inherited by default.",
"facet_count_info": "Show recipe counts on search filters.",
@@ -37,6 +38,7 @@
"Add_nutrition_recipe": "Add nutrition to recipe",
"Remove_nutrition_recipe": "Delete nutrition from recipe",
"Copy_template_reference": "Copy template reference",
"per_serving": "per servings",
"Save_and_View": "Save & View",
"Manage_Books": "Manage Books",
"Meal_Plan": "Meal Plan",
@@ -76,6 +78,19 @@
"Private_Recipe": "Private Recipe",
"Private_Recipe_Help": "Recipe is only shown to you and people its shared with.",
"reusable_help_text": "Should the invite link be usable for more than one user.",
"open_data_help_text": "The Tandoor Open Data project provides community contributed data for Tandoor. This field is filled automatically when importing it and allows updates in the future.",
"Open_Data_Slug": "Open Data Slug",
"Open_Data_Import": "Open Data Import",
"Data_Import_Info": "Enhance your Space by importing a community curated list of foods, units and more to improve your recipe collection.",
"Update_Existing_Data": "Update Existing Data",
"Use_Metric": "Use Metric Units",
"Learn_More": "Learn More",
"converted_unit": "Converted Unit",
"converted_amount": "Converted Amount",
"base_unit": "Base Unit",
"base_amount": "Base Amount",
"Datatype": "Datatype",
"Number of Objects": "Number of Objects",
"Add_Step": "Add Step",
"Keywords": "Keywords",
"Books": "Books",
@@ -161,6 +176,8 @@
"merge_title": "Merge {type}",
"move_title": "Move {type}",
"Food": "Food",
"Property": "Property",
"Conversion": "Conversion",
"Original_Text": "Original Text",
"Recipe_Book": "Recipe Book",
"del_confirmation_tree": "Are you sure that you want to delete {source} and all of it's children?",
@@ -168,6 +185,7 @@
"create_title": "New {type}",
"edit_title": "Edit {type}",
"Name": "Name",
"Properties": "Properties",
"Type": "Type",
"Description": "Description",
"Recipe": "Recipe",
@@ -462,6 +480,7 @@
"Import_Result_Info": "{imported} of {total} recipes were imported",
"Recipes_In_Import": "Recipes in your import file",
"Toggle": "Toggle",
"total": "total",
"Import_Error": "An Error occurred during your import. Please expand the Details at the bottom of the page to view it.",
"Warning_Delete_Supermarket_Category": "Deleting a supermarket category will also delete all relations to foods. Are you sure?",
"New_Supermarket": "Create new supermarket",

View File

@@ -73,7 +73,7 @@
"Carbohydrates": "Carbohydratos",
"Calories": "Calorias",
"Energy": "Energia",
"Nutrition": "Nutricion",
"Nutrition": "Nutrición",
"Date": "Fecha",
"Share": "Compartir",
"Automation": "Automatización",
@@ -452,5 +452,6 @@
"Auto_Sort": "Ordenar Automáticamente",
"Auto_Sort_Help": "Mueva todos los ingredientes al paso que mejor se adapte.",
"Unpin": "Desanclar",
"Amount": "Cantidad"
"Amount": "Cantidad",
"PinnedConfirmation": "{recipe} ha sido fijada."
}

View File

@@ -68,83 +68,83 @@
"Amount": "Mengde",
"Enable_Amount": "Aktiver mengde",
"Disable_Amount": "Deaktiver mengde",
"Ingredient Editor": "",
"Description_Replace": "",
"Instruction_Replace": "",
"Auto_Sort": "",
"Auto_Sort_Help": "",
"Private_Recipe": "",
"Private_Recipe_Help": "",
"reusable_help_text": "",
"Add_Step": "",
"Keywords": "",
"Ingredient Editor": "Ingrediens Behandler",
"Description_Replace": "Erstatt beskrivelse",
"Instruction_Replace": "Erstatt instruksjoner",
"Auto_Sort": "Sorter Automatisk",
"Auto_Sort_Help": "Flytt alle ingredienser til det mest passende steget.",
"Private_Recipe": "Privat Oppskrift",
"Private_Recipe_Help": "Oppskriften er bare vist til deg og dem du har delt den med.",
"reusable_help_text": "Burde invitasjonslenken være brukbar for flere enn én bruker.",
"Add_Step": "Legg til steg",
"Keywords": "Nøkkelord",
"Books": "Bøker",
"Proteins": "",
"Fats": "",
"Proteins": "Protein",
"Fats": "Fett",
"Carbohydrates": "Karbohydrater",
"Calories": "",
"Energy": "",
"Nutrition": "",
"Date": "",
"Share": "",
"Automation": "",
"Parameter": "",
"Export": "",
"Copy": "",
"Rating": "Karakter",
"Calories": "Kalorier",
"Energy": "Energi",
"Nutrition": "Næring",
"Date": "Dato",
"Share": "Del",
"Automation": "Automatiser",
"Parameter": "Parameter",
"Export": "Eksporter",
"Copy": "Kopier",
"Rating": "Vurdering",
"Close": "Lukk",
"Cancel": "",
"Cancel": "Avbryt",
"Link": "Lenke",
"Add": "",
"New": "",
"Note": "",
"Success": "",
"Failure": "",
"Protected": "",
"Add": "Legg til",
"New": "Ny",
"Note": "Merk",
"Success": "Vellykket",
"Failure": "Feil",
"Protected": "Beskyttet",
"Ingredients": "Ingredienser",
"Supermarket": "Butikk",
"Categories": "",
"Category": "",
"Selected": "",
"min": "",
"Servings": "",
"Waiting": "",
"Preparation": "",
"External": "",
"Size": "",
"Files": "",
"File": "",
"Edit": "",
"Image": "",
"Delete": "",
"Open": "",
"Ok": "",
"Save": "",
"Step": "",
"Search": "",
"Import": "",
"Print": "",
"Categories": "Kategorier",
"Category": "Kategori",
"Selected": "Valgte",
"min": "min",
"Servings": "Porsjoner",
"Waiting": "Venter",
"Preparation": "Forberedelse",
"External": "Ekstern",
"Size": "Størrelse",
"Files": "Filer",
"File": "Fil",
"Edit": "Rediger",
"Image": "Bilde",
"Delete": "Slett",
"Open": "Åpne",
"Ok": "Ok",
"Save": "Lagre",
"Step": "Steg",
"Search": "Søk",
"Import": "Importer",
"Print": "Skriv ut",
"Settings": "Innstillinger",
"or": "",
"and": "",
"Information": "",
"Download": "",
"or": "eller",
"and": "og",
"Information": "Informasjon",
"Download": "Last ned",
"Create": "Opprett",
"Search Settings": "",
"View": "",
"Recipes": "",
"Move": "",
"Merge": "",
"Parent": "",
"Copy Link": "",
"Copy Token": "",
"delete_confirmation": "",
"move_confirmation": "",
"merge_confirmation": "",
"create_rule": "",
"move_selection": "",
"merge_selection": "",
"Root": "",
"Search Settings": "Søk Instillinger",
"View": "Visning",
"Recipes": "Oppskrift",
"Move": "Flytt",
"Merge": "Slå sammen",
"Parent": "Forelder",
"Copy Link": "Kopier lenke",
"Copy Token": "Kopier Token",
"delete_confirmation": "Er du sikker på at du vill slette {source}?",
"move_confirmation": "Flytt<i>{child}</i> til forelder <i>{parent}</i>",
"merge_confirmation": "Erstatt<i>{source}</i> med <i>{target}</i>",
"create_rule": "og opprett automasjon",
"move_selection": "Velg en forelder {type} å flytte {source} til.",
"merge_selection": "Erstatt alle tilfeller av {source} med den valgte {type}.",
"Root": "Rot",
"Ignore_Shopping": "",
"Shopping_Category": "",
"Shopping_Categories": "",
@@ -284,33 +284,33 @@
"Hide_Keyword": "",
"Hour": "",
"Hours": "",
"Day": "",
"Days": "",
"Day": "Dag",
"Days": "Dager",
"Second": "",
"Seconds": "",
"Clear": "",
"Users": "",
"Invites": "",
"err_move_self": "",
"nothing": "",
"err_merge_self": "",
"show_sql": "",
"filter_to_supermarket_desc": "",
"CategoryName": "",
"SupermarketName": "",
"CategoryInstruction": "",
"nothing": "Ingenting å gjøre",
"err_merge_self": "Kan ikke slå sammen linje med seg selv",
"show_sql": "Vis SQL",
"filter_to_supermarket_desc": "Som standard, filtrerer handlelisten til å kun inkludere kategorier for den valgte butikken.",
"CategoryName": "Kategori navn",
"SupermarketName": "Butikk Navn",
"CategoryInstruction": "Dra kategorier for å endre på rekkefølgen de vises i handlelisten.",
"shopping_recent_days_desc": "",
"shopping_recent_days": "",
"download_pdf": "",
"download_csv": "",
"download_pdf": "Last ned PDF",
"download_csv": "Last ned CSV",
"csv_delim_help": "",
"csv_delim_label": "",
"SuccessClipboard": "",
"copy_to_clipboard": "",
"csv_prefix_help": "",
"csv_prefix_label": "",
"copy_markdown_table": "",
"in_shopping": "",
"copy_to_clipboard": "Kopier til utklippstavle",
"csv_prefix_help": "Prefiks for å legge til når du kopierer listen til utklippstavlen.",
"csv_prefix_label": "Liste prefiks",
"copy_markdown_table": "Kopier som Markdown tabell",
"in_shopping": "I handleliste",
"DelayUntil": "",
"Pin": "",
"Unpin": "",
@@ -332,37 +332,37 @@
"food_recipe_help": "",
"Foods": "",
"Account": "",
"Cosmetic": "",
"API": "",
"enable_expert": "",
"expert_mode": "",
"simple_mode": "",
"advanced": "",
"fields": "",
"show_keywords": "",
"show_foods": "",
"show_books": "",
"show_rating": "",
"show_units": "",
"show_filters": "",
"not": "",
"save_filter": "",
"filter_name": "",
"left_handed": "",
"left_handed_help": "",
"Custom Filter": "",
"shared_with": "",
"sort_by": "",
"asc": "",
"desc": "",
"date_viewed": "",
"last_cooked": "",
"times_cooked": "",
"date_created": "",
"show_sortby": "",
"search_rank": "",
"make_now": "",
"recipe_filter": "",
"Cosmetic": "Kosmetisk",
"API": "API",
"enable_expert": "Aktiver Ekspert Modus",
"expert_mode": "Ekspert Modus",
"simple_mode": "Enkel Modus",
"advanced": "Avansert",
"fields": "Felt",
"show_keywords": "Vis Nøkkelord",
"show_foods": "Vis Mat",
"show_books": "Vis bøker",
"show_rating": "Vis vurdering",
"show_units": "Vis enheter",
"show_filters": "Vis filtre",
"not": "ikke",
"save_filter": "Lagre filtre",
"filter_name": "Filtrer Navn",
"left_handed": "Venstrehendt Modus",
"left_handed_help": "Vil optimalisere bukergrensesnittet for bruk med venstre hånden.",
"Custom Filter": "Egendefinert Filter",
"shared_with": "Delt med",
"sort_by": "Sorter etter",
"asc": "Stigende",
"desc": "Fallende",
"date_viewed": "Sist sett",
"last_cooked": "Sist tilberedt",
"times_cooked": "Antall ganger tilberedt",
"date_created": "Dato laget",
"show_sortby": "Vis sorter etter",
"search_rank": "Søk etter vurdering",
"make_now": "Lag nå",
"recipe_filter": "Oppskrift filter",
"book_filter_help": "",
"review_shopping": "",
"view_recipe": "",
@@ -373,9 +373,9 @@
"ingredient_list": "",
"explain": "",
"filter": "",
"Website": "",
"App": "",
"Message": "",
"Website": "Nettside",
"App": "App",
"Message": "Melding",
"Bookmarklet": "",
"Sticky_Nav": "",
"Sticky_Nav_Help": "",
@@ -420,11 +420,11 @@
"Quick actions": "",
"Ratings": "",
"Internal": "",
"Units": "",
"Manage_Emails": "",
"Change_Password": "",
"Units": "Enhet",
"Manage_Emails": "Administrer e-poster",
"Change_Password": "Endre passord",
"Social_Authentication": "",
"Random Recipes": "",
"Random Recipes": "Tilfeldige oppskrifter",
"parameter_count": "",
"select_keyword": "",
"add_keyword": "",
@@ -435,13 +435,13 @@
"remove_selection": "",
"empty_list": "",
"Select": "Velg",
"Supermarkets": "",
"User": "",
"Username": "",
"First_name": "",
"Last_name": "",
"Supermarkets": "Butikker",
"User": "Bruker",
"Username": "Brukernavn",
"First_name": "Fornavn",
"Last_name": "Etternavn",
"Keyword": "Nøkkelord",
"Advanced": "",
"Advanced": "Avansert",
"Page": "",
"Single": "",
"Multiple": "",
@@ -478,5 +478,19 @@
"Use_Plural_Food_Simple": "",
"plural_usage_info": "",
"Create Recipe": "",
"Import Recipe": ""
"Import Recipe": "",
"per_serving": "Per porsjon",
"open_data_help_text": "Tandoor Open Data prosjektet gir fra fellesskapet til Tandoor. Dette feltet fylles ut automatisk når det importeres og tillater oppdateringer i fremtiden.",
"Open_Data_Slug": "Åpne data Slug",
"Open_Data_Import": "Åpne Data Import",
"recipe_property_info": "Du kan også legge til egenskaper til mat for å kalkulere dem automatisk basert på oppskriften!",
"Update_Existing_Data": "Oppdater eksisterende data",
"Use_Metric": "Bruk metriske enheter",
"Learn_More": "Lær mer",
"converted_unit": "Konverter enhet",
"converted_amount": "Konverter mengde",
"base_unit": "Baseenhet",
"base_amount": "Basemengde",
"Datatype": "Data-type",
"Number of Objects": "Antall objekter"
}

View File

@@ -480,5 +480,24 @@
"Description_Replace": "Zmień opis",
"Instruction_Replace": "Zmień instrukcję",
"Import Recipe": "Importuj przepis",
"Create Recipe": "Utwórz przepis"
"Create Recipe": "Utwórz przepis",
"recipe_property_info": "Możesz także dodawać właściwości do żywności, aby przeliczać ją automatycznie na podstawie Twojego przepisu!",
"per_serving": "na porcje",
"open_data_help_text": "Projekt Tandoor Open Data dostarcza danych przesłanych przez społeczność dla Tandoor. To pole jest wypełniane automatycznie podczas importu i umożliwia aktualizacje w przyszłości.",
"Open_Data_Slug": "Open Data Slug",
"Open_Data_Import": "Open Data Import",
"Data_Import_Info": "Wzbogać swoją Przestrzeń, importując wyselekcjonowaną przez społeczność listę żywności, jednostek i nie tylko, aby ulepszyć swoją kolekcję przepisów.",
"Update_Existing_Data": "Zaktualizuj istniejące dane",
"Use_Metric": "Użyj jednostek metrycznych",
"Learn_More": "Dowiedz się więcej",
"converted_unit": "Przeliczona jednostka",
"converted_amount": "Przeliczona ilość",
"base_unit": "Jednostka podstawowa",
"base_amount": "Ilość bazowa",
"Datatype": "Typ danych",
"Number of Objects": "Ilość obiektów",
"Property": "Właściwość",
"Conversion": "Konwersja",
"Properties": "Właściwości",
"total": "łącznie"
}

View File

@@ -1,11 +1,11 @@
{
"warning_feature_beta": "Данный функционал находится в сдадии BETA (тестируется). Возможны баги и серьезные изменения функционала в будующем.",
"warning_feature_beta": "Данный функционал находится в стадии BETA (тестируется). Возможны баги и серьезные изменения функционала в будущем.",
"err_fetching_resource": "Ошибка при загрузке продукта!",
"err_creating_resource": "Ошибка при создании продукта!",
"err_updating_resource": "Ошибка при редактировании продукта!",
"err_deleting_resource": "Ошибка при удалении продукта!",
"success_fetching_resource": "Продукт успешно загружен!",
"success_creating_resource": "Продукт успешно загружен!",
"success_creating_resource": "Продукт успешно создан!",
"success_updating_resource": "Продукт успешно обновлен!",
"success_deleting_resource": "Продукт успешно удален!",
"file_upload_disabled": "Выгрузка файла не активирована в настройках.",
@@ -13,7 +13,7 @@
"confirm_delete": "Вы уверены, что хотите удалить этот объект?",
"import_running": "Идет загрузка, пожалуйста ждите!",
"all_fields_optional": "Все поля не обязательны для заполнения.",
"convert_internal": "Конвретировать рецепт во внутренний формат",
"convert_internal": "Конвертировать рецепт во внутренний формат",
"show_only_internal": "Показывать только рецепты во внутреннем формате",
"show_split_screen": "Двухколоночный вид",
"Log_Recipe_Cooking": "Журнал приготовления",
@@ -211,13 +211,13 @@
"FoodNotOnHand": "{food} отсутствует в наличии.",
"Undefined": "Неизвестно",
"AddFoodToShopping": "Добавить {food} в ваш список покупок",
"success_moving_resource": "Успешное перемещение ресурса!",
"success_merging_resource": "Ресурс успешно присоединен!",
"success_moving_resource": "Успешное перемещение продукта!",
"success_merging_resource": "Продукт успешно присоединен!",
"Shopping_Categories": "Категории покупок",
"Search Settings": "Искать настройки",
"err_merging_resource": "Произошла ошибка при перемещении ресурса!",
"err_merging_resource": "Произошла ошибка при перемещении продукта!",
"Remove_nutrition_recipe": "Уберите питательные вещества из рецепта",
"err_moving_resource": "Произошла ошибка при перемещении ресурса!",
"err_moving_resource": "Произошла ошибка при перемещении продукта!",
"NotInShopping": "{food} отсутствует в вашем списке покупок.",
"RemoveFoodFromShopping": "Удалить {food} из вашего списка покупок",
"ShowDelayed": "Показать отложенные элементы",
@@ -345,6 +345,6 @@
"GroupBy": "Сгруппировать по",
"facet_count_info": "Показывать количество рецептов в фильтрах поиска.",
"food_inherit_info": "Поля для продуктов питания, которые должны наследоваться по умолчанию.",
"warning_space_delete": "Вы можете удалить свое пространство, включая все рецепты, списки покупок, планы питания и все остальное, что вы создали. Этого нельзя отменить! Ты уверен, что хочешь это сделать?",
"warning_space_delete": "Вы можете удалить свое пространство, включая все рецепты, списки покупок, планы питания и все остальное, что вы создали. Этого нельзя отменить! Вы уверены, что хотите это сделать?",
"Description_Replace": "Изменить описание"
}

View File

View File

@@ -91,11 +91,13 @@ export class Models {
"substitute_children",
"reset_inherit",
"child_inherit_fields",
"open_data_slug",
],
],
form: {
show_help: true,
component: "FoodEditor",
name: {
form_field: true,
type: "text",
@@ -126,6 +128,14 @@ export class Models {
label: "Recipe", // form.label always translated in utils.getForm()
help_text: "food_recipe_help", // form.help_text always translated
},
open_data_slug: {
form_field: true,
type: "text",
field: "open_data_slug",
disabled: true,
label: "Open_Data_Slug",
help_text: "open_data_help_text",
},
onhand: {
form_field: true,
type: "checkbox",
@@ -269,8 +279,9 @@ export class Models {
apiName: "Unit",
paginated: true,
create: {
params: [["name", "plural_name", "description",]],
params: [["name", "plural_name", "description", "open_data_slug",]],
form: {
show_help: true,
name: {
form_field: true,
type: "text",
@@ -292,6 +303,14 @@ export class Models {
label: "Description",
placeholder: "",
},
open_data_slug: {
form_field: true,
type: "text",
field: "open_data_slug",
disabled: true,
label: "Open_Data_Slug",
help_text: "open_data_help_text",
},
},
},
merge: true,
@@ -418,6 +437,7 @@ export class Models {
create: {
params: [["name", "description", "category_to_supermarket"]],
form: {
show_help: true,
name: {
form_field: true,
type: "text",
@@ -442,6 +462,14 @@ export class Models {
label: "Categories", // form.label always translated in utils.getForm()
placeholder: "",
},
open_data_slug: {
form_field: true,
type: "text",
field: "open_data_slug",
disabled: true,
label: "Open_Data_Slug",
help_text: "open_data_help_text",
},
},
config: {
function: "SupermarketWithCategories",
@@ -562,6 +590,129 @@ export class Models {
},
}
static UNIT_CONVERSION = {
name: "Unit Conversion",
apiName: "UnitConversion",
paginated: false,
list: {
header_component: {
name: "BetaWarning",
},
},
create: {
params: [['food', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'open_data_slug']],
form: {
show_help: true,
// TODO add proper help texts for everything
food: {
form_field: true,
type: "lookup",
field: "food",
list: "FOOD",
list_label: "name",
label: "Food",
multiple: false,
},
base_amount: {
form_field: true,
type: "text",
field: "base_amount",
label: "base_amount",
placeholder: "",
},
base_unit: {
form_field: true,
type: "lookup",
field: "base_unit",
list: "UNIT",
list_label: "name",
label: "base_unit",
multiple: false,
},
converted_amount: {
form_field: true,
type: "text",
field: "converted_amount",
label: "converted_amount",
placeholder: "",
},
converted_unit: {
form_field: true,
type: "lookup",
field: "converted_unit",
list: "UNIT",
list_label: "name",
label: "converted_unit",
multiple: false,
},
open_data_slug: {
form_field: true,
type: "text",
field: "open_data_slug",
disabled: true,
label: "Open_Data_Slug",
help_text: "open_data_help_text",
},
},
},
}
static PROPERTY_TYPE = {
name: "Property Type",
apiName: "PropertyType",
paginated: false,
list: {
header_component: {
name: "BetaWarning",
},
},
create: {
params: [['name', 'icon', 'unit', 'description']],
form: {
show_help: true,
name: {
form_field: true,
type: "text",
field: "name",
label: "Name",
placeholder: "",
},
icon: {
form_field: true,
type: "emoji",
field: "icon",
label: "Icon",
placeholder: "",
},
unit: {
form_field: true,
type: "text",
field: "unit",
label: "Unit",
placeholder: "",
},
description: {
form_field: true,
type: "text",
field: "description",
label: "Description",
placeholder: "",
},
open_data_slug: {
form_field: true,
type: "text",
field: "open_data_slug",
disabled: true,
label: "Open_Data_Slug",
help_text: "open_data_help_text",
},
},
},
}
static RECIPE = {
name: "Recipe",
apiName: "Recipe",

File diff suppressed because it is too large Load Diff

View File

@@ -50,7 +50,7 @@ export class StandardToasts {
static FAIL_MOVE = "FAIL_MOVE"
static FAIL_MERGE = "FAIL_MERGE"
static makeStandardToast(context, toast, err) {
static makeStandardToast(context, toast, err = undefined, always_show_errors = false) {
let title = ''
let msg = ''
let variant = ''
@@ -124,7 +124,7 @@ export class StandardToasts {
}
let DEBUG = localStorage.getItem("DEBUG") === "True" || false
let DEBUG = localStorage.getItem("DEBUG") === "True" || always_show_errors
if (err !== undefined && 'response' in err && 'headers' in err.response) {
if (DEBUG && err.response.headers['content-type'] === 'application/json' && err.response.status < 500) {
@@ -368,6 +368,9 @@ export const ApiMixin = {
let func = setup.function
let parameters = buildParams(options, setup)
let apiClient = new ApiApiFactory()
if (model.apiClient !== undefined) {
apiClient = model.apiClient
}
return apiClient[func](...parameters)
},
genericGetAPI: function (url, options) {

File diff suppressed because it is too large Load Diff