mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-26 11:49:41 -05:00
Compare commits
403 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4378a6b0c7 | ||
|
|
d5ca8e9c96 | ||
|
|
bbcf7ba8a7 | ||
|
|
f29f77a1d5 | ||
|
|
a3f8b2272c | ||
|
|
008a61823d | ||
|
|
a60f1a3e92 | ||
|
|
ebb6e669e2 | ||
|
|
fdd61d3caf | ||
|
|
72ac272962 | ||
|
|
4395737cc5 | ||
|
|
f3aef2c10b | ||
|
|
df1b75d88a | ||
|
|
0b5fb69664 | ||
|
|
ff2a75476b | ||
|
|
ea515c199c | ||
|
|
12f0cdb484 | ||
|
|
f2cd220e22 | ||
|
|
a0a3629e4c | ||
|
|
8263c6b725 | ||
|
|
b8b3620ade | ||
|
|
e55faa02d5 | ||
|
|
64efadfc81 | ||
|
|
fb90eede52 | ||
|
|
48fda987fb | ||
|
|
8e85fd57b6 | ||
|
|
3f475aed03 | ||
|
|
12a11766d9 | ||
|
|
0e90700ce9 | ||
|
|
b7f202d645 | ||
|
|
f0f12ca83f | ||
|
|
b14d8f0051 | ||
|
|
5fd8c56324 | ||
|
|
8abef1d8cc | ||
|
|
0c8c74c0ac | ||
|
|
0b40414d23 | ||
|
|
d4b8190f55 | ||
|
|
0ce7ea0b61 | ||
|
|
817c4cb9d6 | ||
|
|
9962c849ed | ||
|
|
8313dc8abe | ||
|
|
2781730778 | ||
|
|
985e98c699 | ||
|
|
d244af28e3 | ||
|
|
488ac3b94a | ||
|
|
b49426e35c | ||
|
|
a81bac1193 | ||
|
|
7fe80b7a5f | ||
|
|
a6e3ab2dbe | ||
|
|
a4f0f38300 | ||
|
|
1a5b7244dd | ||
|
|
dff9f91d4c | ||
|
|
59d1c1dcdc | ||
|
|
2cff936b5b | ||
|
|
d9dc644cb6 | ||
|
|
2280d04fd2 | ||
|
|
1c8cb69cf3 | ||
|
|
e33c3789b7 | ||
|
|
8d85800e2f | ||
|
|
c08c1d30ad | ||
|
|
3c00e1ecdb | ||
|
|
83947e31aa | ||
|
|
b4f90fbbb3 | ||
|
|
0f55f91586 | ||
|
|
7d0a9b11a0 | ||
|
|
9167261714 | ||
|
|
57fae34ff6 | ||
|
|
961578385d | ||
|
|
0dc6bed7ad | ||
|
|
c78c615372 | ||
|
|
04bdec3889 | ||
|
|
6af3d7c98f | ||
|
|
73be817c10 | ||
|
|
faf78fc254 | ||
|
|
2c85c370e6 | ||
|
|
3a38a095d8 | ||
|
|
e754b13340 | ||
|
|
900c28caba | ||
|
|
c5ce197ed7 | ||
|
|
9573ff0932 | ||
|
|
f554963ae7 | ||
|
|
961619c156 | ||
|
|
4ecadab53c | ||
|
|
744501a65d | ||
|
|
44cc66888b | ||
|
|
0695909b6c | ||
|
|
7a4fa38725 | ||
|
|
9f360d8af6 | ||
|
|
5dad6b8b17 | ||
|
|
5b6df6ed2e | ||
|
|
82d1a75d80 | ||
|
|
50429207c5 | ||
|
|
589bc1f1aa | ||
|
|
824dcefc1a | ||
|
|
3f8c952237 | ||
|
|
077db58de0 | ||
|
|
3c527fd112 | ||
|
|
cd1f6ad7b0 | ||
|
|
3af7e98216 | ||
|
|
cb363d6321 | ||
|
|
39656152d3 | ||
|
|
22c88e5269 | ||
|
|
89550e8345 | ||
|
|
9846c4df18 | ||
|
|
924d1cb71b | ||
|
|
44236f611e | ||
|
|
012dea5a0c | ||
|
|
820c9b704f | ||
|
|
ed92926ec4 | ||
|
|
bc560ee76d | ||
|
|
b6c4130e4b | ||
|
|
b0ca391bb4 | ||
|
|
45a6b1d386 | ||
|
|
4626ffcbc5 | ||
|
|
c3a9cc94fa | ||
|
|
a8eb8bb8d7 | ||
|
|
b14c9aa68c | ||
|
|
b03db7ad36 | ||
|
|
cc706a1195 | ||
|
|
b1028f49d6 | ||
|
|
6d0cc96cc8 | ||
|
|
969a91e751 | ||
|
|
c352bf82dd | ||
|
|
a305527ba2 | ||
|
|
c0e35e89e9 | ||
|
|
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 | ||
|
|
da8262a9b5 | ||
|
|
f0cf4a23e4 | ||
|
|
489c81c378 | ||
|
|
730344e326 | ||
|
|
7e6b1d3638 | ||
|
|
15f65cd711 | ||
|
|
dba205dafb | ||
|
|
5ae149a1b6 | ||
|
|
4bb2307007 | ||
|
|
be0088aec6 | ||
|
|
c56710ae0c | ||
|
|
1a420bc002 | ||
|
|
545e4f7af4 | ||
|
|
d2a148ae7d | ||
|
|
580591a69e | ||
|
|
409b438776 | ||
|
|
549175b56d | ||
|
|
0e3f5006b1 | ||
|
|
54043a0ae5 | ||
|
|
36fdc8cd9e | ||
|
|
87cf3b2289 | ||
|
|
adb4071fdb | ||
|
|
2a20f5e6e2 | ||
|
|
00f7ae3d66 | ||
|
|
f1f4e7ca8e | ||
|
|
6d7b3b8bfa | ||
|
|
7ebccf564d | ||
|
|
0421a1aa6c | ||
|
|
c118ab9a3c | ||
|
|
02a12cf724 | ||
|
|
f28ca41b7b | ||
|
|
6e677cf3cd | ||
|
|
d30a23f7ef | ||
|
|
88fea6f25d | ||
|
|
fc0b5bd738 | ||
|
|
5174f9939c | ||
|
|
8f9a489c7e | ||
|
|
fc72efac04 | ||
|
|
72f57cf671 | ||
|
|
85b95d1e96 | ||
|
|
35dee43f0b | ||
|
|
fb683bf230 | ||
|
|
a852f581ba | ||
|
|
cc417f1499 | ||
|
|
7f9da4c4fb | ||
|
|
31d3f9abee | ||
|
|
f9670e9833 | ||
|
|
465af8c1a4 | ||
|
|
ffe743e233 | ||
|
|
6b09731a55 | ||
|
|
182a94e0c7 | ||
|
|
2adaedfd1a | ||
|
|
5074326471 | ||
|
|
4807a16a0f | ||
|
|
af044f1002 | ||
|
|
cdf77c8796 | ||
|
|
e68bedf7eb | ||
|
|
5e21e7fa8e | ||
|
|
f49b39b216 | ||
|
|
0d24292f52 | ||
|
|
f3b7016be8 | ||
|
|
0f77c831c9 | ||
|
|
be48e57453 | ||
|
|
3b45ca18af | ||
|
|
da1b22c148 | ||
|
|
9dab21f972 | ||
|
|
89a5f92ace | ||
|
|
7be705f6a1 | ||
|
|
8e60566311 | ||
|
|
ca9c96647e | ||
|
|
70e6585669 | ||
|
|
d0cb7a79f9 | ||
|
|
cfd24de72a | ||
|
|
54acfe3e39 | ||
|
|
12ad6af8c3 | ||
|
|
cf24e1014a | ||
|
|
2f8b479fdd |
@@ -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
|
||||
|
||||
8
.idea/dictionaries/vaben.xml
generated
Normal file
8
.idea/dictionaries/vaben.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="vaben">
|
||||
<words>
|
||||
<w>pinia</w>
|
||||
<w>selfhosted</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
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,7 +32,7 @@ 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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -52,7 +52,6 @@ def has_group_permission(user, groups, no_cache=False):
|
||||
return cached_result
|
||||
|
||||
result = False
|
||||
print('running check', user, groups_allowed)
|
||||
if user.is_authenticated:
|
||||
if user_space := user.userspace_set.filter(active=True):
|
||||
if len(user_space) != 1:
|
||||
@@ -124,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]
|
||||
|
||||
|
||||
@@ -396,3 +424,18 @@ def get_images_from_soup(soup, url):
|
||||
if 'http' in u:
|
||||
images.append(u)
|
||||
return images
|
||||
|
||||
|
||||
def clean_dict(input_dict, key):
|
||||
if type(input_dict) == dict:
|
||||
for x in list(input_dict):
|
||||
if x == key:
|
||||
del input_dict[x]
|
||||
elif type(input_dict[x]) == dict:
|
||||
input_dict[x] = clean_dict(input_dict[x], key)
|
||||
elif type(input_dict[x]) == list:
|
||||
temp_list = []
|
||||
for e in input_dict[x]:
|
||||
temp_list.append(clean_dict(e, key))
|
||||
|
||||
return input_dict
|
||||
|
||||
@@ -47,6 +47,8 @@ class RecipeShoppingEditor():
|
||||
self.mealplan = self._kwargs.get('mealplan', None)
|
||||
if type(self.mealplan) in [int, float]:
|
||||
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
|
||||
if type(self.mealplan) == dict:
|
||||
self.mealplan = MealPlan.objects.filter(id=self.mealplan['id'], space=self.space).first()
|
||||
self.id = self._kwargs.get('id', None)
|
||||
|
||||
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
|
||||
@@ -107,7 +109,10 @@ class RecipeShoppingEditor():
|
||||
self.servings = float(servings)
|
||||
|
||||
if mealplan := kwargs.get('mealplan', None):
|
||||
self.mealplan = mealplan
|
||||
if type(mealplan) == dict:
|
||||
self.mealplan = MealPlan.objects.filter(id=mealplan['id'], space=self.space).first()
|
||||
else:
|
||||
self.mealplan = mealplan
|
||||
self.recipe = mealplan.recipe
|
||||
elif recipe := kwargs.get('recipe', None):
|
||||
self.recipe = recipe
|
||||
@@ -310,4 +315,4 @@ class RecipeShoppingEditor():
|
||||
# )
|
||||
|
||||
# # return all shopping list items
|
||||
# return list_recipe
|
||||
# return list_recipe
|
||||
@@ -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:
|
||||
|
||||
@@ -5,6 +5,7 @@ from zipfile import ZipFile
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Recipe, Step
|
||||
|
||||
@@ -23,41 +24,60 @@ class Mealie(Integration):
|
||||
name=recipe_json['name'].strip(), description=description,
|
||||
created_by=self.request.user, internal=True, space=self.request.space)
|
||||
|
||||
# TODO parse times (given in PT2H3M )
|
||||
# @vabene check recipe_url_import.iso_duration_to_minutes I think it does what you are looking for
|
||||
|
||||
ingredients_added = False
|
||||
for s in recipe_json['recipe_instructions']:
|
||||
step = Step.objects.create(
|
||||
instruction=s['text'], space=self.request.space,
|
||||
)
|
||||
if not ingredients_added:
|
||||
ingredients_added = True
|
||||
|
||||
if len(recipe_json['description'].strip()) > 500:
|
||||
step.instruction = recipe_json['description'].strip() + '\n\n' + step.instruction
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
for ingredient in recipe_json['recipe_ingredient']:
|
||||
try:
|
||||
if ingredient['food']:
|
||||
f = ingredient_parser.get_food(ingredient['food'])
|
||||
u = ingredient_parser.get_unit(ingredient['unit'])
|
||||
amount = ingredient['quantity']
|
||||
note = ingredient['note']
|
||||
original_text = None
|
||||
else:
|
||||
amount, unit, food, note = ingredient_parser.parse(ingredient['note'])
|
||||
f = ingredient_parser.get_food(food)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
original_text = ingredient['note']
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note, original_text=original_text, space=self.request.space,
|
||||
))
|
||||
except Exception:
|
||||
pass
|
||||
step = Step.objects.create(instruction=s['text'], space=self.request.space, )
|
||||
recipe.steps.add(step)
|
||||
|
||||
step = recipe.steps.first()
|
||||
if not step: # if there is no step in the exported data
|
||||
step = Step.objects.create(instruction='', space=self.request.space, )
|
||||
recipe.steps.add(step)
|
||||
|
||||
if len(recipe_json['description'].strip()) > 500:
|
||||
step.instruction = recipe_json['description'].strip() + '\n\n' + step.instruction
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
for ingredient in recipe_json['recipe_ingredient']:
|
||||
try:
|
||||
if ingredient['food']:
|
||||
f = ingredient_parser.get_food(ingredient['food'])
|
||||
u = ingredient_parser.get_unit(ingredient['unit'])
|
||||
amount = ingredient['quantity']
|
||||
note = ingredient['note']
|
||||
original_text = None
|
||||
else:
|
||||
amount, unit, food, note = ingredient_parser.parse(ingredient['note'])
|
||||
f = ingredient_parser.get_food(food)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
original_text = ingredient['note']
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note, original_text=original_text, space=self.request.space,
|
||||
))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if 'notes' in recipe_json and len(recipe_json['notes']) > 0:
|
||||
notes_text = "#### Notes \n\n"
|
||||
for n in recipe_json['notes']:
|
||||
notes_text += f'{n["text"]} \n'
|
||||
|
||||
step = Step.objects.create(
|
||||
instruction=notes_text, space=self.request.space,
|
||||
)
|
||||
recipe.steps.add(step)
|
||||
|
||||
if 'recipe_yield' in recipe_json:
|
||||
recipe.servings = parse_servings(recipe_json['recipe_yield'])
|
||||
recipe.servings_text = parse_servings_text(recipe_json['recipe_yield'])
|
||||
|
||||
if 'total_time' in recipe_json and recipe_json['total_time'] is not None:
|
||||
recipe.working_time = parse_time(recipe_json['total_time'])
|
||||
|
||||
if 'org_url' in recipe_json:
|
||||
recipe.source_url = recipe_json['org_url']
|
||||
|
||||
recipe.save()
|
||||
|
||||
for f in self.files:
|
||||
if '.zip' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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
|
||||
from cookbook.helper.recipe_url_import import iso_duration_to_minutes
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Keyword, Recipe, Step
|
||||
from cookbook.models import Ingredient, Keyword, Recipe, Step, NutritionInformation
|
||||
|
||||
|
||||
class NextcloudCookbook(Integration):
|
||||
@@ -70,12 +71,21 @@ class NextcloudCookbook(Integration):
|
||||
recipe.steps.add(step)
|
||||
|
||||
if 'nutrition' in recipe_json:
|
||||
nutrition = {}
|
||||
try:
|
||||
recipe.nutrition.calories = recipe_json['nutrition']['calories'].replace(' kcal', '').replace(' ', '')
|
||||
recipe.nutrition.proteins = recipe_json['nutrition']['calories'].replace(' g', '').replace(',', '.').replace(' ', '')
|
||||
recipe.nutrition.fats = recipe_json['nutrition']['calories'].replace(' g', '').replace(',', '.').replace(' ', '')
|
||||
recipe.nutrition.carbohydrates = recipe_json['nutrition']['calories'].replace(' g', '').replace(',', '.').replace(' ', '')
|
||||
except Exception:
|
||||
if 'calories' in recipe_json['nutrition']:
|
||||
nutrition['calories'] = int(re.search(r'\d+', recipe_json['nutrition']['calories']).group())
|
||||
if 'proteinContent' in recipe_json['nutrition']:
|
||||
nutrition['proteins'] = int(re.search(r'\d+', recipe_json['nutrition']['proteinContent']).group())
|
||||
if 'fatContent' in recipe_json['nutrition']:
|
||||
nutrition['fats'] = int(re.search(r'\d+', recipe_json['nutrition']['fatContent']).group())
|
||||
if 'carbohydrateContent' in recipe_json['nutrition']:
|
||||
nutrition['carbohydrates'] = int(re.search(r'\d+', recipe_json['nutrition']['carbohydrateContent']).group())
|
||||
|
||||
if nutrition != {}:
|
||||
recipe.nutrition = NutritionInformation.objects.create(**nutrition, space=self.request.space)
|
||||
recipe.save()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
for f in self.files:
|
||||
@@ -87,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()
|
||||
|
||||
@@ -5,6 +5,9 @@ import re
|
||||
from gettext import gettext as _
|
||||
from io import BytesIO
|
||||
|
||||
import requests
|
||||
import validators
|
||||
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text
|
||||
from cookbook.integration.integration import Integration
|
||||
@@ -81,7 +84,14 @@ class Paprika(Integration):
|
||||
|
||||
recipe.steps.add(step)
|
||||
|
||||
if recipe_json.get("photo_data", None):
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])), filetype='.jpeg')
|
||||
try:
|
||||
if recipe_json.get("image_url", None):
|
||||
url = recipe_json.get("image_url", None)
|
||||
if validators.url(url, public=True):
|
||||
response = requests.get(url)
|
||||
self.import_recipe_image(recipe, BytesIO(response.content))
|
||||
except:
|
||||
if recipe_json.get("photo_data", None):
|
||||
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])), filetype='.jpeg')
|
||||
|
||||
return recipe
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -41,7 +41,7 @@ class RecipeKeeper(Integration):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
step = Step.objects.create(instruction='', space=self.request.space,)
|
||||
step = Step.objects.create(instruction='', space=self.request.space, )
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"):
|
||||
@@ -51,13 +51,20 @@ class RecipeKeeper(Integration):
|
||||
f = ingredient_parser.get_food(food)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
|
||||
food=f, unit=u, amount=amount, note=note, original_text=str(ingredient).replace('<p>', '').replace('</p>', ''), space=self.request.space,
|
||||
))
|
||||
|
||||
for s in file.find("div", {"itemprop": "recipeDirections"}).find_all("p"):
|
||||
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."
|
||||
|
||||
@@ -8,8 +8,8 @@ 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: 2022-08-18 14:32+0000\n"
|
||||
"Last-Translator: Mathias Rasmussen <math625f@gmail.com>\n"
|
||||
"PO-Revision-Date: 2023-03-06 10:55+0000\n"
|
||||
"Last-Translator: Anders Obro <oebro@duck.com>\n"
|
||||
"Language-Team: Danish <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/da/>\n"
|
||||
"Language: da\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.10.1\n"
|
||||
"X-Generator: Weblate 4.15\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
|
||||
#: .\cookbook\templates\space.html:49 .\cookbook\templates\stats.html:28
|
||||
@@ -2122,9 +2122,9 @@ msgid ""
|
||||
"return more results than needed to make sure you find what you are looking "
|
||||
"for."
|
||||
msgstr ""
|
||||
"Find hvad du har brug for selvom opskriften har stavefejl. Kan måske "
|
||||
"returnere flere resultater end du har brug for, for at være sikker på at du "
|
||||
"finder hvad du leder efter."
|
||||
"Find hvad du har brug for, selvom opskriften har stavefejl. Kan måske "
|
||||
"returnere flere resultater end du har brug for, for at være sikker på, at du "
|
||||
"finder, hvad du leder efter."
|
||||
|
||||
#: .\cookbook\templates\settings.html:182
|
||||
msgid "This is the default behavior"
|
||||
@@ -2196,8 +2196,7 @@ msgid ""
|
||||
"You can sign in to your account using any of the following third party\n"
|
||||
" accounts:"
|
||||
msgstr ""
|
||||
"Du kan logge ind på din konto med enhver af de følgende tredjepartsapps\n"
|
||||
" kontoer:"
|
||||
"Du kan logge ind på din konto med enhver af de følgende tredjepartskontoer:"
|
||||
|
||||
#: .\cookbook\templates\socialaccount\connections.html:52
|
||||
msgid ""
|
||||
@@ -2212,7 +2211,7 @@ msgstr "Tilføj en tredjepartskonto"
|
||||
|
||||
#: .\cookbook\templates\socialaccount\signup.html:5
|
||||
msgid "Signup"
|
||||
msgstr "Registrering"
|
||||
msgstr "Registrer"
|
||||
|
||||
#: .\cookbook\templates\socialaccount\signup.html:10
|
||||
#, python-format
|
||||
|
||||
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.
2647
cookbook/locale/id/LC_MESSAGES/django.po
Normal file
2647
cookbook/locale/id/LC_MESSAGES/django.po
Normal file
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.
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: 2022-02-11 08:52+0100\n"
|
||||
"PO-Revision-Date: 2022-03-08 01:31+0000\n"
|
||||
"Last-Translator: Felipe Castro <felipefcastro@gmail.com>\n"
|
||||
"PO-Revision-Date: 2023-02-18 10:55+0000\n"
|
||||
"Last-Translator: Joachim Weber <joachim.weber@gmx.de>\n"
|
||||
"Language-Team: Portuguese (Brazil) <http://translate.tandoor.dev/projects/"
|
||||
"tandoor/recipes-backend/pt_BR/>\n"
|
||||
"Language: pt_BR\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||
"X-Generator: Weblate 4.10.1\n"
|
||||
"X-Generator: Weblate 4.15\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
|
||||
#: .\cookbook\templates\space.html:50 .\cookbook\templates\stats.html:28
|
||||
@@ -158,7 +158,7 @@ msgstr ""
|
||||
#: .\cookbook\templates\url_import.html:195
|
||||
#: .\cookbook\templates\url_import.html:585 .\cookbook\views\lists.py:97
|
||||
msgid "Keywords"
|
||||
msgstr ""
|
||||
msgstr "Palavras-chave"
|
||||
|
||||
#: .\cookbook\forms.py:131
|
||||
msgid "Preparation time in minutes"
|
||||
@@ -513,7 +513,7 @@ msgstr ""
|
||||
#: .\cookbook\templates\url_import.html:231
|
||||
#: .\cookbook\templates\url_import.html:462
|
||||
msgid "Servings"
|
||||
msgstr ""
|
||||
msgstr "Porções"
|
||||
|
||||
#: .\cookbook\integration\saffron.py:25
|
||||
msgid "Waiting time"
|
||||
@@ -585,7 +585,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\models.py:302 .\cookbook\templates\base.html:90
|
||||
msgid "Books"
|
||||
msgstr ""
|
||||
msgstr "Livros"
|
||||
|
||||
#: .\cookbook\models.py:310
|
||||
msgid "Small"
|
||||
@@ -598,7 +598,7 @@ msgstr ""
|
||||
#: .\cookbook\models.py:310 .\cookbook\templates\generic\new_template.html:6
|
||||
#: .\cookbook\templates\generic\new_template.html:14
|
||||
msgid "New"
|
||||
msgstr ""
|
||||
msgstr "Novo"
|
||||
|
||||
#: .\cookbook\models.py:513
|
||||
msgid " is part of a recipe step and cannot be deleted"
|
||||
@@ -677,7 +677,7 @@ msgstr ""
|
||||
#: .\cookbook\templates\shopping_list.html:37
|
||||
#: .\cookbook\templates\space.html:109
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
msgstr "Editar"
|
||||
|
||||
#: .\cookbook\tables.py:115 .\cookbook\tables.py:138
|
||||
#: .\cookbook\templates\generic\delete_template.html:7
|
||||
@@ -715,7 +715,7 @@ msgstr ""
|
||||
#: .\cookbook\templates\settings.html:17
|
||||
#: .\cookbook\templates\socialaccount\connections.html:10
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
msgstr "Configurações"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:13
|
||||
msgid "Email"
|
||||
@@ -937,7 +937,7 @@ msgstr ""
|
||||
#: .\cookbook\templates\account\signup.html:48
|
||||
#: .\cookbook\templates\socialaccount\signup.html:39
|
||||
msgid "and"
|
||||
msgstr ""
|
||||
msgstr "e"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:52
|
||||
#: .\cookbook\templates\socialaccount\signup.html:43
|
||||
@@ -989,7 +989,7 @@ msgstr ""
|
||||
#: .\cookbook\templates\shopping_list.html:208
|
||||
#: .\cookbook\templates\supermarket.html:7
|
||||
msgid "Supermarket"
|
||||
msgstr ""
|
||||
msgstr "Supermercado"
|
||||
|
||||
#: .\cookbook\templates\base.html:163
|
||||
msgid "Supermarket Category"
|
||||
@@ -1027,7 +1027,7 @@ msgstr ""
|
||||
#: .\cookbook\templates\shopping_list.html:165
|
||||
#: .\cookbook\templates\shopping_list.html:188
|
||||
msgid "Create"
|
||||
msgstr ""
|
||||
msgstr "Criar"
|
||||
|
||||
#: .\cookbook\templates\base.html:259
|
||||
#: .\cookbook\templates\generic\list_template.html:14
|
||||
@@ -1190,7 +1190,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:26
|
||||
msgid "Protected"
|
||||
msgstr ""
|
||||
msgstr "Protegido"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:41
|
||||
msgid "Cascade"
|
||||
@@ -1268,7 +1268,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\include\recipe_open_modal.html:18
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
msgstr "Fechar"
|
||||
|
||||
#: .\cookbook\templates\include\recipe_open_modal.html:32
|
||||
msgid "Open Recipe"
|
||||
@@ -1821,7 +1821,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\settings.html:162
|
||||
msgid "or"
|
||||
msgstr ""
|
||||
msgstr "ou"
|
||||
|
||||
#: .\cookbook\templates\settings.html:173
|
||||
msgid ""
|
||||
@@ -2062,7 +2062,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\space.html:120
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
msgstr "usuário"
|
||||
|
||||
#: .\cookbook\templates\space.html:121
|
||||
msgid "guest"
|
||||
@@ -2273,7 +2273,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\url_import.html:214
|
||||
msgid "Image"
|
||||
msgstr ""
|
||||
msgstr "Imagem"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:246
|
||||
msgid "Prep Time"
|
||||
@@ -2359,7 +2359,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\url_import.html:640
|
||||
msgid "Information"
|
||||
msgstr ""
|
||||
msgstr "Informação"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:642
|
||||
msgid ""
|
||||
|
||||
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
2621
cookbook/locale/tr/id/LC_MESSAGES/django.po
Normal file
2621
cookbook/locale/tr/id/LC_MESSAGES/django.po
Normal file
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
@@ -8,14 +8,16 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-06-12 20:30+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-03-12 02:55+0000\n"
|
||||
"Last-Translator: Feng Zhong <fewoodse@gmail.com>\n"
|
||||
"Language-Team: Chinese (Traditional) <http://translate.tandoor.dev/projects/"
|
||||
"tandoor/recipes-backend/zh_Hant/>\n"
|
||||
"Language: zh_Hant\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 4.15\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:98
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:246
|
||||
@@ -23,41 +25,41 @@ msgstr ""
|
||||
#: .\cookbook\templates\space.html:37 .\cookbook\templates\stats.html:28
|
||||
#: .\cookbook\templates\url_import.html:270 .\cookbook\views\lists.py:67
|
||||
msgid "Ingredients"
|
||||
msgstr ""
|
||||
msgstr "食材"
|
||||
|
||||
#: .\cookbook\forms.py:49
|
||||
msgid ""
|
||||
"Color of the top navigation bar. Not all colors work with all themes, just "
|
||||
"try them out!"
|
||||
msgstr ""
|
||||
msgstr "頂部導航欄的顏色。並非所有的顏色都適用於所有的主題,只要試一試就可以了!"
|
||||
|
||||
#: .\cookbook\forms.py:51
|
||||
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
|
||||
msgstr ""
|
||||
msgstr "在菜譜中插入新食材時使用的默認單位。"
|
||||
|
||||
#: .\cookbook\forms.py:53
|
||||
msgid ""
|
||||
"Enables support for fractions in ingredient amounts (e.g. convert decimals "
|
||||
"to fractions automatically)"
|
||||
msgstr ""
|
||||
msgstr "啟用對食材數量的分數支持(例如自動將小數轉換為分數)"
|
||||
|
||||
#: .\cookbook\forms.py:56
|
||||
msgid ""
|
||||
"Users with whom newly created meal plan/shopping list entries should be "
|
||||
"shared by default."
|
||||
msgstr ""
|
||||
msgstr "默認情況下,將自動與用戶共享新創建的膳食計劃。"
|
||||
|
||||
#: .\cookbook\forms.py:58
|
||||
msgid "Show recently viewed recipes on search page."
|
||||
msgstr ""
|
||||
msgstr "在搜索頁面上查看最近看過的食譜。"
|
||||
|
||||
#: .\cookbook\forms.py:59
|
||||
msgid "Number of decimals to round ingredients."
|
||||
msgstr ""
|
||||
msgstr "四舍五入食材的小數點數量。"
|
||||
|
||||
#: .\cookbook\forms.py:60
|
||||
msgid "If you want to be able to create and see comments underneath recipes."
|
||||
msgstr ""
|
||||
msgstr "如果你希望能夠在菜譜下面創建並看到評論。"
|
||||
|
||||
#: .\cookbook\forms.py:62
|
||||
msgid ""
|
||||
@@ -66,22 +68,25 @@ msgid ""
|
||||
"Useful when shopping with multiple people but might use a little bit of "
|
||||
"mobile data. If lower than instance limit it is reset when saving."
|
||||
msgstr ""
|
||||
"設置為0將禁用自動同步。當查看購物清單時,清單會每隔幾秒鐘更新一次,以同步其他"
|
||||
"人可能做出的改變。在與多人一起購物時很有用,但可能會消耗一點移動數據。如果低"
|
||||
"於實例限制,它將在保存時被重置。"
|
||||
|
||||
#: .\cookbook\forms.py:65
|
||||
msgid "Makes the navbar stick to the top of the page."
|
||||
msgstr ""
|
||||
msgstr "使導航欄保持在頁面的頂部。"
|
||||
|
||||
#: .\cookbook\forms.py:81
|
||||
msgid ""
|
||||
"Both fields are optional. If none are given the username will be displayed "
|
||||
"instead"
|
||||
msgstr ""
|
||||
msgstr "這兩個字段都是可選的。如果沒有輸入,將顯示用戶名"
|
||||
|
||||
#: .\cookbook\forms.py:102 .\cookbook\forms.py:331
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:49
|
||||
#: .\cookbook\templates\url_import.html:154
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
msgstr "名字"
|
||||
|
||||
#: .\cookbook\forms.py:103 .\cookbook\forms.py:332
|
||||
#: .\cookbook\templates\base.html:105
|
||||
@@ -90,37 +95,37 @@ msgstr ""
|
||||
#: .\cookbook\templates\url_import.html:188
|
||||
#: .\cookbook\templates\url_import.html:573
|
||||
msgid "Keywords"
|
||||
msgstr ""
|
||||
msgstr "關鍵詞"
|
||||
|
||||
#: .\cookbook\forms.py:104
|
||||
msgid "Preparation time in minutes"
|
||||
msgstr ""
|
||||
msgstr "準備時間(分鐘)"
|
||||
|
||||
#: .\cookbook\forms.py:105
|
||||
msgid "Waiting time (cooking/baking) in minutes"
|
||||
msgstr ""
|
||||
msgstr "等候(烹飪、烘焙等)時間(分鐘)"
|
||||
|
||||
#: .\cookbook\forms.py:106 .\cookbook\forms.py:333
|
||||
msgid "Path"
|
||||
msgstr ""
|
||||
msgstr "路徑"
|
||||
|
||||
#: .\cookbook\forms.py:107
|
||||
msgid "Storage UID"
|
||||
msgstr ""
|
||||
msgstr "存儲ID"
|
||||
|
||||
#: .\cookbook\forms.py:133
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
msgstr "默認"
|
||||
|
||||
#: .\cookbook\forms.py:144 .\cookbook\templates\url_import.html:90
|
||||
msgid ""
|
||||
"To prevent duplicates recipes with the same name as existing ones are "
|
||||
"ignored. Check this box to import everything."
|
||||
msgstr ""
|
||||
msgstr "為防止重復,忽略與現有同名的菜譜。選中此框可導入所有內容(包括同名菜譜)。"
|
||||
|
||||
#: .\cookbook\forms.py:164
|
||||
msgid "New Unit"
|
||||
msgstr ""
|
||||
msgstr "新單位"
|
||||
|
||||
#: .\cookbook\forms.py:165
|
||||
msgid "New unit that other gets replaced by."
|
||||
@@ -128,15 +133,15 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:170
|
||||
msgid "Old Unit"
|
||||
msgstr ""
|
||||
msgstr "舊單位"
|
||||
|
||||
#: .\cookbook\forms.py:171
|
||||
msgid "Unit that should be replaced."
|
||||
msgstr ""
|
||||
msgstr "該被替換的單位。"
|
||||
|
||||
#: .\cookbook\forms.py:187
|
||||
msgid "New Food"
|
||||
msgstr ""
|
||||
msgstr "新食物"
|
||||
|
||||
#: .\cookbook\forms.py:188
|
||||
msgid "New food that other gets replaced by."
|
||||
@@ -144,85 +149,86 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:193
|
||||
msgid "Old Food"
|
||||
msgstr ""
|
||||
msgstr "舊食物"
|
||||
|
||||
#: .\cookbook\forms.py:194
|
||||
msgid "Food that should be replaced."
|
||||
msgstr ""
|
||||
msgstr "該被替換的食物。"
|
||||
|
||||
#: .\cookbook\forms.py:212
|
||||
msgid "Add your comment: "
|
||||
msgstr ""
|
||||
msgstr "發表評論。 "
|
||||
|
||||
#: .\cookbook\forms.py:253
|
||||
msgid "Leave empty for dropbox and enter app password for nextcloud."
|
||||
msgstr ""
|
||||
msgstr "Dropbox 留空並輸入 Nextcloud 應用密碼。"
|
||||
|
||||
#: .\cookbook\forms.py:260
|
||||
msgid "Leave empty for nextcloud and enter api token for dropbox."
|
||||
msgstr ""
|
||||
msgstr "Nextcloud 留空並輸入 Dropbox API 令牌。"
|
||||
|
||||
#: .\cookbook\forms.py:269
|
||||
msgid ""
|
||||
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
|
||||
"php/webdav/</code> is added automatically)"
|
||||
msgstr ""
|
||||
msgstr "Dropbox 留空並輸入基礎 Nextcloud 網址(<code>/remote.php/webdav/</code> "
|
||||
"會自動添加)"
|
||||
|
||||
#: .\cookbook\forms.py:307
|
||||
msgid "Search String"
|
||||
msgstr ""
|
||||
msgstr "搜索字符串"
|
||||
|
||||
#: .\cookbook\forms.py:334
|
||||
msgid "File ID"
|
||||
msgstr ""
|
||||
msgstr "文件編號"
|
||||
|
||||
#: .\cookbook\forms.py:370
|
||||
msgid "You must provide at least a recipe or a title."
|
||||
msgstr ""
|
||||
msgstr "你必須至少提供一份菜譜或一個標題。"
|
||||
|
||||
#: .\cookbook\forms.py:383
|
||||
msgid "You can list default users to share recipes with in the settings."
|
||||
msgstr ""
|
||||
msgstr "你可以在設置中列出默認用戶來分享菜譜。"
|
||||
|
||||
#: .\cookbook\forms.py:384
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:404
|
||||
msgid ""
|
||||
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
|
||||
"\">docs here</a>"
|
||||
msgstr ""
|
||||
msgstr "可以使用 Markdown 設置此字段格式。<a href=\"/docs/markdown/\">查看文檔</a>"
|
||||
|
||||
#: .\cookbook\forms.py:409
|
||||
msgid "Maximum number of users for this space reached."
|
||||
msgstr ""
|
||||
msgstr "已達到該空間的最大用戶數。"
|
||||
|
||||
#: .\cookbook\forms.py:415
|
||||
msgid "Email address already taken!"
|
||||
msgstr ""
|
||||
msgstr "電子郵件地址已被註冊!"
|
||||
|
||||
#: .\cookbook\forms.py:423
|
||||
msgid ""
|
||||
"An email address is not required but if present the invite link will be send "
|
||||
"to the user."
|
||||
msgstr ""
|
||||
msgstr "電子郵件地址不是必需的,但如果存在,邀請鏈接將被發送給用戶。"
|
||||
|
||||
#: .\cookbook\forms.py:438
|
||||
msgid "Name already taken."
|
||||
msgstr ""
|
||||
msgstr "名字已被占用。"
|
||||
|
||||
#: .\cookbook\forms.py:449
|
||||
msgid "Accept Terms and Privacy"
|
||||
msgstr ""
|
||||
msgstr "接受條款及隱私政策"
|
||||
|
||||
#: .\cookbook\helper\AllAuthCustomAdapter.py:30
|
||||
msgid ""
|
||||
"In order to prevent spam, the requested email was not send. Please wait a "
|
||||
"few minutes and try again."
|
||||
msgstr ""
|
||||
msgstr "為了防止垃圾郵件,所要求的電子郵件沒有被發送。請等待幾分鐘後再試。"
|
||||
|
||||
#: .\cookbook\helper\permission_helper.py:124
|
||||
#: .\cookbook\helper\permission_helper.py:144 .\cookbook\views\views.py:147
|
||||
msgid "You are not logged in and therefore cannot view this page!"
|
||||
msgstr ""
|
||||
msgstr "你还沒有登錄,因此不能查看這個頁面!"
|
||||
|
||||
#: .\cookbook\helper\permission_helper.py:127
|
||||
#: .\cookbook\helper\permission_helper.py:132
|
||||
@@ -234,18 +240,18 @@ msgstr ""
|
||||
#: .\cookbook\views\views.py:158 .\cookbook\views\views.py:165
|
||||
#: .\cookbook\views\views.py:253
|
||||
msgid "You do not have the required permissions to view this page!"
|
||||
msgstr ""
|
||||
msgstr "你沒有必要的權限來查看這個頁面!"
|
||||
|
||||
#: .\cookbook\helper\permission_helper.py:148
|
||||
#: .\cookbook\helper\permission_helper.py:170
|
||||
#: .\cookbook\helper\permission_helper.py:185
|
||||
msgid "You cannot interact with this object as it is not owned by you!"
|
||||
msgstr ""
|
||||
msgstr "你不能與此對象交互,因為它不屬於你!"
|
||||
|
||||
#: .\cookbook\helper\template_helper.py:60
|
||||
#: .\cookbook\helper\template_helper.py:62
|
||||
msgid "Could not parse template code."
|
||||
msgstr ""
|
||||
msgstr "無法解析模板代碼。"
|
||||
|
||||
#: .\cookbook\integration\integration.py:102
|
||||
#: .\cookbook\templates\import.html:14 .\cookbook\templates\import.html:20
|
||||
@@ -258,40 +264,40 @@ msgstr ""
|
||||
#: .\cookbook\templates\url_import.html:604 .\cookbook\views\delete.py:60
|
||||
#: .\cookbook\views\edit.py:199
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
msgstr "導入"
|
||||
|
||||
#: .\cookbook\integration\integration.py:162
|
||||
msgid ""
|
||||
"Importer expected a .zip file. Did you choose the correct importer type for "
|
||||
"your data ?"
|
||||
msgstr ""
|
||||
msgstr "導入需要一個 .zip 文件。你是否為數據選擇了正確的導入器類型?"
|
||||
|
||||
#: .\cookbook\integration\integration.py:165
|
||||
msgid ""
|
||||
"An unexpected error occurred during the import. Please make sure you have "
|
||||
"uploaded a valid file."
|
||||
msgstr ""
|
||||
msgstr "在導入過程中發生了一個意外的錯誤。請確認你上傳的文件是否有效。"
|
||||
|
||||
#: .\cookbook\integration\integration.py:169
|
||||
msgid "The following recipes were ignored because they already existed:"
|
||||
msgstr ""
|
||||
msgstr "以下菜譜被忽略了,因為它們已經存在了:"
|
||||
|
||||
#: .\cookbook\integration\integration.py:173
|
||||
#, python-format
|
||||
msgid "Imported %s recipes."
|
||||
msgstr ""
|
||||
msgstr "導入了%s菜譜。"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:46
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
msgstr "說明"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:49
|
||||
msgid "Nutritional Information"
|
||||
msgstr ""
|
||||
msgstr "營養信息"
|
||||
|
||||
#: .\cookbook\integration\paprika.py:53
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
msgstr "來源"
|
||||
|
||||
#: .\cookbook\integration\safron.py:23
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:79
|
||||
@@ -299,101 +305,101 @@ msgstr ""
|
||||
#: .\cookbook\templates\url_import.html:224
|
||||
#: .\cookbook\templates\url_import.html:455
|
||||
msgid "Servings"
|
||||
msgstr ""
|
||||
msgstr "份量"
|
||||
|
||||
#: .\cookbook\integration\safron.py:25
|
||||
msgid "Waiting time"
|
||||
msgstr ""
|
||||
msgstr "等待時間"
|
||||
|
||||
#: .\cookbook\integration\safron.py:27
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:73
|
||||
msgid "Preparation Time"
|
||||
msgstr ""
|
||||
msgstr "準備時間"
|
||||
|
||||
#: .\cookbook\integration\safron.py:29 .\cookbook\templates\base.html:78
|
||||
#: .\cookbook\templates\forms\ingredients.html:7
|
||||
#: .\cookbook\templates\index.html:7
|
||||
msgid "Cookbook"
|
||||
msgstr ""
|
||||
msgstr "菜譜"
|
||||
|
||||
#: .\cookbook\integration\safron.py:31
|
||||
msgid "Section"
|
||||
msgstr ""
|
||||
msgstr "部分"
|
||||
|
||||
#: .\cookbook\migrations\0047_auto_20200602_1133.py:14
|
||||
msgid "Breakfast"
|
||||
msgstr ""
|
||||
msgstr "早餐"
|
||||
|
||||
#: .\cookbook\migrations\0047_auto_20200602_1133.py:19
|
||||
msgid "Lunch"
|
||||
msgstr ""
|
||||
msgstr "午餐"
|
||||
|
||||
#: .\cookbook\migrations\0047_auto_20200602_1133.py:24
|
||||
msgid "Dinner"
|
||||
msgstr ""
|
||||
msgstr "晚餐"
|
||||
|
||||
#: .\cookbook\migrations\0047_auto_20200602_1133.py:29
|
||||
msgid "Other"
|
||||
msgstr ""
|
||||
msgstr "其他"
|
||||
|
||||
#: .\cookbook\models.py:71
|
||||
msgid ""
|
||||
"Maximum file storage for space in MB. 0 for unlimited, -1 to disable file "
|
||||
"upload."
|
||||
msgstr ""
|
||||
msgstr "空間的最大文件存儲量,單位為 MB。0表示無限製,-1表示禁止上傳文件。"
|
||||
|
||||
#: .\cookbook\models.py:121 .\cookbook\templates\search.html:7
|
||||
#: .\cookbook\templates\shopping_list.html:52
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
msgstr "搜索"
|
||||
|
||||
#: .\cookbook\models.py:122 .\cookbook\templates\base.html:92
|
||||
#: .\cookbook\templates\meal_plan.html:5 .\cookbook\views\delete.py:152
|
||||
#: .\cookbook\views\edit.py:233 .\cookbook\views\new.py:201
|
||||
msgid "Meal-Plan"
|
||||
msgstr ""
|
||||
msgstr "膳食計劃"
|
||||
|
||||
#: .\cookbook\models.py:123 .\cookbook\templates\base.html:89
|
||||
msgid "Books"
|
||||
msgstr ""
|
||||
msgstr "書籍"
|
||||
|
||||
#: .\cookbook\models.py:131
|
||||
msgid "Small"
|
||||
msgstr ""
|
||||
msgstr "小"
|
||||
|
||||
#: .\cookbook\models.py:131
|
||||
msgid "Large"
|
||||
msgstr ""
|
||||
msgstr "大"
|
||||
|
||||
#: .\cookbook\models.py:131 .\cookbook\templates\generic\new_template.html:6
|
||||
#: .\cookbook\templates\generic\new_template.html:14
|
||||
#: .\cookbook\templates\meal_plan.html:323
|
||||
msgid "New"
|
||||
msgstr ""
|
||||
msgstr "新"
|
||||
|
||||
#: .\cookbook\models.py:340
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:202
|
||||
msgid "Text"
|
||||
msgstr ""
|
||||
msgstr "文本"
|
||||
|
||||
#: .\cookbook\models.py:340
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:203
|
||||
msgid "Time"
|
||||
msgstr ""
|
||||
msgstr "時間"
|
||||
|
||||
#: .\cookbook\models.py:340
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:204
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:218
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
msgstr "文件"
|
||||
|
||||
#: .\cookbook\serializer.py:109
|
||||
msgid "File uploads are not enabled for this Space."
|
||||
msgstr ""
|
||||
msgstr "未為此空間啟用文件上傳。"
|
||||
|
||||
#: .\cookbook\serializer.py:117
|
||||
msgid "You have reached your file upload limit."
|
||||
msgstr ""
|
||||
msgstr "你已達到文件上傳的限製。"
|
||||
|
||||
#: .\cookbook\tables.py:35 .\cookbook\templates\books.html:36
|
||||
#: .\cookbook\templates\generic\edit_template.html:6
|
||||
@@ -403,7 +409,7 @@ msgstr ""
|
||||
#: .\cookbook\templates\shopping_list.html:33
|
||||
#: .\cookbook\templates\space.html:84
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
msgstr "編輯"
|
||||
|
||||
#: .\cookbook\tables.py:124 .\cookbook\tables.py:147
|
||||
#: .\cookbook\templates\books.html:38
|
||||
@@ -413,28 +419,28 @@ msgstr ""
|
||||
#: .\cookbook\templates\meal_plan.html:277
|
||||
#: .\cookbook\templates\recipes_table.html:90
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
msgstr "刪除"
|
||||
|
||||
#: .\cookbook\templates\404.html:5
|
||||
msgid "404 Error"
|
||||
msgstr ""
|
||||
msgstr "404錯誤"
|
||||
|
||||
#: .\cookbook\templates\404.html:18
|
||||
msgid "The page you are looking for could not be found."
|
||||
msgstr ""
|
||||
msgstr "找不到你要找的頁面。"
|
||||
|
||||
#: .\cookbook\templates\404.html:33
|
||||
msgid "Take me Home"
|
||||
msgstr ""
|
||||
msgstr "回到主頁"
|
||||
|
||||
#: .\cookbook\templates\404.html:35
|
||||
msgid "Report a Bug"
|
||||
msgstr ""
|
||||
msgstr "報告一個錯誤"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:6
|
||||
#: .\cookbook\templates\account\email.html:9
|
||||
msgid "E-mail Addresses"
|
||||
msgstr ""
|
||||
msgstr "電子郵件地址"
|
||||
|
||||
#: .\cookbook\templates\account\email.html:11
|
||||
msgid "The following e-mail addresses are associated with your account:"
|
||||
@@ -1769,7 +1775,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\space.html:100
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
msgstr "用戶"
|
||||
|
||||
#: .\cookbook\templates\space.html:101
|
||||
msgid "guest"
|
||||
|
||||
@@ -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)
|
||||
@@ -366,7 +368,7 @@ class UserPreference(models.Model, PermissionModelMixin):
|
||||
)
|
||||
|
||||
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True,blank=True, related_name='user_image')
|
||||
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='user_image')
|
||||
theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
|
||||
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
|
||||
default_unit = models.CharField(max_length=32, default='g')
|
||||
@@ -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 is not None 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']
|
||||
@@ -1202,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)
|
||||
|
||||
@@ -1212,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):
|
||||
|
||||
10
cookbook/static/css/app.min.css
vendored
10
cookbook/static/css/app.min.css
vendored
@@ -2,6 +2,16 @@
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.two-row-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2; /* number of lines to show */
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.menu-dropdown-text {
|
||||
font-size: 14px;
|
||||
|
||||
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 %}
|
||||
|
||||
@@ -33,7 +48,7 @@
|
||||
{% endfor %}
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="d-print-none">
|
||||
<div class="d-print-none" style="padding-bottom: 60px">
|
||||
|
||||
<form method="POST" class="post-form">
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -10,11 +10,24 @@
|
||||
|
||||
{% block content_fluid %}
|
||||
|
||||
{{ data }}
|
||||
<div id="app">
|
||||
<test-view></test-view>
|
||||
</div>
|
||||
|
||||
{% 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)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import io
|
||||
import json
|
||||
import mimetypes
|
||||
import pathlib
|
||||
import re
|
||||
import threading
|
||||
import traceback
|
||||
@@ -56,7 +57,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner,
|
||||
CustomIsSpaceOwner, CustomIsUser, group_required,
|
||||
is_space_owner, switch_user_active_space, above_space_limit, CustomRecipePermission, CustomUserPermission, CustomTokenHasReadWriteScope, CustomTokenHasScope, has_group_permission)
|
||||
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch
|
||||
from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup
|
||||
from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup, clean_dict
|
||||
from cookbook.helper.scrapers.scrapers import text_scraper
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
|
||||
from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food,
|
||||
@@ -87,7 +88,7 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
|
||||
SupermarketCategorySerializer, SupermarketSerializer,
|
||||
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
||||
UserFileSerializer, UserSerializer, UserPreferenceSerializer,
|
||||
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer)
|
||||
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer, RecipeExportSerializer)
|
||||
from cookbook.views.import_export import get_integration
|
||||
from recipes import settings
|
||||
|
||||
@@ -533,6 +534,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 +661,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 +858,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:
|
||||
@@ -1116,16 +1122,17 @@ class CustomAuthToken(ObtainAuthToken):
|
||||
context={'request': request})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = serializer.validated_data['user']
|
||||
if token := AccessToken.objects.filter(scope__contains='read').filter(scope__contains='write').first():
|
||||
if token := AccessToken.objects.filter(user=user, expires__gt=timezone.now(), scope__contains='read').filter(scope__contains='write').first():
|
||||
access_token = token
|
||||
else:
|
||||
access_token = AccessToken.objects.create(user=request.user, token=f'tda_{str(uuid.uuid4()).replace("-", "_")}', expires=(timezone.now() + timezone.timedelta(days=365 * 5)), scope='read write app')
|
||||
access_token = AccessToken.objects.create(user=user, token=f'tda_{str(uuid.uuid4()).replace("-", "_")}', expires=(timezone.now() + timezone.timedelta(days=365 * 5)), scope='read write app')
|
||||
return Response({
|
||||
'id': access_token.id,
|
||||
'token': access_token.token,
|
||||
'scope': access_token.scope,
|
||||
'expires': access_token.expires,
|
||||
'user_id': user.pk,
|
||||
'user_id': access_token.user.pk,
|
||||
'test': user.pk
|
||||
})
|
||||
|
||||
|
||||
@@ -1168,6 +1175,18 @@ def recipe_from_source(request):
|
||||
# 'recipe_html': '',
|
||||
'recipe_images': [],
|
||||
}, status=status.HTTP_200_OK)
|
||||
if re.match('^(.)*/view/recipe/[0-9]+/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', url):
|
||||
recipe_json = requests.get(url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1], '') + '?share=' + re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
|
||||
recipe_json = clean_dict(recipe_json, 'id')
|
||||
serialized_recipe = RecipeExportSerializer(data=recipe_json, context={'request': request})
|
||||
if serialized_recipe.is_valid():
|
||||
recipe = serialized_recipe.save()
|
||||
recipe.image = File(handle_image(request, File(io.BytesIO(requests.get(recipe_json['image']).content), name='image'), filetype=pathlib.Path(recipe_json['image']).suffix),
|
||||
name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}')
|
||||
recipe.save()
|
||||
return Response({
|
||||
'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
try:
|
||||
if validators.url(url, public=True):
|
||||
@@ -1305,6 +1324,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):
|
||||
@@ -1381,13 +1402,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')
|
||||
|
||||
@@ -438,7 +438,7 @@ def test(request):
|
||||
parser = IngredientParser(request, False)
|
||||
|
||||
data = {
|
||||
'original': '1 Porreestange(n) , ca. 200 g'
|
||||
'original': '90g golden syrup'
|
||||
}
|
||||
data['parsed'] = parser.parse(data['original'])
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
24
docs/faq.md
24
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.
|
||||
|
||||
@@ -86,3 +95,14 @@ To create a superuser you need to
|
||||
1. execute into the container using `docker-compose exec web_recipes sh`
|
||||
2. activate the virtual environment `source venv/bin/activate`
|
||||
3. run `python manage.py createsuperuser` and follow the steps shown.
|
||||
|
||||
|
||||
## Why cant I get support for my manual setup?
|
||||
Even tough I would love to help everyone get tandoor up and running I have only so much time
|
||||
that I can spend on this project besides work, family and other life things.
|
||||
Due to the countless problems that can occur when manually installing I simply do not have
|
||||
the time to help solving each one.
|
||||
|
||||
You can install Tandoor manually but please do not expect me or anyone to help you with that.
|
||||
As a general advice: If you do it manually do NOT change anything at first and slowly work yourself
|
||||
to your dream setup.
|
||||
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'
|
||||
|
||||
12
node_modules/.yarn-integrity
generated
vendored
Normal file
12
node_modules/.yarn-integrity
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"systemParams": "win32-x64-93",
|
||||
"modulesFolders": [
|
||||
"node_modules"
|
||||
],
|
||||
"flags": [],
|
||||
"linkedModules": [],
|
||||
"topLevelPatterns": [],
|
||||
"lockfileEntries": {},
|
||||
"files": [],
|
||||
"artifacts": {}
|
||||
}
|
||||
@@ -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 ""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user