mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-30 13:40:01 -05:00
Compare commits
238 Commits
feature/co
...
1.4.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82d1a75d80 | ||
|
|
50429207c5 | ||
|
|
589bc1f1aa | ||
|
|
824dcefc1a | ||
|
|
3f8c952237 | ||
|
|
077db58de0 | ||
|
|
3c527fd112 | ||
|
|
cb363d6321 | ||
|
|
39656152d3 | ||
|
|
22c88e5269 | ||
|
|
89550e8345 | ||
|
|
9846c4df18 | ||
|
|
924d1cb71b | ||
|
|
44236f611e | ||
|
|
012dea5a0c | ||
|
|
820c9b704f | ||
|
|
ed92926ec4 | ||
|
|
bc560ee76d | ||
|
|
b6c4130e4b | ||
|
|
b0ca391bb4 | ||
|
|
45a6b1d386 | ||
|
|
4626ffcbc5 | ||
|
|
c3a9cc94fa | ||
|
|
a8eb8bb8d7 | ||
|
|
b14c9aa68c | ||
|
|
b03db7ad36 | ||
|
|
cc706a1195 | ||
|
|
b1028f49d6 | ||
|
|
6d0cc96cc8 | ||
|
|
969a91e751 | ||
|
|
f47470a9ad | ||
|
|
9ad9fe275d | ||
|
|
33448c98c0 | ||
|
|
90e389f2fa | ||
|
|
af7acd7473 | ||
|
|
dfa0794281 | ||
|
|
a36d42df84 | ||
|
|
269dded046 | ||
|
|
826ccc2760 | ||
|
|
7190dc17a7 | ||
|
|
5e332bb88c | ||
|
|
fb21622bfe | ||
|
|
191c38db8f | ||
|
|
71132fe992 | ||
|
|
d1d568a9d3 | ||
|
|
68d4fb3b59 | ||
|
|
b4c26682c7 | ||
|
|
e2cfb53ec4 | ||
|
|
274b17a860 | ||
|
|
97bcf1111b | ||
|
|
3b9d221258 | ||
|
|
d8bcb8bcb6 | ||
|
|
5cb0f1761a | ||
|
|
516b551528 | ||
|
|
f43d4d3971 | ||
|
|
e0dd70027b | ||
|
|
79efd94d6f | ||
|
|
f16870c59e | ||
|
|
c690bc18a0 | ||
|
|
394f24c29f | ||
|
|
c2a8214290 | ||
|
|
4d0cfc95e4 | ||
|
|
972c103538 | ||
|
|
2f62f51dc2 | ||
|
|
56ee173c07 | ||
|
|
774c633d5c | ||
|
|
93dd35fde3 | ||
|
|
666e4d282f | ||
|
|
ed6ca613ff | ||
|
|
285364e12a | ||
|
|
9a46a91652 | ||
|
|
4e4078f3da | ||
|
|
41c9290ba8 | ||
|
|
4460fe013f | ||
|
|
558a5d6554 | ||
|
|
388f2f441f | ||
|
|
11e8af4c46 | ||
|
|
387893e1ef | ||
|
|
78dc1bf9ec | ||
|
|
12638096b1 | ||
|
|
3e82199c44 | ||
|
|
b4bcf5c032 | ||
|
|
afdd92c903 | ||
|
|
1462300eda | ||
|
|
d0417d09db | ||
|
|
39c8da8305 | ||
|
|
c9af9277ae | ||
|
|
5e2be34f7b | ||
|
|
73a2476a79 | ||
|
|
f61032cc74 | ||
|
|
f6956388c7 | ||
|
|
7e9b303e8b | ||
|
|
f617dedfa2 | ||
|
|
942c26c581 | ||
|
|
a00d90398e | ||
|
|
61e6d855ec | ||
|
|
6b59f53273 | ||
|
|
798457e7e2 | ||
|
|
6176eeb024 | ||
|
|
56252a707a | ||
|
|
9c649d743c | ||
|
|
00b1ca5454 | ||
|
|
f2e1648556 | ||
|
|
b1945edf04 | ||
|
|
9f20db0ed3 | ||
|
|
852756f099 | ||
|
|
452617ef30 | ||
|
|
4e972835e5 | ||
|
|
5e4d983b79 | ||
|
|
0de8065212 | ||
|
|
1a6e677c06 | ||
|
|
8307828528 | ||
|
|
12a1f261db | ||
|
|
79e400ce6f | ||
|
|
d51de1bd79 | ||
|
|
6b3f1aa038 | ||
|
|
7d7803a07e | ||
|
|
559c574fd6 | ||
|
|
5e0131acd9 | ||
|
|
d560b7a143 | ||
|
|
15e5366dbb | ||
|
|
6ccfdd6f2e | ||
|
|
d2b79990cb | ||
|
|
01bb84e40b | ||
|
|
f66b422f68 | ||
|
|
abab970f08 | ||
|
|
edaa6de71d | ||
|
|
85a65127cf | ||
|
|
99035190f4 | ||
|
|
f1611fbafd | ||
|
|
e42ff2fb8b | ||
|
|
bc93071167 | ||
|
|
410fa58d47 | ||
|
|
06fd03fbde | ||
|
|
d169456c78 | ||
|
|
7785aa4904 | ||
|
|
33798fe47e | ||
|
|
0522b15cfd | ||
|
|
8cfd3995d0 | ||
|
|
84cdd8bb78 | ||
|
|
ad0177235d | ||
|
|
e5782151a1 | ||
|
|
adf4dafd01 | ||
|
|
4214ef4a9f | ||
|
|
1df0ad202f | ||
|
|
e35cbba8b2 | ||
|
|
e6fa660c8f | ||
|
|
8aefdb71bb | ||
|
|
5da8c6fe7b | ||
|
|
520697e988 | ||
|
|
7fe6fd3462 | ||
|
|
85b3941539 | ||
|
|
6f5ea7bb48 | ||
|
|
e9a2b101d8 | ||
|
|
c01faff135 | ||
|
|
bcee0007a5 | ||
|
|
f5ec956e08 | ||
|
|
56926d55ba | ||
|
|
55cfe9e9e7 | ||
|
|
31bdd97a56 | ||
|
|
eb60cbdd6b | ||
|
|
39ccf7bbcf | ||
|
|
f92ee32c01 | ||
|
|
5aecf7e61c | ||
|
|
20435450f3 | ||
|
|
13e5fb4143 | ||
|
|
bdc6434839 | ||
|
|
796609de37 | ||
|
|
8759e8dd73 | ||
|
|
f8bf54189e | ||
|
|
3e2988f998 | ||
|
|
2e5571f0a9 | ||
|
|
c97bb900a3 | ||
|
|
b1ad5ef205 | ||
|
|
9994b6f9c2 | ||
|
|
a8a590a942 | ||
|
|
4e13fb3b8c | ||
|
|
24f331c208 | ||
|
|
16d0fc38f9 | ||
|
|
5e4cac52d6 | ||
|
|
b489a2d849 | ||
|
|
7c5707e0c0 | ||
|
|
946699a335 | ||
|
|
4888e2d476 | ||
|
|
44b2c02034 | ||
|
|
c150c7f84e | ||
|
|
97503a68d8 | ||
|
|
126a2d870e | ||
|
|
02bad8cfb9 | ||
|
|
d9465c7f9d | ||
|
|
ead3168d80 | ||
|
|
a71bba307e | ||
|
|
d2a652891c | ||
|
|
a70ac42717 | ||
|
|
a7bcf105dc | ||
|
|
8be02b4e74 | ||
|
|
9ba9cda1c0 | ||
|
|
4310282dc3 | ||
|
|
c2c08391cc | ||
|
|
bc9d077b9d | ||
|
|
fe0f739bd5 | ||
|
|
e5b11a34f6 | ||
|
|
1df7a4df91 | ||
|
|
d401c143ec | ||
|
|
00a59baa92 | ||
|
|
327c83ce32 | ||
|
|
3371102e64 | ||
|
|
aec396e214 | ||
|
|
2b52b5c264 | ||
|
|
19c24a85a1 | ||
|
|
c147903f1e | ||
|
|
9dedc5b8fa | ||
|
|
d781cbe743 | ||
|
|
37bd2017b0 | ||
|
|
2de8070156 | ||
|
|
f70377c59b | ||
|
|
6fc4151de5 | ||
|
|
1fa001aad3 | ||
|
|
b84e03c58b | ||
|
|
e9dac25ff4 | ||
|
|
611787dbb6 | ||
|
|
bfbfb1d2a8 | ||
|
|
d9662f7fa5 | ||
|
|
9e44944b1d | ||
|
|
4de9a7ff89 | ||
|
|
32a663c5d7 | ||
|
|
3bee5ed35a | ||
|
|
bee5d6b7eb | ||
|
|
00ed9b07b6 | ||
|
|
2279bba838 | ||
|
|
57f5343c77 | ||
|
|
89a5f92ace | ||
|
|
33e5bb7d0a | ||
|
|
16c0189b80 | ||
|
|
fd325c1797 | ||
|
|
2902262503 | ||
|
|
12ad6af8c3 | ||
|
|
cf24e1014a |
@@ -97,7 +97,7 @@ GUNICORN_MEDIA=0
|
||||
# ACCOUNT_EMAIL_SUBJECT_PREFIX=
|
||||
|
||||
# allow authentication via reverse proxy (e.g. authelia), leave off if you dont know what you are doing
|
||||
# see docs for more information https://vabene1111.github.io/recipes/features/authentication/
|
||||
# see docs for more information https://docs.tandoor.dev/features/authentication/
|
||||
# when unset: 0 (false)
|
||||
REVERSE_PROXY_AUTH=0
|
||||
|
||||
@@ -126,7 +126,7 @@ REVERSE_PROXY_AUTH=0
|
||||
# ENABLE_METRICS=0
|
||||
|
||||
# allows you to setup OAuth providers
|
||||
# see docs for more information https://vabene1111.github.io/recipes/features/authentication/
|
||||
# see docs for more information https://docs.tandoor.dev/features/authentication/
|
||||
# SOCIAL_PROVIDERS = allauth.socialaccount.providers.github, allauth.socialaccount.providers.nextcloud,
|
||||
|
||||
# Should a newly created user from a social provider get assigned to the default space and given permission by default ?
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on: [push]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -14,13 +14,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- name: Install Vue dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn install
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Install Django dependencies
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
sudo apt-get install -y libsasl2-dev python-dev libldap2-dev libssl-dev
|
||||
sudo apt-get install -y libsasl2-dev python3-dev libldap2-dev libssl-dev
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
python3 manage.py collectstatic --noinput
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -84,3 +84,4 @@ cookbook/static/vue
|
||||
vue/webpack-stats.json
|
||||
cookbook/templates/sw.js
|
||||
.prettierignore
|
||||
vue/.yarn
|
||||
|
||||
2
.idea/dictionaries/vabene1111_PC.xml
generated
2
.idea/dictionaries/vabene1111_PC.xml
generated
@@ -3,8 +3,6 @@
|
||||
<words>
|
||||
<w>autosync</w>
|
||||
<w>chowdown</w>
|
||||
<w>cookingmachine</w>
|
||||
<w>cookit</w>
|
||||
<w>csrftoken</w>
|
||||
<w>gunicorn</w>
|
||||
<w>ical</w>
|
||||
|
||||
2
.idea/recipes.iml
generated
2
.idea/recipes.iml
generated
@@ -18,7 +18,7 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/staticfiles" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.9 (recipes)" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.11 (recipes)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
|
||||
@@ -6,5 +6,4 @@ Since this software is still considered beta/WIP support is always only given fo
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please open a normal public issue if you have any security related concerns. If you feel like the issue should not be discussed in
|
||||
public just open a generic issue and we will discuss further communication there (since GitHub does not allow everyone to create a security advisory :/).
|
||||
Please use GitHub Security Advisories to report any kind of security vulnerabilities.
|
||||
|
||||
@@ -32,11 +32,11 @@ admin.site.unregister(Group)
|
||||
@admin.action(description='Delete all data from a space')
|
||||
def delete_space_action(modeladmin, request, queryset):
|
||||
for space in queryset:
|
||||
space.save()
|
||||
space.safe_delete()
|
||||
|
||||
|
||||
class SpaceAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing', 'use_plural')
|
||||
search_fields = ('name', 'created_by__username')
|
||||
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import json
|
||||
import random
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
from django.utils.timezone import now
|
||||
|
||||
from cookbook.models import CookingMachine
|
||||
|
||||
|
||||
# Tandoor is not affiliated in any way or form with the holders of Trademark or other right associated with the mentioned names. All mentioned protected names are purely used to identify to the user a certain device or integration.
|
||||
class HomeConnectCookit:
|
||||
AUTH_AUTHORIZE_URL = 'https://api.home-connect.com/security/oauth/authorize'
|
||||
AUTH_TOKEN_URL = 'https://api.home-connect.com/security/oauth/token'
|
||||
AUTH_REFRESH_URL = 'https://api.home-connect.com/security/oauth/token'
|
||||
|
||||
RECIPE_API_URL = 'https://prod.reu.rest.homeconnectegw.com/user-generated-recipes/server/api/v1/recipes'
|
||||
IMAGE_API_URL = 'https://prod.reu.rest.homeconnectegw.com/user-generated-recipes/server/api/v1/images'
|
||||
|
||||
CLIENT_ID = '' # TODO load from .env settings
|
||||
_CLIENT_SECRET = '' # TODO load from .env settings
|
||||
|
||||
_cooking_machine = None
|
||||
|
||||
def __init__(self, cooking_machine):
|
||||
self._cooking_machine = cooking_machine
|
||||
|
||||
def get_auth_link(self):
|
||||
return f"{self.AUTH_AUTHORIZE_URL}?client_id={self.CLIENT_ID}&response_type=code&scope=IdentifyAppliance%20Settings&state={random.randint(100000, 999999)}"
|
||||
|
||||
def _validate_token(self):
|
||||
if self._cooking_machine.access_token is None and self._cooking_machine.refresh_token is None:
|
||||
return False # user needs to login
|
||||
elif self._cooking_machine.access_token_expiry < now() + timedelta(minutes=10):
|
||||
return False # refresh token
|
||||
|
||||
def _refresh_access_token(self):
|
||||
token_response = requests.post(self.AUTH_REFRESH_URL, {
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': self._cooking_machine.refresh_token,
|
||||
'client_secret': self._CLIENT_SECRET,
|
||||
})
|
||||
if token_response.status_code == 200:
|
||||
token_response_body = json.loads(token_response.content)
|
||||
self._cooking_machine.access_token = token_response_body['access_token']
|
||||
self._cooking_machine.access_token_expiry = now() + timedelta(seconds=(token_response_body['expires_in'] - (60 * 10)))
|
||||
self._cooking_machine.refresh_token = token_response_body['refresh_token']
|
||||
self._cooking_machine.refresh_token_expiry = now() + timedelta(days=58)
|
||||
self._cooking_machine.save()
|
||||
|
||||
def get_access_token(self, code):
|
||||
token_response = requests.post(self.AUTH_TOKEN_URL, {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'client_id': self.CLIENT_ID,
|
||||
'client_secret': self._CLIENT_SECRET,
|
||||
})
|
||||
if token_response.status_code == 200:
|
||||
token_response_body = json.loads(token_response.content)
|
||||
self._cooking_machine.access_token = token_response_body['access_token']
|
||||
self._cooking_machine.access_token_expiry = now() + timedelta(seconds=(token_response_body['expires_in'] - (60 * 10)))
|
||||
self._cooking_machine.refresh_token = token_response_body['refresh_token']
|
||||
self._cooking_machine.refresh_token_expiry = now() + timedelta(days=58)
|
||||
self._cooking_machine.save()
|
||||
|
||||
def _get_default_headers(self):
|
||||
auth_token = ''
|
||||
return {
|
||||
'authorization': f'Bearer {self._cooking_machine.access_token}',
|
||||
'accept-language': "en-US",
|
||||
"referer": "https://prod.reu.rest.homeconnectegw.com/user-generated- recipes/client/editor/recipedetails",
|
||||
"user-agent": "Mozilla/5.0 (Linux; Android 10; Android SDK built for x86 Build/QSR1.210802.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.185 Mobile Safari/537.36",
|
||||
"x-requested-with": "com.bshg.homeconnect.android.release"
|
||||
}
|
||||
|
||||
def push_recipe(self, recipe):
|
||||
data = {
|
||||
"title": recipe.name,
|
||||
"description": recipe.description,
|
||||
"ingredients": [],
|
||||
"steps": [],
|
||||
"durations": {
|
||||
"totalTime": (recipe.working_time + recipe.waiting_time) * 60,
|
||||
"cookingTime": 0, # recipe.waiting_time * 60, #TODO cooking time must be sum of step duration attribute, otherwise creation fails
|
||||
"preparationTime": recipe.working_time * 60,
|
||||
},
|
||||
"keywords": [],
|
||||
"servings": {
|
||||
"amount": int(recipe.servings),
|
||||
"unit": 'portion', # required
|
||||
},
|
||||
"servingTipsStep": {
|
||||
"textInstructions": "Serve"
|
||||
},
|
||||
"complexityLevel": "medium",
|
||||
"image": {
|
||||
"url": "https://media3.bsh-group.com/Recipes/800x480/17062805_210217_My-own-recipe_Picture_188ppi.jpg",
|
||||
"mimeType": "image/jpeg"
|
||||
# TODO add image upload
|
||||
},
|
||||
"accessories": [], # TODO add once tandoor supports tools
|
||||
"legalDisclaimerApproved": True # TODO force user to approve disclaimer
|
||||
}
|
||||
for step in recipe.steps.all():
|
||||
data['steps'].append({
|
||||
"textInstructions": step.instruction
|
||||
})
|
||||
for i in step.ingredients.all():
|
||||
data['ingredients'].append({
|
||||
"amount": int(i.amount),
|
||||
"unit": i.unit.name,
|
||||
"name": i.food.name,
|
||||
})
|
||||
# TODO create synced recipe
|
||||
response = requests.post(f'{self.RECIPE_API_URL}', json=data, headers=self._get_default_headers())
|
||||
return response
|
||||
@@ -154,6 +154,7 @@ class ImportExportBase(forms.Form):
|
||||
COOKBOOKAPP = 'COOKBOOKAPP'
|
||||
COPYMETHAT = 'COPYMETHAT'
|
||||
COOKMATE = 'COOKMATE'
|
||||
REZEPTSUITEDE = 'REZEPTSUITEDE'
|
||||
PDF = 'PDF'
|
||||
|
||||
type = forms.ChoiceField(choices=(
|
||||
@@ -162,7 +163,7 @@ class ImportExportBase(forms.Form):
|
||||
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
|
||||
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
|
||||
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'), (MELARECIPES, 'Melarecipes'),
|
||||
(COOKMATE, 'Cookmate')
|
||||
(COOKMATE, 'Cookmate'), (REZEPTSUITEDE, 'Recipesuite.de')
|
||||
))
|
||||
|
||||
|
||||
@@ -533,11 +534,13 @@ class SpacePreferenceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Space
|
||||
|
||||
fields = ('food_inherit', 'reset_food_inherit', 'show_facet_count')
|
||||
fields = ('food_inherit', 'reset_food_inherit', 'show_facet_count', 'use_plural')
|
||||
|
||||
help_texts = {
|
||||
'food_inherit': _('Fields on food that should be inherited by default.'),
|
||||
'show_facet_count': _('Show recipe counts on search filters'), }
|
||||
'show_facet_count': _('Show recipe counts on search filters'),
|
||||
'use_plural': _('Use the plural form for units and food inside this space.'),
|
||||
}
|
||||
|
||||
widgets = {
|
||||
'food_inherit': MultiSelectWidget
|
||||
|
||||
@@ -28,7 +28,7 @@ class IngredientParser:
|
||||
self.food_aliases = c
|
||||
caches['default'].touch(FOOD_CACHE_KEY, 30)
|
||||
else:
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').all():
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').order_by('order').all():
|
||||
self.food_aliases[a.param_1] = a.param_2
|
||||
caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
|
||||
|
||||
@@ -37,7 +37,7 @@ class IngredientParser:
|
||||
self.unit_aliases = c
|
||||
caches['default'].touch(UNIT_CACHE_KEY, 30)
|
||||
else:
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').all():
|
||||
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all():
|
||||
self.unit_aliases[a.param_1] = a.param_2
|
||||
caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
|
||||
else:
|
||||
@@ -59,7 +59,7 @@ class IngredientParser:
|
||||
except KeyError:
|
||||
return food
|
||||
else:
|
||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1=food, disabled=False).first():
|
||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1=food, disabled=False).order_by('order').first():
|
||||
return automation.param_2
|
||||
return food
|
||||
|
||||
@@ -78,7 +78,7 @@ class IngredientParser:
|
||||
except KeyError:
|
||||
return unit
|
||||
else:
|
||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1=unit, disabled=False).first():
|
||||
if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1=unit, disabled=False).order_by('order').first():
|
||||
return automation.param_2
|
||||
return unit
|
||||
|
||||
@@ -126,6 +126,8 @@ class IngredientParser:
|
||||
amount = 0
|
||||
unit = None
|
||||
note = ''
|
||||
if x.strip() == '':
|
||||
return amount, unit, note
|
||||
|
||||
did_check_frac = False
|
||||
end = 0
|
||||
@@ -235,6 +237,14 @@ class IngredientParser:
|
||||
# leading spaces before commas result in extra tokens, clean them out
|
||||
ingredient = ingredient.replace(' ,', ',')
|
||||
|
||||
# handle "(from) - (to)" amounts by using the minimum amount and adding the range to the description
|
||||
# "10.5 - 200 g XYZ" => "100 g XYZ (10.5 - 200)"
|
||||
ingredient = re.sub("^(\d+|\d+[\\.,]\d+) - (\d+|\d+[\\.,]\d+) (.*)", "\\1 \\3 (\\1 - \\2)", ingredient)
|
||||
|
||||
# if amount and unit are connected add space in between
|
||||
if re.match('([0-9])+([A-z])+\s', ingredient):
|
||||
ingredient = re.sub(r'(?<=([a-z])|\d)(?=(?(1)\d|[a-z]))', ' ', ingredient)
|
||||
|
||||
tokens = ingredient.split() # split at each space into tokens
|
||||
if len(tokens) == 1:
|
||||
# there only is one argument, that must be the food
|
||||
|
||||
@@ -35,6 +35,7 @@ Negative examples:
|
||||
u'<p>del.icio.us</p>'
|
||||
|
||||
"""
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
import markdown
|
||||
|
||||
@@ -64,7 +65,7 @@ class UrlizePattern(markdown.inlinepatterns.Pattern):
|
||||
else:
|
||||
url = 'http://' + url
|
||||
|
||||
el = markdown.util.etree.Element("a")
|
||||
el = Element("a")
|
||||
el.set('href', url)
|
||||
el.text = markdown.util.AtomicString(text)
|
||||
return el
|
||||
|
||||
@@ -123,7 +123,7 @@ def share_link_valid(recipe, share):
|
||||
return c
|
||||
|
||||
if link := ShareLink.objects.filter(recipe=recipe, uuid=share, abuse_blocked=False).first():
|
||||
if 0 < settings.SHARING_LIMIT < link.request_count:
|
||||
if 0 < settings.SHARING_LIMIT < link.request_count and not link.space.no_sharing_limit:
|
||||
return False
|
||||
link.request_count += 1
|
||||
link.save()
|
||||
|
||||
@@ -12,7 +12,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 Keyword
|
||||
from cookbook.models import Keyword, Automation
|
||||
|
||||
|
||||
# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
|
||||
|
||||
@@ -121,7 +122,13 @@ def get_from_scraper(scrape, request):
|
||||
try:
|
||||
keywords.append(source_url.replace('http://', '').replace('https://', '').split('/')[0])
|
||||
except Exception:
|
||||
pass
|
||||
recipe_json['source_url'] = ''
|
||||
|
||||
try:
|
||||
if scrape.author():
|
||||
keywords.append(scrape.author())
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space)
|
||||
@@ -139,42 +146,58 @@ def get_from_scraper(scrape, request):
|
||||
if len(recipe_json['steps']) == 0:
|
||||
recipe_json['steps'].append({'instruction': '', 'ingredients': [], })
|
||||
|
||||
if len(parse_description(description)) > 256: # split at 256 as long descriptions dont look good on recipe cards
|
||||
recipe_json['steps'][0]['instruction'] = f'*{parse_description(description)}* \n\n' + recipe_json['steps'][0]['instruction']
|
||||
parsed_description = parse_description(description)
|
||||
# TODO notify user about limit if reached
|
||||
# limits exist to limit the attack surface for dos style attacks
|
||||
automations = Automation.objects.filter(type=Automation.DESCRIPTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').all().order_by('order')[:512]
|
||||
for a in automations:
|
||||
if re.match(a.param_1, (recipe_json['source_url'])[:512]):
|
||||
parsed_description = re.sub(a.param_2, a.param_3, parsed_description, count=1)
|
||||
|
||||
if len(parsed_description) > 256: # split at 256 as long descriptions don't look good on recipe cards
|
||||
recipe_json['steps'][0]['instruction'] = f'*{parsed_description}* \n\n' + recipe_json['steps'][0]['instruction']
|
||||
else:
|
||||
recipe_json['description'] = parse_description(description)[:512]
|
||||
recipe_json['description'] = parsed_description[:512]
|
||||
|
||||
try:
|
||||
for x in scrape.ingredients():
|
||||
try:
|
||||
amount, unit, ingredient, note = ingredient_parser.parse(x)
|
||||
ingredient = {
|
||||
'amount': amount,
|
||||
'food': {
|
||||
'name': ingredient,
|
||||
},
|
||||
'unit': None,
|
||||
'note': note,
|
||||
'original_text': x
|
||||
}
|
||||
if unit:
|
||||
ingredient['unit'] = {'name': unit, }
|
||||
recipe_json['steps'][0]['ingredients'].append(ingredient)
|
||||
except Exception:
|
||||
recipe_json['steps'][0]['ingredients'].append(
|
||||
{
|
||||
'amount': 0,
|
||||
'unit': None,
|
||||
if x.strip() != '':
|
||||
try:
|
||||
amount, unit, ingredient, note = ingredient_parser.parse(x)
|
||||
ingredient = {
|
||||
'amount': amount,
|
||||
'food': {
|
||||
'name': x,
|
||||
'name': ingredient,
|
||||
},
|
||||
'note': '',
|
||||
'unit': None,
|
||||
'note': note,
|
||||
'original_text': x
|
||||
}
|
||||
)
|
||||
if unit:
|
||||
ingredient['unit'] = {'name': unit, }
|
||||
recipe_json['steps'][0]['ingredients'].append(ingredient)
|
||||
except Exception:
|
||||
recipe_json['steps'][0]['ingredients'].append(
|
||||
{
|
||||
'amount': 0,
|
||||
'unit': None,
|
||||
'food': {
|
||||
'name': x,
|
||||
},
|
||||
'note': '',
|
||||
'original_text': x
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if recipe_json['source_url']:
|
||||
automations = Automation.objects.filter(type=Automation.INSTRUCTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').order_by('order').all()[:512]
|
||||
for a in automations:
|
||||
if re.match(a.param_1, (recipe_json['source_url'])[:512]):
|
||||
for s in recipe_json['steps']:
|
||||
s['instruction'] = re.sub(a.param_2, a.param_3, s['instruction'])
|
||||
|
||||
return recipe_json
|
||||
|
||||
|
||||
@@ -299,6 +322,11 @@ def parse_servings_text(servings):
|
||||
servings = re.sub("\d+", '', servings).strip()
|
||||
except Exception:
|
||||
servings = ''
|
||||
if type(servings) == list:
|
||||
try:
|
||||
servings = parse_servings_text(servings[1])
|
||||
except Exception:
|
||||
pass
|
||||
return str(servings)[:32]
|
||||
|
||||
|
||||
|
||||
@@ -22,10 +22,25 @@ class IngredientObject(object):
|
||||
else:
|
||||
self.amount = f"<scalable-number v-bind:number='{bleach.clean(str(ingredient.amount))}' v-bind:factor='ingredient_factor'></scalable-number>"
|
||||
if ingredient.unit:
|
||||
self.unit = bleach.clean(str(ingredient.unit))
|
||||
if ingredient.unit.plural_name in (None, ""):
|
||||
self.unit = bleach.clean(str(ingredient.unit))
|
||||
else:
|
||||
if ingredient.always_use_plural_unit or ingredient.amount > 1 and not ingredient.no_amount:
|
||||
self.unit = bleach.clean(ingredient.unit.plural_name)
|
||||
else:
|
||||
self.unit = bleach.clean(str(ingredient.unit))
|
||||
else:
|
||||
self.unit = ""
|
||||
self.food = bleach.clean(str(ingredient.food))
|
||||
if ingredient.food:
|
||||
if ingredient.food.plural_name in (None, ""):
|
||||
self.food = bleach.clean(str(ingredient.food))
|
||||
else:
|
||||
if ingredient.always_use_plural_food or ingredient.amount > 1 and not ingredient.no_amount:
|
||||
self.food = bleach.clean(str(ingredient.food.plural_name))
|
||||
else:
|
||||
self.food = bleach.clean(str(ingredient.food))
|
||||
else:
|
||||
self.food = ""
|
||||
self.note = bleach.clean(str(ingredient.note))
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import time
|
||||
import traceback
|
||||
import datetime
|
||||
import json
|
||||
import traceback
|
||||
import uuid
|
||||
from io import BytesIO, StringIO
|
||||
from io import BytesIO
|
||||
from zipfile import BadZipFile, ZipFile
|
||||
|
||||
import lxml
|
||||
from django.core.cache import cache
|
||||
import datetime
|
||||
|
||||
from bs4 import Tag
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.files import File
|
||||
from django.db import IntegrityError
|
||||
@@ -20,8 +16,7 @@ from django.utils.translation import gettext as _
|
||||
from django_scopes import scope
|
||||
from lxml import etree
|
||||
|
||||
from cookbook.forms import ImportExportBase
|
||||
from cookbook.helper.image_processing import get_filetype, handle_image
|
||||
from cookbook.helper.image_processing import handle_image
|
||||
from cookbook.models import Keyword, Recipe
|
||||
from recipes.settings import DEBUG
|
||||
from recipes.settings import EXPORT_FILE_CACHE_DURATION
|
||||
@@ -182,7 +177,7 @@ class Integration:
|
||||
traceback.print_exc()
|
||||
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
|
||||
import_zip.close()
|
||||
elif '.json' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name'] or '.rk' in f['name'] or '.melarecipe' in f['name']:
|
||||
elif '.json' in f['name'] or '.xml' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name'] or '.rk' in f['name'] or '.melarecipe' in f['name']:
|
||||
data_list = self.split_recipe_file(f['file'])
|
||||
il.total_recipes += len(data_list)
|
||||
for d in data_list:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import json
|
||||
import re
|
||||
from io import BytesIO
|
||||
from io import BytesIO, StringIO
|
||||
from zipfile import ZipFile
|
||||
from PIL import Image
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
@@ -96,5 +97,92 @@ class NextcloudCookbook(Integration):
|
||||
|
||||
return recipe
|
||||
|
||||
def formatTime(self, min):
|
||||
h = min//60
|
||||
m = min % 60
|
||||
return f'PT{h}H{m}M0S'
|
||||
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
raise NotImplementedError('Method not implemented in storage integration')
|
||||
|
||||
export = {}
|
||||
export['name'] = recipe.name
|
||||
export['description'] = recipe.description
|
||||
export['url'] = recipe.source_url
|
||||
export['prepTime'] = self.formatTime(recipe.working_time)
|
||||
export['cookTime'] = self.formatTime(recipe.waiting_time)
|
||||
export['totalTime'] = self.formatTime(recipe.working_time+recipe.waiting_time)
|
||||
export['recipeYield'] = recipe.servings
|
||||
export['image'] = f'/Recipes/{recipe.name}/full.jpg'
|
||||
export['imageUrl'] = f'/Recipes/{recipe.name}/full.jpg'
|
||||
|
||||
recipeKeyword = []
|
||||
for k in recipe.keywords.all():
|
||||
recipeKeyword.append(k.name)
|
||||
|
||||
export['keywords'] = recipeKeyword
|
||||
|
||||
recipeInstructions = []
|
||||
recipeIngredient = []
|
||||
for s in recipe.steps.all():
|
||||
recipeInstructions.append(s.instruction)
|
||||
|
||||
for i in s.ingredients.all():
|
||||
recipeIngredient.append(f'{float(i.amount)} {i.unit} {i.food}')
|
||||
|
||||
export['recipeIngredient'] = recipeIngredient
|
||||
export['recipeInstructions'] = recipeInstructions
|
||||
|
||||
|
||||
return "recipe.json", json.dumps(export)
|
||||
|
||||
def get_files_from_recipes(self, recipes, el, cookie):
|
||||
export_zip_stream = BytesIO()
|
||||
export_zip_obj = ZipFile(export_zip_stream, 'w')
|
||||
|
||||
for recipe in recipes:
|
||||
if recipe.internal and recipe.space == self.request.space:
|
||||
|
||||
recipe_stream = StringIO()
|
||||
filename, data = self.get_file_from_recipe(recipe)
|
||||
recipe_stream.write(data)
|
||||
export_zip_obj.writestr(f'{recipe.name}/{filename}', recipe_stream.getvalue())
|
||||
recipe_stream.close()
|
||||
|
||||
try:
|
||||
imageByte = recipe.image.file.read()
|
||||
export_zip_obj.writestr(f'{recipe.name}/full.jpg', self.getJPEG(imageByte))
|
||||
export_zip_obj.writestr(f'{recipe.name}/thumb.jpg', self.getThumb(171, imageByte))
|
||||
export_zip_obj.writestr(f'{recipe.name}/thumb16.jpg', self.getThumb(16, imageByte))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
el.exported_recipes += 1
|
||||
el.msg += self.get_recipe_processed_msg(recipe)
|
||||
el.save()
|
||||
|
||||
export_zip_obj.close()
|
||||
|
||||
return [[ self.get_export_file_name(), export_zip_stream.getvalue() ]]
|
||||
|
||||
def getJPEG(self, imageByte):
|
||||
image = Image.open(BytesIO(imageByte))
|
||||
image = image.convert('RGB')
|
||||
|
||||
bytes = BytesIO()
|
||||
image.save(bytes, "JPEG")
|
||||
return bytes.getvalue()
|
||||
|
||||
def getThumb(self, size, imageByte):
|
||||
image = Image.open(BytesIO(imageByte))
|
||||
|
||||
w, h = image.size
|
||||
m = min(w, h)
|
||||
|
||||
image = image.crop(((w-m)//2, (h-m)//2, (w+m)//2, (h+m)//2))
|
||||
image = image.resize([size, size], Image.Resampling.LANCZOS)
|
||||
image = image.convert('RGB')
|
||||
|
||||
bytes = BytesIO()
|
||||
image.save(bytes, "JPEG")
|
||||
return bytes.getvalue()
|
||||
|
||||
@@ -61,7 +61,7 @@ class RecetteTek(Integration):
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
for ingredient in file['ingredients'].split('\n'):
|
||||
if len(ingredient.strip()) > 0:
|
||||
amount, unit, food, note = ingredient_parser.parse(food)
|
||||
amount, unit, food, note = ingredient_parser.parse(ingredient.strip())
|
||||
f = ingredient_parser.get_food(ingredient)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
|
||||
@@ -58,6 +58,13 @@ class RecipeKeeper(Integration):
|
||||
if s.text == "":
|
||||
continue
|
||||
step.instruction += s.text + ' \n'
|
||||
step.save()
|
||||
|
||||
for s in file.find("div", {"itemprop": "recipeNotes"}).find_all("p"):
|
||||
if s.text == "":
|
||||
continue
|
||||
step.instruction += s.text + ' \n'
|
||||
step.save()
|
||||
|
||||
if file.find("span", {"itemprop": "recipeSource"}).text != '':
|
||||
step.instruction += "\n\n" + _("Imported from") + ": " + file.find("span", {"itemprop": "recipeSource"}).text
|
||||
|
||||
72
cookbook/integration/rezeptsuitede.py
Normal file
72
cookbook/integration/rezeptsuitede.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from xml import etree
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import parse_time, parse_servings, parse_servings_text
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Recipe, Step, Keyword
|
||||
|
||||
|
||||
class Rezeptsuitede(Integration):
|
||||
|
||||
def split_recipe_file(self, file):
|
||||
return etree.parse(file).getroot().getchildren()
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
recipe_xml = file
|
||||
|
||||
recipe = Recipe.objects.create(
|
||||
name=recipe_xml.find('head').attrib['title'].strip(),
|
||||
created_by=self.request.user, internal=True, space=self.request.space)
|
||||
|
||||
if recipe_xml.find('head').attrib['servingtype']:
|
||||
recipe.servings = parse_servings(recipe_xml.find('head').attrib['servingtype'].strip())
|
||||
recipe.servings_text = parse_servings_text(recipe_xml.find('head').attrib['servingtype'].strip())
|
||||
|
||||
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:
|
||||
recipe.description = recipe_xml.find('remark').find('line').text[:512]
|
||||
|
||||
for prep in recipe_xml.findall('preparation'):
|
||||
try:
|
||||
if prep.find('step').text:
|
||||
step = Step.objects.create(
|
||||
instruction=prep.find('step').text.strip(), space=self.request.space,
|
||||
)
|
||||
recipe.steps.add(step)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
|
||||
if recipe_xml.find('part').find('ingredient') is not None:
|
||||
ingredient_step = recipe.steps.first()
|
||||
if ingredient_step is None:
|
||||
ingredient_step = Step.objects.create(space=self.request.space, instruction='')
|
||||
|
||||
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, 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, ))
|
||||
|
||||
try:
|
||||
k, created = Keyword.objects.get_or_create(name=recipe_xml.find('head').find('cat').text.strip(), space=self.request.space)
|
||||
recipe.keywords.add(k)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
recipe.save()
|
||||
|
||||
try:
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_xml.find('head').find('picbin').text)), filetype='.jpeg')
|
||||
except:
|
||||
pass
|
||||
|
||||
return recipe
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
raise NotImplementedError('Method not implemented in storage integration')
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -6,20 +6,22 @@
|
||||
# Translators:
|
||||
# Pavel Solař <pavelsolar86@gmail.com>, 2021
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-09 18:01+0100\n"
|
||||
"PO-Revision-Date: 2020-06-02 19:28+0000\n"
|
||||
"Last-Translator: Pavel Solař <pavelsolar86@gmail.com>, 2021\n"
|
||||
"Language-Team: Czech (https://www.transifex.com/django-recipes/teams/110507/cs/)\n"
|
||||
"PO-Revision-Date: 2023-01-08 17:55+0000\n"
|
||||
"Last-Translator: Joachim Weber <joachim.weber@gmx.de>\n"
|
||||
"Language-Team: Czech <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/cs/>\n"
|
||||
"Language: cs\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: cs\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n "
|
||||
"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
|
||||
"X-Generator: Weblate 4.15\n"
|
||||
|
||||
#: .\cookbook\filters.py:22 .\cookbook\templates\base.html:87
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:219
|
||||
@@ -173,7 +175,7 @@ msgstr "Potravina, která by měla být nahrazena."
|
||||
|
||||
#: .\cookbook\forms.py:198
|
||||
msgid "Add your comment: "
|
||||
msgstr "Přidat vlastní komentář:"
|
||||
msgstr "Přidat vlastní komentář: "
|
||||
|
||||
#: .\cookbook\forms.py:229
|
||||
msgid "Leave empty for dropbox and enter app password for nextcloud."
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
cookbook/locale/el/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/el/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2610
cookbook/locale/el/LC_MESSAGES/django.po
Normal file
2610
cookbook/locale/el/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -11,8 +11,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-02-09 18:01+0100\n"
|
||||
"PO-Revision-Date: 2021-10-13 12:50+0000\n"
|
||||
"Last-Translator: Hrachya Kocharyan <hkocharyan@ctemplar.com>\n"
|
||||
"PO-Revision-Date: 2023-01-08 17:55+0000\n"
|
||||
"Last-Translator: Joachim Weber <joachim.weber@gmx.de>\n"
|
||||
"Language-Team: Armenian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/hy/>\n"
|
||||
"Language: hy\n"
|
||||
@@ -20,7 +20,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.8\n"
|
||||
"X-Generator: Weblate 4.15\n"
|
||||
|
||||
#: .\cookbook\filters.py:22 .\cookbook\templates\base.html:87
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:219
|
||||
@@ -410,7 +410,7 @@ msgstr "Դուրս գալ"
|
||||
|
||||
#: .\cookbook\templates\account\logout.html:11
|
||||
msgid "Are you sure you want to sign out?"
|
||||
msgstr "Համոզվա՞ծ եք, որ ցանկանում եք դուրս գալ:"
|
||||
msgstr "Համոզվա՞ծ եք, որ ցանկանում եք դուրս գալ՞"
|
||||
|
||||
#: .\cookbook\templates\account\password_reset.html:5
|
||||
#: .\cookbook\templates\account\password_reset_done.html:5
|
||||
|
||||
BIN
cookbook/locale/id/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/id/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
@@ -8,7 +8,7 @@ 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-01 16:38+0000\n"
|
||||
"PO-Revision-Date: 2022-10-12 08:33+0000\n"
|
||||
"Last-Translator: wella <wella.design@gmail.com>\n"
|
||||
"Language-Team: Indonesian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/id/>\n"
|
||||
@@ -155,154 +155,180 @@ msgstr "Kecualikan bahan-bahan yang ada."
|
||||
|
||||
#: .\cookbook\forms.py:90
|
||||
msgid "Will optimize the UI for use with your left hand."
|
||||
msgstr ""
|
||||
msgstr "Akan mengoptimalkan UI untuk digunakan dengan tangan kiri Anda."
|
||||
|
||||
#: .\cookbook\forms.py:107
|
||||
msgid ""
|
||||
"Both fields are optional. If none are given the username will be displayed "
|
||||
"instead"
|
||||
msgstr ""
|
||||
"Kedua bidang ini opsional. Jika tidak ada yang diberikan nama pengguna akan "
|
||||
"ditampilkan sebagai gantinya"
|
||||
|
||||
#: .\cookbook\forms.py:128 .\cookbook\forms.py:301
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
msgstr "Nama"
|
||||
|
||||
#: .\cookbook\forms.py:129 .\cookbook\forms.py:302
|
||||
#: .\cookbook\templates\stats.html:24 .\cookbook\views\lists.py:88
|
||||
msgid "Keywords"
|
||||
msgstr ""
|
||||
msgstr "Kata Kunci"
|
||||
|
||||
#: .\cookbook\forms.py:130
|
||||
msgid "Preparation time in minutes"
|
||||
msgstr ""
|
||||
msgstr "Waktu persiapan dalam hitungan menit"
|
||||
|
||||
#: .\cookbook\forms.py:131
|
||||
msgid "Waiting time (cooking/baking) in minutes"
|
||||
msgstr ""
|
||||
msgstr "Waktu tunggu (memasak/memanggang) dalam hitungan menit"
|
||||
|
||||
#: .\cookbook\forms.py:132 .\cookbook\forms.py:270 .\cookbook\forms.py:303
|
||||
msgid "Path"
|
||||
msgstr ""
|
||||
msgstr "Jalur"
|
||||
|
||||
#: .\cookbook\forms.py:133
|
||||
msgid "Storage UID"
|
||||
msgstr ""
|
||||
msgstr "UID penyimpanan"
|
||||
|
||||
#: .\cookbook\forms.py:165
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
msgstr "Bawaan"
|
||||
|
||||
#: .\cookbook\forms.py:177
|
||||
msgid ""
|
||||
"To prevent duplicates recipes with the same name as existing ones are "
|
||||
"ignored. Check this box to import everything."
|
||||
msgstr ""
|
||||
"Untuk mencegah duplikat resep dengan nama yang sama dengan yang sudah ada "
|
||||
"diabaikan. Centang kotak ini untuk mengimpor semuanya."
|
||||
|
||||
#: .\cookbook\forms.py:200
|
||||
msgid "Add your comment: "
|
||||
msgstr ""
|
||||
msgstr "Tambahkan komentar Anda: "
|
||||
|
||||
#: .\cookbook\forms.py:215
|
||||
msgid "Leave empty for dropbox and enter app password for nextcloud."
|
||||
msgstr ""
|
||||
"Biarkan kosong untuk dropbox dan masukkan kata sandi aplikasi untuk "
|
||||
"nextcloud."
|
||||
|
||||
#: .\cookbook\forms.py:222
|
||||
msgid "Leave empty for nextcloud and enter api token for dropbox."
|
||||
msgstr ""
|
||||
msgstr "Biarkan kosong untuk nextcloud dan masukkan token api untuk dropbox."
|
||||
|
||||
#: .\cookbook\forms.py:231
|
||||
msgid ""
|
||||
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
|
||||
"php/webdav/</code> is added automatically)"
|
||||
msgstr ""
|
||||
"Biarkan kosong untuk dropbox dan masukkan hanya url dasar untuk cloud "
|
||||
"berikutnya (<code>/remote.php/webdav/</code> ditambahkan secara otomatis)"
|
||||
|
||||
#: .\cookbook\forms.py:269 .\cookbook\views\edit.py:157
|
||||
msgid "Storage"
|
||||
msgstr ""
|
||||
msgstr "Penyimpanan"
|
||||
|
||||
#: .\cookbook\forms.py:271
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
msgstr "Aktif"
|
||||
|
||||
#: .\cookbook\forms.py:277
|
||||
msgid "Search String"
|
||||
msgstr ""
|
||||
msgstr "Cari String"
|
||||
|
||||
#: .\cookbook\forms.py:304
|
||||
msgid "File ID"
|
||||
msgstr ""
|
||||
msgstr "ID Berkas"
|
||||
|
||||
#: .\cookbook\forms.py:326
|
||||
msgid "You must provide at least a recipe or a title."
|
||||
msgstr ""
|
||||
msgstr "Anda harus memberikan setidaknya resep atau judul."
|
||||
|
||||
#: .\cookbook\forms.py:339
|
||||
msgid "You can list default users to share recipes with in the settings."
|
||||
msgstr ""
|
||||
"Anda dapat membuat daftar pengguna default untuk berbagi resep di pengaturan."
|
||||
|
||||
#: .\cookbook\forms.py:340
|
||||
msgid ""
|
||||
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
|
||||
"\">docs here</a>"
|
||||
msgstr ""
|
||||
"Anda dapat menggunakan penurunan harga untuk memformat bidang ini. Lihat <a "
|
||||
"href=\"/docs/markdown/\">dokumen di sini</a>"
|
||||
|
||||
#: .\cookbook\forms.py:366
|
||||
msgid "Maximum number of users for this space reached."
|
||||
msgstr ""
|
||||
msgstr "Jumlah maksimum pengguna untuk ruang ini tercapai."
|
||||
|
||||
#: .\cookbook\forms.py:372
|
||||
msgid "Email address already taken!"
|
||||
msgstr ""
|
||||
msgstr "Alamat email sudah terpakai!"
|
||||
|
||||
#: .\cookbook\forms.py:380
|
||||
msgid ""
|
||||
"An email address is not required but if present the invite link will be sent "
|
||||
"to the user."
|
||||
msgstr ""
|
||||
"Alamat email tidak diperlukan tetapi jika ada, tautan undangan akan dikirim "
|
||||
"ke pengguna."
|
||||
|
||||
#: .\cookbook\forms.py:395
|
||||
msgid "Name already taken."
|
||||
msgstr ""
|
||||
msgstr "Nama sudah terpakai."
|
||||
|
||||
#: .\cookbook\forms.py:406
|
||||
msgid "Accept Terms and Privacy"
|
||||
msgstr ""
|
||||
msgstr "Terima Persyaratan dan Privasi"
|
||||
|
||||
#: .\cookbook\forms.py:438
|
||||
msgid ""
|
||||
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
|
||||
"g. low values mean more typos are ignored)."
|
||||
msgstr ""
|
||||
"Menentukan seberapa kabur pencarian jika menggunakan pencocokan kesamaan "
|
||||
"trigram (misalnya nilai rendah berarti lebih banyak kesalahan ketik yang "
|
||||
"diabaikan)."
|
||||
|
||||
#: .\cookbook\forms.py:448
|
||||
msgid ""
|
||||
"Select type method of search. Click <a href=\"/docs/search/\">here</a> for "
|
||||
"full description of choices."
|
||||
msgstr ""
|
||||
"Pilih jenis metode pencarian. Klik <a href=\"/docs/search/\">di sini</a> "
|
||||
"untuk deskripsi lengkap pilihan."
|
||||
|
||||
#: .\cookbook\forms.py:449
|
||||
msgid ""
|
||||
"Use fuzzy matching on units, keywords and ingredients when editing and "
|
||||
"importing recipes."
|
||||
msgstr ""
|
||||
"Gunakan fuzzy pencocokan pada unit, kata kunci, dan bahan saat mengedit dan "
|
||||
"mengimpor resep."
|
||||
|
||||
#: .\cookbook\forms.py:451
|
||||
msgid ""
|
||||
"Fields to search ignoring accents. Selecting this option can improve or "
|
||||
"degrade search quality depending on language"
|
||||
msgstr ""
|
||||
"Bidang untuk mencari mengabaikan aksen. Memilih opsi ini dapat meningkatkan "
|
||||
"atau menurunkan kualitas pencarian tergantung pada bahasa"
|
||||
|
||||
#: .\cookbook\forms.py:453
|
||||
msgid ""
|
||||
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
|
||||
"'pie' and 'piece' and 'soapie')"
|
||||
msgstr ""
|
||||
"Bidang untuk mencari kecocokan sebagian. (mis. mencari 'Pie' akan "
|
||||
"mengembalikan 'pie' dan 'piece' dan 'soapie')"
|
||||
|
||||
#: .\cookbook\forms.py:455
|
||||
msgid ""
|
||||
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
|
||||
"will return 'salad' and 'sandwich')"
|
||||
msgstr ""
|
||||
"Bidang untuk mencari awal kata yang cocok. (misalnya mencari 'sa' akan "
|
||||
"mengembalikan 'salad' dan 'sandwich')"
|
||||
|
||||
#: .\cookbook\forms.py:457
|
||||
msgid ""
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-09-13 22:40+0200\n"
|
||||
"PO-Revision-Date: 2022-04-07 19:32+0000\n"
|
||||
"Last-Translator: Artem Aksenov <artemmillerr@gmail.com>\n"
|
||||
"PO-Revision-Date: 2022-11-30 19:09+0000\n"
|
||||
"Last-Translator: Alex <kovsharoff@gmail.com>\n"
|
||||
"Language-Team: Russian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/ru/>\n"
|
||||
"Language: ru\n"
|
||||
@@ -18,7 +18,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 4.10.1\n"
|
||||
"X-Generator: Weblate 4.14.1\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
|
||||
#: .\cookbook\templates\forms\ingredients.html:34
|
||||
@@ -396,8 +396,9 @@ msgstr ""
|
||||
#: .\cookbook\templates\include\log_cooking.html:16
|
||||
#: .\cookbook\templates\url_import.html:224
|
||||
#: .\cookbook\templates\url_import.html:455
|
||||
#, fuzzy
|
||||
msgid "Servings"
|
||||
msgstr ""
|
||||
msgstr "Порции"
|
||||
|
||||
#: .\cookbook\integration\safron.py:25
|
||||
msgid "Waiting time"
|
||||
@@ -468,7 +469,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\models.py:198 .\cookbook\templates\base.html:90
|
||||
msgid "Books"
|
||||
msgstr ""
|
||||
msgstr "Книги"
|
||||
|
||||
#: .\cookbook\models.py:206
|
||||
msgid "Small"
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -8,15 +8,17 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-04-29 18:42+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"PO-Revision-Date: 2023-02-09 13:55+0000\n"
|
||||
"Last-Translator: vertilo <vertilo.dev@gmail.com>\n"
|
||||
"Language-Team: Ukrainian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/uk/>\n"
|
||||
"Language: uk\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 4.15\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
|
||||
#: .\cookbook\templates\space.html:49 .\cookbook\templates\stats.html:28
|
||||
@@ -2026,7 +2028,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\space.html:118
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
msgstr "користувач"
|
||||
|
||||
#: .\cookbook\templates\space.html:119
|
||||
msgid "guest"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,31 +0,0 @@
|
||||
# Generated by Django 4.0.7 on 2022-10-02 11:51
|
||||
|
||||
import cookbook.models
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0184_alter_userpreference_image'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CookingMachine',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.CharField(choices=[('HOMECONNECT_COOKIT', 'HomeConnect CookIt')], max_length=128)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('serial', models.CharField(blank=True, max_length=512, null=True)),
|
||||
('description', models.TextField(blank=True, default='')),
|
||||
('access_token', models.CharField(blank=True, max_length=4096, null=True)),
|
||||
('access_token_expiry', models.DateTimeField(blank=True, null=True)),
|
||||
('refresh_token', models.CharField(blank=True, max_length=4096, null=True)),
|
||||
('refresh_token_expiry', models.DateTimeField(blank=True, null=True)),
|
||||
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
|
||||
],
|
||||
bases=(models.Model, cookbook.models.PermissionModelMixin),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 4.0.8 on 2022-11-22 06:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0184_alter_userpreference_image'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='plural_name',
|
||||
field=models.CharField(blank=True, default=None, max_length=128, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ingredient',
|
||||
name='always_use_plural_food',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ingredient',
|
||||
name='always_use_plural_unit',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='use_plural',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unit',
|
||||
name='plural_name',
|
||||
field=models.CharField(blank=True, default=None, max_length=128, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.1.4 on 2023-01-03 21:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0185_food_plural_name_ingredient_always_use_plural_food_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='automation',
|
||||
name='order',
|
||||
field=models.IntegerField(default=1000),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='automation',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('FOOD_ALIAS', 'Food Alias'), ('UNIT_ALIAS', 'Unit Alias'), ('KEYWORD_ALIAS', 'Keyword Alias'), ('DESCRIPTION_REPLACE', 'Description Replace'), ('INSTRUCTION_REPLACE', 'Instruction Replace')], max_length=128),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0187_alter_space_use_plural.py
Normal file
18
cookbook/migrations/0187_alter_space_use_plural.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.4 on 2023-01-20 09:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0186_automation_order_alter_automation_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='space',
|
||||
name='use_plural',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0188_space_no_sharing_limit.py
Normal file
18
cookbook/migrations/0188_space_no_sharing_limit.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.4 on 2023-02-12 16:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0187_alter_space_use_plural'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='no_sharing_limit',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -260,7 +260,9 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
||||
max_recipes = models.IntegerField(default=0)
|
||||
max_file_storage_mb = models.IntegerField(default=0, help_text=_('Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.'))
|
||||
max_users = models.IntegerField(default=0)
|
||||
use_plural = models.BooleanField(default=True)
|
||||
allow_sharing = models.BooleanField(default=True)
|
||||
no_sharing_limit = models.BooleanField(default=False)
|
||||
demo = models.BooleanField(default=False)
|
||||
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
|
||||
show_facet_count = models.BooleanField(default=False)
|
||||
@@ -530,6 +532,7 @@ class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelM
|
||||
|
||||
class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixin):
|
||||
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)
|
||||
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
@@ -554,6 +557,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
if SORT_TREE_BY_NAME:
|
||||
node_order_by = ['name']
|
||||
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
|
||||
plural_name = models.CharField(max_length=128, null=True, blank=True, default=None)
|
||||
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
|
||||
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) # inherited field
|
||||
ignore_shopping = models.BooleanField(default=False) # inherited field
|
||||
@@ -654,6 +658,8 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
|
||||
note = models.CharField(max_length=256, null=True, blank=True)
|
||||
is_header = models.BooleanField(default=False)
|
||||
no_amount = models.BooleanField(default=False)
|
||||
always_use_plural_unit = models.BooleanField(default=False)
|
||||
always_use_plural_food = models.BooleanField(default=False)
|
||||
order = models.IntegerField(default=0)
|
||||
original_text = models.CharField(max_length=512, null=True, blank=True, default=None)
|
||||
|
||||
@@ -663,7 +669,23 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
def __str__(self):
|
||||
return str(self.amount) + ' ' + str(self.unit) + ' ' + str(self.food)
|
||||
food = ""
|
||||
unit = ""
|
||||
if self.always_use_plural_food and self.food.plural_name not in (None, "") and not self.no_amount:
|
||||
food = self.food.plural_name
|
||||
else:
|
||||
if self.amount > 1 and self.food.plural_name not in (None, "") and not self.no_amount:
|
||||
food = self.food.plural_name
|
||||
else:
|
||||
food = str(self.food)
|
||||
if self.always_use_plural_unit and self.unit.plural_name not in (None, "") and not self.no_amount:
|
||||
unit = self.unit.plural_name
|
||||
else:
|
||||
if self.amount > 1 and self.unit.plural_name not in (None, "") and not self.no_amount:
|
||||
unit = self.unit.plural_name
|
||||
else:
|
||||
unit = str(self.unit)
|
||||
return str(self.amount) + ' ' + str(unit) + ' ' + str(food)
|
||||
|
||||
class Meta:
|
||||
ordering = ['order', 'pk']
|
||||
@@ -1010,27 +1032,6 @@ def default_valid_until():
|
||||
return date.today() + timedelta(days=14)
|
||||
|
||||
|
||||
class CookingMachine(models.Model, PermissionModelMixin):
|
||||
# Tandoor is not affiliated in any way or form with the holders of Trademark or other right associated with the mentioned names. All mentioned protected names are purely used to identify to the user a certain device or integration.
|
||||
HOMECONNECT_COOKIT = 'HOMECONNECT_COOKIT'
|
||||
MACHINE_TYPES = (
|
||||
(HOMECONNECT_COOKIT, _('HomeConnect CookIt')),
|
||||
)
|
||||
|
||||
type = models.CharField(choices=MACHINE_TYPES, max_length=128)
|
||||
name = models.CharField(max_length=128)
|
||||
serial = models.CharField(max_length=512, null=True, blank=True)
|
||||
description = models.TextField(default='', blank=True)
|
||||
|
||||
access_token = models.CharField(max_length=4096, null=True, blank=True)
|
||||
access_token_expiry = models.DateTimeField(null=True, blank=True)
|
||||
refresh_token = models.CharField(max_length=4096, null=True, blank=True)
|
||||
refresh_token_expiry = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space')
|
||||
|
||||
|
||||
class InviteLink(ExportModelOperationsMixin('invite_link'), models.Model, PermissionModelMixin):
|
||||
uuid = models.UUIDField(default=uuid.uuid4)
|
||||
email = models.EmailField(blank=True)
|
||||
@@ -1223,9 +1224,12 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
|
||||
FOOD_ALIAS = 'FOOD_ALIAS'
|
||||
UNIT_ALIAS = 'UNIT_ALIAS'
|
||||
KEYWORD_ALIAS = 'KEYWORD_ALIAS'
|
||||
DESCRIPTION_REPLACE = 'DESCRIPTION_REPLACE'
|
||||
INSTRUCTION_REPLACE = 'INSTRUCTION_REPLACE'
|
||||
|
||||
type = models.CharField(max_length=128,
|
||||
choices=((FOOD_ALIAS, _('Food Alias')), (UNIT_ALIAS, _('Unit Alias')), (KEYWORD_ALIAS, _('Keyword Alias')),))
|
||||
choices=((FOOD_ALIAS, _('Food Alias')), (UNIT_ALIAS, _('Unit Alias')), (KEYWORD_ALIAS, _('Keyword Alias')),
|
||||
(DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')),))
|
||||
name = models.CharField(max_length=128, default='')
|
||||
description = models.TextField(blank=True, null=True)
|
||||
|
||||
@@ -1233,6 +1237,8 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
|
||||
param_2 = models.CharField(max_length=128, blank=True, null=True)
|
||||
param_3 = models.CharField(max_length=128, blank=True, null=True)
|
||||
|
||||
order = models.IntegerField(default=1000)
|
||||
|
||||
disabled = models.BooleanField(default=False)
|
||||
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -277,7 +277,8 @@ class SpaceSerializer(WritableNestedModelSerializer):
|
||||
class Meta:
|
||||
model = Space
|
||||
fields = ('id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
|
||||
'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb', 'image',)
|
||||
'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',)
|
||||
|
||||
|
||||
@@ -431,17 +432,26 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
|
||||
|
||||
def create(self, validated_data):
|
||||
name = validated_data.pop('name').strip()
|
||||
|
||||
if plural_name := validated_data.pop('plural_name', None):
|
||||
plural_name = plural_name.strip()
|
||||
|
||||
if unit := Unit.objects.filter(Q(name=name) | Q(plural_name=name)).first():
|
||||
return unit
|
||||
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
obj, created = Unit.objects.get_or_create(name=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):
|
||||
validated_data['name'] = validated_data['name'].strip()
|
||||
if plural_name := validated_data.get('plural_name', None):
|
||||
validated_data['plural_name'] = plural_name.strip()
|
||||
return super(UnitSerializer, self).update(instance, validated_data)
|
||||
|
||||
class Meta:
|
||||
model = Unit
|
||||
fields = ('id', 'name', 'description', 'numrecipe', 'image')
|
||||
fields = ('id', 'name', 'plural_name', 'description', 'numrecipe', 'image')
|
||||
read_only_fields = ('id', 'numrecipe', 'image')
|
||||
|
||||
|
||||
@@ -499,7 +509,7 @@ class RecipeSimpleSerializer(WritableNestedModelSerializer):
|
||||
class FoodSimpleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = ('id', 'name')
|
||||
fields = ('id', 'name', 'plural_name')
|
||||
|
||||
|
||||
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
@@ -538,6 +548,13 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
|
||||
def create(self, validated_data):
|
||||
name = validated_data.pop('name').strip()
|
||||
|
||||
if plural_name := validated_data.pop('plural_name', None):
|
||||
plural_name = plural_name.strip()
|
||||
|
||||
if food := Food.objects.filter(Q(name=name) | Q(plural_name=name)).first():
|
||||
return food
|
||||
|
||||
space = validated_data.pop('space', self.context['request'].space)
|
||||
# supermarket category needs to be handled manually as food.get or create does not create nested serializers unlike a super.create of serializer
|
||||
if 'supermarket_category' in validated_data and validated_data['supermarket_category']:
|
||||
@@ -562,12 +579,14 @@ 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, space=space, defaults=validated_data)
|
||||
obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space, defaults=validated_data)
|
||||
return obj
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if name := validated_data.get('name', None):
|
||||
validated_data['name'] = name.strip()
|
||||
if plural_name := validated_data.get('plural_name', None):
|
||||
validated_data['plural_name'] = plural_name.strip()
|
||||
# assuming if on hand for user also onhand for shopping_share users
|
||||
onhand = validated_data.get('food_onhand', None)
|
||||
reset_inherit = self.initial_data.get('reset_inherit', False)
|
||||
@@ -587,7 +606,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = (
|
||||
'id', 'name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category',
|
||||
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category',
|
||||
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
|
||||
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields'
|
||||
)
|
||||
@@ -616,6 +635,7 @@ class IngredientSimpleSerializer(WritableNestedModelSerializer):
|
||||
fields = (
|
||||
'id', 'food', 'unit', 'amount', 'note', 'order',
|
||||
'is_header', 'no_amount', 'original_text', 'used_in_recipes',
|
||||
'always_use_plural_unit', 'always_use_plural_food',
|
||||
)
|
||||
|
||||
|
||||
@@ -864,11 +884,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
value = value.quantize(
|
||||
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
||||
return (
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
) + f' ({value:.2g})'
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
) + f' ({value:.2g})'
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# TODO remove once old shopping list
|
||||
@@ -1055,7 +1075,7 @@ class AutomationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Automation
|
||||
fields = (
|
||||
'id', 'type', 'name', 'description', 'param_1', 'param_2', 'param_3', 'disabled', 'created_by',)
|
||||
'id', 'type', 'name', 'description', 'param_1', 'param_2', 'param_3', 'order', 'disabled', 'created_by',)
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
@@ -1162,7 +1182,7 @@ class SupermarketCategoryExportSerializer(SupermarketCategorySerializer):
|
||||
class UnitExportSerializer(UnitSerializer):
|
||||
class Meta:
|
||||
model = Unit
|
||||
fields = ('name', 'description')
|
||||
fields = ('name', 'plural_name', 'description')
|
||||
|
||||
|
||||
class FoodExportSerializer(FoodSerializer):
|
||||
@@ -1170,7 +1190,7 @@ class FoodExportSerializer(FoodSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = ('name', 'ignore_shopping', 'supermarket_category',)
|
||||
fields = ('name', 'plural_name', 'ignore_shopping', 'supermarket_category',)
|
||||
|
||||
|
||||
class IngredientExportSerializer(WritableNestedModelSerializer):
|
||||
@@ -1184,7 +1204,7 @@ class IngredientExportSerializer(WritableNestedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Ingredient
|
||||
fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount')
|
||||
fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount', 'always_use_plural_unit', 'always_use_plural_food')
|
||||
|
||||
|
||||
class StepExportSerializer(WritableNestedModelSerializer):
|
||||
|
||||
1
cookbook/static/css/bootstrap-vue.min.css
vendored
1
cookbook/static/css/bootstrap-vue.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
cookbook/static/themes/bootstrap.min.css
vendored
1
cookbook/static/themes/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
2
cookbook/static/themes/tandoor.min.css
vendored
2
cookbook/static/themes/tandoor.min.css
vendored
@@ -10438,8 +10438,6 @@ footer a:hover {
|
||||
padding: 5px 0 20px 39px
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=maps/style.min.css.map */
|
||||
|
||||
.bg-header {
|
||||
background-color: rgb(221, 191, 134) !important;
|
||||
}
|
||||
|
||||
@@ -35,9 +35,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if EMAIL_ENABLED %}
|
||||
<a class="btn btn-warning float-right d-none d-xl-block d-lg-block"
|
||||
href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a>
|
||||
<p class="d-xl-none d-lg-none">{% trans 'Lost your password?' %} <a
|
||||
<p>{% trans 'Lost your password?' %} <a
|
||||
href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a></p>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<link rel="mask-icon" href="{% static 'assets/safari-pinned-tab.svg' %}" color="#161616">
|
||||
<link rel="apple-touch-icon" href="{% static 'assets/apple-touch-icon.png' %}" sizes="180x180">
|
||||
|
||||
<link rel="manifest" href="{% url 'web_manifest' %}">
|
||||
<link rel="manifest" crossorigin="use-credentials" href="{% url 'web_manifest' %}">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
$.fn.select2.defaults.set("theme", "bootstrap");
|
||||
{% if request.user.is_authenticated %}
|
||||
window.ACTIVE_SPACE_ID = '{{request.space.id}}';
|
||||
{% endif %}
|
||||
</script>
|
||||
|
||||
<!-- Fontawesome icons -->
|
||||
@@ -347,8 +350,8 @@
|
||||
|
||||
{% message_of_the_day request as message_of_the_day %}
|
||||
{% if message_of_the_day %}
|
||||
<div class="bg-success" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
|
||||
{{ message_of_the_day }}
|
||||
<div class="bg-info" style=" width: 100%; text-align: center!important; color: #ffffff; padding: 8px">
|
||||
{{ message_of_the_day | markdown |safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -409,6 +412,7 @@
|
||||
localStorage.setItem('STATIC_URL', "{% base_path request 'static_base' %}")
|
||||
localStorage.setItem('DEBUG', "{% is_debug %}")
|
||||
localStorage.setItem('USER_ID', "{{request.user.pk}}")
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("{% url 'service_worker' %}", {scope: "{% base_path request 'base' %}" + '/'}).then(function (reg) {
|
||||
|
||||
@@ -7,6 +7,21 @@
|
||||
|
||||
{% block title %}{{ recipe.name }}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<meta property="og:title" content="{{ recipe.name }}"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:url" content="{% base_path request 'base' %}{% url 'view_recipe' recipe.pk share %}"/>
|
||||
{% if recipe.image %}
|
||||
<meta property="og:image" content="{% base_path request 'base' %}{{ recipe.image.url }}"/>
|
||||
<meta property="og:image:url" content="{% base_path request 'base' %}{{ recipe.image.url }}"/>
|
||||
<meta property="og:image:secure" content="{% base_path request 'base' %}{{ recipe.image.url }}"/>
|
||||
{% endif %}
|
||||
{% if recipe.description %}
|
||||
<meta property="og:description" content="{{ recipe.description }}"/>
|
||||
{% endif %}
|
||||
<meta property="og:site_name" content="Tandoor Recipes"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% recipe_rating recipe request.user as rating %}
|
||||
|
||||
|
||||
@@ -10,15 +10,22 @@
|
||||
|
||||
{% block content_fluid %}
|
||||
|
||||
<a href="{{ login_url }}" target="_blank" rel="noreferrer nofollow">Login CookIt</a>
|
||||
<br/>
|
||||
<a href="?sync=748" >Sync</a>
|
||||
|
||||
{{ data }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
{% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
{% else %}
|
||||
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||
{% endif %}
|
||||
|
||||
<script type="application/javascript">
|
||||
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||
</script>
|
||||
|
||||
{% render_bundle 'test_view' %}
|
||||
{% endblock %}
|
||||
@@ -57,6 +57,8 @@ def markdown(value):
|
||||
]
|
||||
)
|
||||
markdown_attrs['*'] = markdown_attrs['*'] + ['class']
|
||||
parsed_md = parsed_md[3:] # remove outer paragraph
|
||||
parsed_md = parsed_md[:len(parsed_md)-4]
|
||||
return bleach.clean(parsed_md, tags, markdown_attrs)
|
||||
|
||||
|
||||
@@ -101,6 +103,7 @@ def page_help(page_name):
|
||||
'view_shopping': 'https://docs.tandoor.dev/features/shopping/',
|
||||
'view_import': 'https://docs.tandoor.dev/features/import_export/',
|
||||
'view_export': 'https://docs.tandoor.dev/features/import_export/',
|
||||
'list_automation': 'https://docs.tandoor.dev/features/automation/',
|
||||
}
|
||||
|
||||
link = help_pages.get(page_name, '')
|
||||
|
||||
22
cookbook/tests/api/test_api_share_link.py
Normal file
22
cookbook/tests/api/test_api_share_link.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import json
|
||||
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.helper.permission_helper import share_link_valid
|
||||
from cookbook.models import Recipe
|
||||
|
||||
|
||||
def test_get_share_link(recipe_1_s1, u1_s1, u1_s2, g1_s1, a_u, space_1):
|
||||
assert u1_s1.get(reverse('api_share_link', args=[recipe_1_s1.pk])).status_code == 200
|
||||
assert u1_s2.get(reverse('api_share_link', args=[recipe_1_s1.pk])).status_code == 404
|
||||
assert g1_s1.get(reverse('api_share_link', args=[recipe_1_s1.pk])).status_code == 403
|
||||
assert a_u.get(reverse('api_share_link', args=[recipe_1_s1.pk])).status_code == 403
|
||||
|
||||
with scopes_disabled():
|
||||
sl = json.loads(u1_s1.get(reverse('api_share_link', args=[recipe_1_s1.pk])).content)
|
||||
assert share_link_valid(Recipe.objects.filter(pk=sl['pk']).get(), sl['share'])
|
||||
|
||||
space_1.allow_sharing = False
|
||||
space_1.save()
|
||||
assert u1_s1.get(reverse('api_share_link', args=[recipe_1_s1.pk])).status_code == 403
|
||||
@@ -97,7 +97,8 @@ class SupermarketCategoryFactory(factory.django.DjangoModelFactory):
|
||||
@register
|
||||
class FoodFactory(factory.django.DjangoModelFactory):
|
||||
"""Food factory."""
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=3, variable_nb_words=False))
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10)[:128])
|
||||
plural_name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=3, variable_nb_words=False))
|
||||
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
|
||||
supermarket_category = factory.Maybe(
|
||||
factory.LazyAttribute(lambda x: x.has_category),
|
||||
@@ -126,7 +127,7 @@ class FoodFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
class Meta:
|
||||
model = 'cookbook.Food'
|
||||
django_get_or_create = ('name', 'space',)
|
||||
django_get_or_create = ('name', 'plural_name', 'space',)
|
||||
|
||||
|
||||
@register
|
||||
@@ -159,13 +160,14 @@ class RecipeBookEntryFactory(factory.django.DjangoModelFactory):
|
||||
@register
|
||||
class UnitFactory(factory.django.DjangoModelFactory):
|
||||
"""Unit factory."""
|
||||
name = factory.LazyAttribute(lambda x: faker.word())
|
||||
name = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10)[:128])
|
||||
plural_name = factory.LazyAttribute(lambda x: faker.word())
|
||||
description = factory.LazyAttribute(lambda x: faker.sentence(nb_words=10))
|
||||
space = factory.SubFactory(SpaceFactory)
|
||||
|
||||
class Meta:
|
||||
model = 'cookbook.Unit'
|
||||
django_get_or_create = ('name', 'space',)
|
||||
django_get_or_create = ('name', 'plural_name', 'space',)
|
||||
|
||||
|
||||
@register
|
||||
|
||||
50
cookbook/tests/other/test_automations.py
Normal file
50
cookbook/tests/other/test_automations.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import pytest
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.forms import ImportExportBase
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.models import ExportLog, Automation
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.tests.conftest import validate_recipe
|
||||
|
||||
IMPORT_SOURCE_URL = 'api_recipe_from_source'
|
||||
|
||||
|
||||
# for some reason this tests cant run due to some kind of encoding issue, needs to be fixed
|
||||
# def test_description_replace_automation(u1_s1, space_1):
|
||||
# if 'cookbook' in os.getcwd():
|
||||
# test_file = os.path.join(os.getcwd(), 'other', 'test_data', 'chefkoch2.html')
|
||||
# else:
|
||||
# test_file = os.path.join(os.getcwd(), 'cookbook', 'tests', 'other', 'test_data', 'chefkoch2.html')
|
||||
#
|
||||
# # original description
|
||||
# # Brokkoli - Bratlinge. Über 91 Bewertungen und für vorzüglich befunden. Mit ► Portionsrechner ► Kochbuch ► Video-Tipps! Jetzt entdecken und ausprobieren!
|
||||
#
|
||||
# with scopes_disabled():
|
||||
# Automation.objects.create(
|
||||
# name='test1',
|
||||
# created_by=auth.get_user(u1_s1),
|
||||
# space=space_1,
|
||||
# param_1='.*',
|
||||
# param_2='.*',
|
||||
# param_3='',
|
||||
# order=1000,
|
||||
# )
|
||||
#
|
||||
# with open(test_file, 'r', encoding='UTF-8') as d:
|
||||
# response = u1_s1.post(
|
||||
# reverse(IMPORT_SOURCE_URL),
|
||||
# {
|
||||
# 'data': d.read(),
|
||||
# 'url': 'https://www.chefkoch.de/rezepte/804871184310070/Brokkoli-Bratlinge.html',
|
||||
# },
|
||||
# content_type='application/json')
|
||||
# recipe = json.loads(response.content)['recipe_json']
|
||||
# assert recipe['description'] == ''
|
||||
@@ -54,7 +54,7 @@ def test_ingredient_parser():
|
||||
"3,5 l Wasser": (3.5, "l", "Wasser", ""),
|
||||
"3.5 l Wasser": (3.5, "l", "Wasser", ""),
|
||||
"400 g Karotte(n)": (400, "g", "Karotte(n)", ""),
|
||||
"400g unsalted butter": (400, "g", "butter", "unsalted"),
|
||||
"400g unsalted butter": (400, "g", "unsalted butter", ""),
|
||||
"2L Wasser": (2, "L", "Wasser", ""),
|
||||
"1 (16 ounce) package dry lentils, rinsed": (1, "package", "dry lentils, rinsed", "16 ounce"),
|
||||
"2-3 c Water": (2, "c", "Water", "2-3"),
|
||||
@@ -75,6 +75,8 @@ def test_ingredient_parser():
|
||||
# an amount # and it starts with a lowercase letter, then that
|
||||
# is a unit ("etwas", "evtl.") does not apply to English tho
|
||||
|
||||
# TODO maybe add/improve support for weired stuff like this https://www.rainbownourishments.com/vegan-lemon-tart/#recipe
|
||||
|
||||
ingredient_parser = IngredientParser(None, False, ignore_automations=True)
|
||||
|
||||
count = 0
|
||||
|
||||
@@ -2,13 +2,16 @@ import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.tests.conftest import validate_recipe
|
||||
|
||||
from ._recipes import (ALLRECIPES, AMERICAS_TEST_KITCHEN, CHEF_KOCH, CHEF_KOCH2, COOKPAD,
|
||||
COOKS_COUNTRY, DELISH, FOOD_NETWORK, GIALLOZAFFERANO, JOURNAL_DES_FEMMES,
|
||||
MADAME_DESSERT, MARMITON, TASTE_OF_HOME, THE_SPRUCE_EATS, TUDOGOSTOSO)
|
||||
from ...models import Automation
|
||||
|
||||
IMPORT_SOURCE_URL = 'api_recipe_from_source'
|
||||
DATA_DIR = "cookbook/tests/other/test_data/"
|
||||
@@ -72,3 +75,5 @@ def test_recipe_import(arg, u1_s1):
|
||||
content_type='application/json')
|
||||
recipe = json.loads(response.content)['recipe_json']
|
||||
validate_recipe(arg, recipe)
|
||||
|
||||
|
||||
|
||||
@@ -120,8 +120,6 @@ urlpatterns = [
|
||||
path('api/download-file/<int:file_id>/', api.download_file, name='api_download_file'),
|
||||
|
||||
|
||||
path('cooking-machine/auth/homeconnect/', views.test2, name='cookingmachine_auth_homeconnect'),
|
||||
|
||||
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?
|
||||
|
||||
@@ -87,7 +87,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
|
||||
SupermarketCategorySerializer, SupermarketSerializer,
|
||||
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
||||
UserFileSerializer, UserSerializer, UserPreferenceSerializer,
|
||||
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer)
|
||||
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer)
|
||||
from cookbook.views.import_export import get_integration
|
||||
from recipes import settings
|
||||
|
||||
@@ -533,6 +533,11 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
||||
.prefetch_related('onhand_users', 'inherit_fields', 'child_inherit_fields', 'substitute') \
|
||||
.select_related('recipe', 'supermarket_category')
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request and self.request.query_params.get('simple', False):
|
||||
return FoodSimpleSerializer
|
||||
return self.serializer_class
|
||||
|
||||
@decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer, )
|
||||
# TODO DRF only allows one action in a decorator action without overriding get_operation_id_base() this should be PUT and DELETE probably
|
||||
def shopping(self, request, pk):
|
||||
@@ -655,7 +660,7 @@ class IngredientViewSet(viewsets.ModelViewSet):
|
||||
def get_serializer_class(self):
|
||||
if self.request and self.request.query_params.get('simple', False):
|
||||
return IngredientSimpleSerializer
|
||||
return IngredientSerializer
|
||||
return self.serializer_class
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset.filter(step__recipe__space=self.request.space)
|
||||
@@ -852,7 +857,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
|
||||
if image is not None:
|
||||
img = handle_image(request, image, filetype)
|
||||
obj.image = File(img, name=f'{uuid.uuid4()}_{obj.pk}{filetype}')
|
||||
obj.image.save(f'{uuid.uuid4()}_{obj.pk}{filetype}', img)
|
||||
obj.save()
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
@@ -1306,6 +1311,8 @@ 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)
|
||||
else:
|
||||
return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
def get_recipe_provider(recipe):
|
||||
@@ -1382,13 +1389,16 @@ def sync_all(request):
|
||||
|
||||
|
||||
def share_link(request, pk):
|
||||
if request.space.allow_sharing and has_group_permission(request.user, 'user'):
|
||||
recipe = get_object_or_404(Recipe, pk=pk, space=request.space)
|
||||
link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space)
|
||||
return JsonResponse({'pk': pk, 'share': link.uuid,
|
||||
'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
|
||||
else:
|
||||
return JsonResponse({'error': 'sharing_disabled'}, status=403)
|
||||
if request.user.is_authenticated:
|
||||
if request.space.allow_sharing and has_group_permission(request.user, ('user',)):
|
||||
recipe = get_object_or_404(Recipe, pk=pk, space=request.space)
|
||||
link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space)
|
||||
return JsonResponse({'pk': pk, 'share': link.uuid,
|
||||
'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
|
||||
else:
|
||||
return JsonResponse({'error': 'sharing_disabled'}, status=403)
|
||||
|
||||
return JsonResponse({'error': 'not_authenticated'}, status=403)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
|
||||
@@ -31,6 +31,7 @@ from cookbook.integration.plantoeat import Plantoeat
|
||||
from cookbook.integration.recettetek import RecetteTek
|
||||
from cookbook.integration.recipekeeper import RecipeKeeper
|
||||
from cookbook.integration.recipesage import RecipeSage
|
||||
from cookbook.integration.rezeptsuitede import Rezeptsuitede
|
||||
from cookbook.integration.rezkonv import RezKonv
|
||||
from cookbook.integration.saffron import Saffron
|
||||
from cookbook.models import ExportLog, ImportLog, Recipe, UserPreference
|
||||
@@ -80,6 +81,8 @@ def get_integration(request, export_type):
|
||||
return MelaRecipes(request, export_type)
|
||||
if export_type == ImportExportBase.COOKMATE:
|
||||
return Cookmate(request, export_type)
|
||||
if export_type == ImportExportBase.REZEPTSUITEDE:
|
||||
return Rezeptsuitede(request, export_type)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
|
||||
@@ -20,13 +20,12 @@ from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
from oauth2_provider.models import AccessToken
|
||||
|
||||
from cookbook.cooking_machines.homeconnect_cookit import HomeConnectCookit
|
||||
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm,
|
||||
SpaceCreateForm, SpaceJoinForm, User,
|
||||
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm)
|
||||
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, CookingMachine)
|
||||
Space, ViewLog, UserSpace)
|
||||
from cookbook.tables import (CookLogTable, ViewLogTable)
|
||||
from recipes.version import BUILD_REF, VERSION_NUMBER
|
||||
|
||||
@@ -435,22 +434,17 @@ def test(request):
|
||||
if not settings.DEBUG:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
machine = CookingMachine.objects.get_or_create(type=CookingMachine.HOMECONNECT_COOKIT, name='Test', space=request.space)[0]
|
||||
test = HomeConnectCookit(machine)
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
parser = IngredientParser(request, False)
|
||||
|
||||
return render(request, 'test.html', {'login_url': test.get_auth_link()})
|
||||
data = {
|
||||
'original': '90g golden syrup'
|
||||
}
|
||||
data['parsed'] = parser.parse(data['original'])
|
||||
|
||||
return render(request, 'test.html', {'data': data})
|
||||
|
||||
|
||||
def test2(request):
|
||||
if not settings.DEBUG:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
machine = CookingMachine.objects.get_or_create(type=CookingMachine.HOMECONNECT_COOKIT, name='Test', space=request.space)[0]
|
||||
test = HomeConnectCookit(machine)
|
||||
if 'code' in request.GET:
|
||||
test.get_access_token(request.GET['code'])
|
||||
|
||||
if 'sync' in request.GET:
|
||||
result = test.push_recipe(Recipe.objects.get(pk=request.GET['sync'], space=request.space))
|
||||
|
||||
return render(request, 'test.html', {'data': request})
|
||||
|
||||
@@ -41,7 +41,7 @@ Most new frontend pages are build using [Vue.js](https://vuejs.org/).
|
||||
|
||||
In order to work on these pages, you will have to install a Javascript package manager of your choice. The following examples use yarn.
|
||||
|
||||
Run `yarn install` to install the dependencies. After that you can use `yarn serve` to start the development server,
|
||||
In the `vue` folder run `yarn install` to install the dependencies. After that you can use `yarn serve` to start the development server,
|
||||
and proceed to test your changes. If you do not wish to work on those pages, but instead want the application to work properly during
|
||||
development, run `yarn build` to build the frontend pages once.
|
||||
|
||||
|
||||
13
docs/faq.md
13
docs/faq.md
@@ -48,6 +48,15 @@ The other common issue is that the recommended nginx container is removed from t
|
||||
If removed, the nginx webserver needs to be replaced by something else that servers the /mediafiles/ directory or
|
||||
`GUNICORN_MEDIA` needs to be enabled to allow media serving by the application container itself.
|
||||
|
||||
|
||||
## Why does the Text/Markdown preview look different than the final recipe?
|
||||
|
||||
Tandoor has always rendered the recipe instructions markdown on the server. This also allows tandoor to implement things like ingredient templating and scaling in text.
|
||||
To make editing easier a markdown editor was added to the frontend with integrated preview as a temporary solution. Since the markdown editor uses a different
|
||||
specification than the server the preview is different to the final result. It is planned to improve this in the future.
|
||||
|
||||
The markdown renderer follows this markdown specification https://daringfireball.net/projects/markdown/
|
||||
|
||||
## Why is Tandoor not working on my Raspberry Pi?
|
||||
|
||||
Please refer to [here](install/docker.md#setup-issues-on-raspberry-pi).
|
||||
@@ -57,7 +66,7 @@ To create a new user click on your name (top right corner) and select 'space set
|
||||
|
||||
It is not possible to create users through the admin because users must be assigned a default group and space.
|
||||
|
||||
To change a users space you need to go to the admin and select User Infos.
|
||||
To change a user's space you need to go to the admin and select User Infos.
|
||||
|
||||
If you use an external auth provider or proxy authentication make sure to specify a default group and space in the
|
||||
environment configuration.
|
||||
@@ -69,7 +78,7 @@ In technical terms it is a multi tenant system.
|
||||
You can compare a space to something like google drive or dropbox.
|
||||
There is only one installation of the Dropbox system, but it handles multiple users without them noticing each other.
|
||||
For Tandoor that means all people that work together on one recipe collection can be in one space.
|
||||
If you want to host the collection of your friends family or your neighbor you can create a separate space for them (through the admin interface).
|
||||
If you want to host the collection of your friends, family, or neighbor you can create a separate space for them (through the admin interface).
|
||||
|
||||
Sharing between spaces is currently not possible but is planned for future releases.
|
||||
|
||||
|
||||
64
docs/features/automation.md
Normal file
64
docs/features/automation.md
Normal file
@@ -0,0 +1,64 @@
|
||||
!!! warning
|
||||
Automations are currently in a beta stage. They work pretty stable but if I encounter any
|
||||
issues while working on them, I might change how they work breaking existing automations.
|
||||
I will try to avoid this and am pretty confident it won't happen.
|
||||
|
||||
|
||||
Automations allow Tandoor to automatically perform certain tasks, especially when importing recipes, that
|
||||
would otherwise have to be done manually. Currently, the following automations are supported.
|
||||
|
||||
## Unit, Food, Keyword Alias
|
||||
Foods, Units and Keywords can have automations that automatically replace them with another object
|
||||
to allow aliasing them.
|
||||
|
||||
This helps to add consistency to the naming of objects, for example to always use the singular form
|
||||
for the main name if a plural form is configured.
|
||||
|
||||
These automations are best created by dragging and dropping Foods, Units or Keywords in their respective
|
||||
views and creating the automation there.
|
||||
|
||||
You can also create them manually by setting the following
|
||||
- **Parameter 1**: name of food/unit/keyword to match
|
||||
- **Parameter 2**: name of food/unit/keyword to replace matched food with
|
||||
|
||||
These rules are processed whenever you are importing recipes from websites or other apps
|
||||
and when using the simple ingredient input (shopping, recipe editor, ...).
|
||||
|
||||
## Description Replace
|
||||
This automation is a bit more complicated than the alis rules. It is run when importing a recipe
|
||||
from a website.
|
||||
|
||||
It uses Regular Expressions (RegEx) to determine if a description should be altered, what exactly to remove
|
||||
and what to replace it with.
|
||||
|
||||
- **Parameter 1**: pattern of which sites to match (e.g. `.*.chefkoch.de.*`, `.*`)
|
||||
- **Parameter 2**: pattern of what to replace (e.g. `.*`)
|
||||
- **Parameter 3**: value to replace matched occurrence of parameter 2 with. Only one occurrence of the pattern is replaced.
|
||||
|
||||
To replace the description the python [re.sub](https://docs.python.org/2/library/re.html#re.sub) function is used
|
||||
like this `re.sub(<parameter 2>, <parameter 2>, <descriotion>, count=1)`
|
||||
|
||||
To test out your patterns and learn about RegEx you can use [regexr.com](https://regexr.com/)
|
||||
|
||||
!!! info
|
||||
In order to prevent denial of service attacks on the RegEx engine the number of replace automations
|
||||
and the length of the inputs that are processed are limited. Those limits should never be reached
|
||||
during normal usage.
|
||||
|
||||
## Instruction Replace
|
||||
This works just like the Description Replace automation but runs against all instruction texts
|
||||
in all steps of a recipe during import.
|
||||
|
||||
Also instead of just replacing a single occurrence of the matched pattern it will replace all.
|
||||
|
||||
# Order
|
||||
If the Automation type allows for more than one rule to be executed (for example description replace)
|
||||
the rules are processed in ascending order (ordered by the *order* property of the automation).
|
||||
The default order is always 1000 to make it easier to add automations before and after other automations.
|
||||
|
||||
Example:
|
||||
1. Rule ABC (order 1000) replaces `everything` with `abc`
|
||||
2. Rule DEF (order 2000) replaces `everything` with `def`
|
||||
3. Rule XYZ (order 500) replaces `everything` with `xyz`
|
||||
|
||||
After processing rules XYZ, then ABC and then DEF the description will have the value `def`
|
||||
@@ -1,7 +1,7 @@
|
||||
This application features a very versatile import and export feature in order
|
||||
to offer the best experience possible and allow you to freely choose where your data goes.
|
||||
|
||||
!!! warning "WIP"
|
||||
!!! WARNING "WIP"
|
||||
The Module is relatively new. There is a known issue with [Timeouts](https://github.com/vabene1111/recipes/issues/417) on large exports.
|
||||
A fix is being developed and will likely be released with the next version.
|
||||
|
||||
@@ -13,7 +13,7 @@ if your favorite one is missing.
|
||||
|
||||
!!! info "Export"
|
||||
I strongly believe in everyone's right to use their data as they please and therefore want to give you
|
||||
the most possible flexibility with your recipes.
|
||||
the best possible flexibility with your recipes.
|
||||
That said for most of the people getting this application running with their recipes is the biggest priority.
|
||||
Because of this importing as many formats as possible is prioritized over exporting.
|
||||
Exporter for the different formats will follow over time.
|
||||
@@ -31,6 +31,7 @@ Overview of the capabilities of the different integrations.
|
||||
| ChefTap | ✔️ | ❌ | ❌ |
|
||||
| Pepperplate | ✔️ | ⌚ | ❌ |
|
||||
| RecipeSage | ✔️ | ✔️ | ✔️ |
|
||||
| Rezeptsuite.de | ✔️ | ❌ | ✔️ |
|
||||
| Domestica | ✔️ | ⌚ | ✔️ |
|
||||
| MealMaster | ✔️ | ❌ | ❌ |
|
||||
| RezKonv | ✔️ | ❌ | ❌ |
|
||||
@@ -75,7 +76,7 @@ Follow these steps to import your recipes
|
||||
|
||||
You will get a `Recipes.zip` file. Simply upload the file and choose the Nextcloud Cookbook type.
|
||||
|
||||
!!! warning "Folder Structure"
|
||||
!!! WARNING "Folder Structure"
|
||||
Importing only works if the folder structure is correct. If you do not use the standard path or create the
|
||||
zip file in any other way make sure the structure is as follows
|
||||
```
|
||||
@@ -94,9 +95,9 @@ Mealie provides structured data similar to nextcloud.
|
||||
|
||||
To migrate your recipes
|
||||
|
||||
1. Go to your Mealie settings and create a new Backup
|
||||
2. Download the backup by clicking on it and pressing download (this wasn't working for me, so I had to manually pull it from the server)
|
||||
3. Upload the entire `.zip` file to the importer page and import everything
|
||||
1. Go to your Mealie settings and create a new Backup.
|
||||
2. Download the backup by clicking on it and pressing download (this wasn't working for me, so I had to manually pull it from the server).
|
||||
3. Upload the entire `.zip` file to the importer page and import everything.
|
||||
|
||||
## Chowdown
|
||||
Chowdown stores all your recipes in plain text markdown files in a directory called `_recipes`.
|
||||
@@ -158,7 +159,7 @@ As ChefTap cannot import these files anyway there won't be an exporter implement
|
||||
Meal master can be imported by uploading one or more meal master files.
|
||||
The files should either be `.txt`, `.MMF` or `.MM` files.
|
||||
|
||||
The MealMaster spec allow for many variations. Currently, only the one column format for ingredients is supported.
|
||||
The MealMaster spec allows for many variations. Currently, only the one column format for ingredients is supported.
|
||||
Second line notes to ingredients are currently also not imported as a note but simply put into the instructions.
|
||||
If you have MealMaster recipes that cannot be imported feel free to raise an issue.
|
||||
|
||||
@@ -233,6 +234,9 @@ Cookmate allows you to export a `.mcb` file which you can simply upload to tando
|
||||
## RecetteTek
|
||||
RecetteTek exports are `.rtk` files which can simply be uploaded to tandoor to import all your recipes.
|
||||
|
||||
## Rezeptsuite.de
|
||||
Rezeptsuite.de exports are `.xml` files which can simply be uploaded to tandoor to import all your recipes.
|
||||
|
||||
## Melarecipes
|
||||
|
||||
Melarecipes provides multiple export formats but only the `MelaRecipes` format can export the complete collection.
|
||||
@@ -248,4 +252,4 @@ For that to work it downloads a chromium binary of about 140 MB to your server a
|
||||
Since that is something some server administrators might not want there the PDF exporter is disabled by default and can be enabled with `ENABLE_PDF_EXPORT=1` in `.env`.
|
||||
|
||||
See [this issue](https://github.com/TandoorRecipes/recipes/pull/1211) for more discussion on this and
|
||||
[this issue](https://github.com/TandoorRecipes/recipes/issues/781) for the future plans to support server side rendering.
|
||||
[this issue](https://github.com/TandoorRecipes/recipes/issues/781) for the future plans to support server side rendering.
|
||||
|
||||
@@ -180,11 +180,11 @@ server {
|
||||
#error_log /var/log/nginx/error.log;
|
||||
|
||||
# serve media files
|
||||
location /static {
|
||||
location /static/ {
|
||||
alias /var/www/recipes/staticfiles;
|
||||
}
|
||||
|
||||
location /media {
|
||||
location /media/ {
|
||||
alias /var/www/recipes/mediafiles;
|
||||
}
|
||||
|
||||
@@ -210,9 +210,12 @@ cd /var/www/recipes
|
||||
git pull
|
||||
# load envirtonment variables
|
||||
export $(cat /var/www/recipes/.env |grep "^[^#]" | xargs)
|
||||
#install project requirements
|
||||
bin/pip3 install -r requirements.txt
|
||||
# migrate database
|
||||
bin/python3 manage.py migrate
|
||||
# collect static files
|
||||
# if the output is not "0 static files copied" you might want to run the commands again to make sure everythig is collected
|
||||
bin/python3 manage.py collectstatic --no-input
|
||||
bin/python3 manage.py collectstatic_js_reverse
|
||||
# change to frontend directory
|
||||
|
||||
@@ -61,4 +61,4 @@ I left out the TLS config in this example for simplicity.
|
||||
|
||||
## WSL
|
||||
|
||||
If you want to install Tandoor on the Windows Subsystem for Linux you can find a detailed post herre https://github.com/TandoorRecipes/recipes/issues/1733
|
||||
If you want to install Tandoor on the Windows Subsystem for Linux you can find a detailed post here: <https://github.com/TandoorRecipes/recipes/issues/1733>.
|
||||
|
||||
27
docs/system/settings.md
Normal file
27
docs/system/settings.md
Normal file
@@ -0,0 +1,27 @@
|
||||
Following is a description of the different settings for a space
|
||||
|
||||
!!! WARNING WIP
|
||||
Some settings and especially this page is work in Progress and the settings may
|
||||
behave differently the described here.
|
||||
|
||||
## Use Plural form
|
||||
|
||||
Default Value: `off`
|
||||
|
||||
This setting enables tandoor to display a plural form of a food or unit, if the
|
||||
plural version is entered for the food or unit. The plural version is displayed if the
|
||||
amount needed for a recipe is greater than 1 and will be adjusted to the current amount.
|
||||
|
||||
In addition, this setting enables two new settings for an ingredient:
|
||||
|
||||
- Always show the plural version of the food: This will always display the plural version for
|
||||
a food, even if the amount is below or equal to 1. Requirement for this setting to activate
|
||||
is a plural version available in the database.
|
||||
- Always show the plural version of the unit: This will always display the plural version for
|
||||
a unit, even if the amount is below or equal to 1. Requirement for this setting to activate
|
||||
is a plural version available in the database.
|
||||
|
||||
!!! WARNING Note
|
||||
This setting is only meant to be a very simple version to enable some kind of pluralization
|
||||
for food and units. This feature may not meet your needs, but pluralization is a difficult
|
||||
topic and was discussed [here](https://github.com/TandoorRecipes/recipes/pull/1860).
|
||||
@@ -23,6 +23,7 @@ markdown_extensions:
|
||||
|
||||
plugins:
|
||||
- include-markdown
|
||||
- search
|
||||
|
||||
nav:
|
||||
- Home: 'index.md'
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||
"spaces": 2,
|
||||
"generator-cli": {
|
||||
"version": "6.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
|
||||
"POT-Creation-Date: 2023-01-19 19:14+0100\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"
|
||||
@@ -18,62 +18,62 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:369
|
||||
#: .\recipes\settings.py:382
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:370
|
||||
#: .\recipes\settings.py:383
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:371
|
||||
#: .\recipes\settings.py:384
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:372
|
||||
#: .\recipes\settings.py:385
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:373
|
||||
#: .\recipes\settings.py:386
|
||||
msgid "Danish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:374
|
||||
#: .\recipes\settings.py:387
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:375
|
||||
#: .\recipes\settings.py:388
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:376
|
||||
#: .\recipes\settings.py:389
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:377
|
||||
#: .\recipes\settings.py:390
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:378
|
||||
#: .\recipes\settings.py:391
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:379
|
||||
#: .\recipes\settings.py:392
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:380
|
||||
#: .\recipes\settings.py:393
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:381
|
||||
#: .\recipes\settings.py:394
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:382
|
||||
#: .\recipes\settings.py:395
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:383
|
||||
#: .\recipes\settings.py:396
|
||||
msgid "Swedish"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
|
||||
"POT-Creation-Date: 2023-01-19 19:14+0100\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"
|
||||
@@ -18,64 +18,64 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:369
|
||||
#: .\recipes\settings.py:382
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:370
|
||||
#: .\recipes\settings.py:383
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:371
|
||||
#: .\recipes\settings.py:384
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:372
|
||||
#: .\recipes\settings.py:385
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:373
|
||||
#: .\recipes\settings.py:386
|
||||
msgid "Danish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:374
|
||||
#: .\recipes\settings.py:387
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:375
|
||||
#: .\recipes\settings.py:388
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
|
||||
#: .\recipes\settings.py:376
|
||||
#: .\recipes\settings.py:389
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:377
|
||||
#: .\recipes\settings.py:390
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
|
||||
#: .\recipes\settings.py:378
|
||||
#: .\recipes\settings.py:391
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:379
|
||||
#: .\recipes\settings.py:392
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:380
|
||||
#: .\recipes\settings.py:393
|
||||
#, fuzzy
|
||||
#| msgid "English"
|
||||
msgid "Polish"
|
||||
msgstr "Englisch"
|
||||
|
||||
#: .\recipes\settings.py:381
|
||||
#: .\recipes\settings.py:394
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:382
|
||||
#: .\recipes\settings.py:395
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:383
|
||||
#: .\recipes\settings.py:396
|
||||
msgid "Swedish"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
|
||||
"POT-Creation-Date: 2023-01-19 19:14+0100\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"
|
||||
@@ -18,62 +18,62 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:369
|
||||
#: .\recipes\settings.py:382
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:370
|
||||
#: .\recipes\settings.py:383
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:371
|
||||
#: .\recipes\settings.py:384
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:372
|
||||
#: .\recipes\settings.py:385
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:373
|
||||
#: .\recipes\settings.py:386
|
||||
msgid "Danish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:374
|
||||
#: .\recipes\settings.py:387
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:375
|
||||
#: .\recipes\settings.py:388
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:376
|
||||
#: .\recipes\settings.py:389
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:377
|
||||
#: .\recipes\settings.py:390
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:378
|
||||
#: .\recipes\settings.py:391
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:379
|
||||
#: .\recipes\settings.py:392
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:380
|
||||
#: .\recipes\settings.py:393
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:381
|
||||
#: .\recipes\settings.py:394
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:382
|
||||
#: .\recipes\settings.py:395
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:383
|
||||
#: .\recipes\settings.py:396
|
||||
msgid "Swedish"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
|
||||
"POT-Creation-Date: 2023-01-19 19:14+0100\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"
|
||||
@@ -18,62 +18,62 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:369
|
||||
#: .\recipes\settings.py:382
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:370
|
||||
#: .\recipes\settings.py:383
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:371
|
||||
#: .\recipes\settings.py:384
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:372
|
||||
#: .\recipes\settings.py:385
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:373
|
||||
#: .\recipes\settings.py:386
|
||||
msgid "Danish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:374
|
||||
#: .\recipes\settings.py:387
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:375
|
||||
#: .\recipes\settings.py:388
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:376
|
||||
#: .\recipes\settings.py:389
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:377
|
||||
#: .\recipes\settings.py:390
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:378
|
||||
#: .\recipes\settings.py:391
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:379
|
||||
#: .\recipes\settings.py:392
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:380
|
||||
#: .\recipes\settings.py:393
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:381
|
||||
#: .\recipes\settings.py:394
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:382
|
||||
#: .\recipes\settings.py:395
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:383
|
||||
#: .\recipes\settings.py:396
|
||||
msgid "Swedish"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
|
||||
"POT-Creation-Date: 2023-01-19 19:14+0100\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"
|
||||
@@ -18,62 +18,62 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:369
|
||||
#: .\recipes\settings.py:382
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:370
|
||||
#: .\recipes\settings.py:383
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:371
|
||||
#: .\recipes\settings.py:384
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:372
|
||||
#: .\recipes\settings.py:385
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:373
|
||||
#: .\recipes\settings.py:386
|
||||
msgid "Danish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:374
|
||||
#: .\recipes\settings.py:387
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:375
|
||||
#: .\recipes\settings.py:388
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:376
|
||||
#: .\recipes\settings.py:389
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:377
|
||||
#: .\recipes\settings.py:390
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:378
|
||||
#: .\recipes\settings.py:391
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:379
|
||||
#: .\recipes\settings.py:392
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:380
|
||||
#: .\recipes\settings.py:393
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:381
|
||||
#: .\recipes\settings.py:394
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:382
|
||||
#: .\recipes\settings.py:395
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:383
|
||||
#: .\recipes\settings.py:396
|
||||
msgid "Swedish"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
|
||||
"POT-Creation-Date: 2023-01-19 19:14+0100\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"
|
||||
@@ -17,62 +17,62 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: .\recipes\settings.py:369
|
||||
#: .\recipes\settings.py:382
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:370
|
||||
#: .\recipes\settings.py:383
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:371
|
||||
#: .\recipes\settings.py:384
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:372
|
||||
#: .\recipes\settings.py:385
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:373
|
||||
#: .\recipes\settings.py:386
|
||||
msgid "Danish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:374
|
||||
#: .\recipes\settings.py:387
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:375
|
||||
#: .\recipes\settings.py:388
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:376
|
||||
#: .\recipes\settings.py:389
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:377
|
||||
#: .\recipes\settings.py:390
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:378
|
||||
#: .\recipes\settings.py:391
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:379
|
||||
#: .\recipes\settings.py:392
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:380
|
||||
#: .\recipes\settings.py:393
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:381
|
||||
#: .\recipes\settings.py:394
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:382
|
||||
#: .\recipes\settings.py:395
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:383
|
||||
#: .\recipes\settings.py:396
|
||||
msgid "Swedish"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
|
||||
"POT-Creation-Date: 2023-01-19 19:14+0100\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"
|
||||
@@ -18,62 +18,62 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: .\recipes\settings.py:369
|
||||
#: .\recipes\settings.py:382
|
||||
msgid "Armenian "
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:370
|
||||
#: .\recipes\settings.py:383
|
||||
msgid "Bulgarian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:371
|
||||
#: .\recipes\settings.py:384
|
||||
msgid "Catalan"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:372
|
||||
#: .\recipes\settings.py:385
|
||||
msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:373
|
||||
#: .\recipes\settings.py:386
|
||||
msgid "Danish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:374
|
||||
#: .\recipes\settings.py:387
|
||||
msgid "Dutch"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:375
|
||||
#: .\recipes\settings.py:388
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:376
|
||||
#: .\recipes\settings.py:389
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:377
|
||||
#: .\recipes\settings.py:390
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:378
|
||||
#: .\recipes\settings.py:391
|
||||
msgid "Italian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:379
|
||||
#: .\recipes\settings.py:392
|
||||
msgid "Latvian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:380
|
||||
#: .\recipes\settings.py:393
|
||||
msgid "Polish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:381
|
||||
#: .\recipes\settings.py:394
|
||||
msgid "Russian"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:382
|
||||
#: .\recipes\settings.py:395
|
||||
msgid "Spanish"
|
||||
msgstr ""
|
||||
|
||||
#: .\recipes\settings.py:383
|
||||
#: .\recipes\settings.py:396
|
||||
msgid "Swedish"
|
||||
msgstr ""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user