mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-25 11:19:39 -05:00
Compare commits
536 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
018e9ef88f | ||
|
|
7d99a9a9c3 | ||
|
|
c4078800e3 | ||
|
|
d87f0f3c15 | ||
|
|
6e9b504a9d | ||
|
|
7397210729 | ||
|
|
7830ddd4e9 | ||
|
|
b711ee5257 | ||
|
|
c7b6253e04 | ||
|
|
85dcb6c61f | ||
|
|
323ded1814 | ||
|
|
595de0c5a1 | ||
|
|
8c3195937b | ||
|
|
8a8be7fb2d | ||
|
|
ec68da051d | ||
|
|
29f13d687c | ||
|
|
980e83b23c | ||
|
|
668ccf89fd | ||
|
|
93f7da3ed9 | ||
|
|
3f88778013 | ||
|
|
499d026b5c | ||
|
|
e0a1189430 | ||
|
|
e2905eb999 | ||
|
|
8b6f2c1e70 | ||
|
|
b7e4e53519 | ||
|
|
80eee255f7 | ||
|
|
60a4a63f56 | ||
|
|
82b80e60e6 | ||
|
|
915d0359bf | ||
|
|
10f1a77c1c | ||
|
|
ea141577d0 | ||
|
|
d0a1151a33 | ||
|
|
bc461997f8 | ||
|
|
d8051203c1 | ||
|
|
af71407ca6 | ||
|
|
cb9a90d1e7 | ||
|
|
4a4e4719b3 | ||
|
|
a0ff489be0 | ||
|
|
147aae318a | ||
|
|
d8e61a485e | ||
|
|
c2be329495 | ||
|
|
2552d27f6f | ||
|
|
9db5e45c26 | ||
|
|
74f88eb952 | ||
|
|
9a0a99a21f | ||
|
|
b35b731b6d | ||
|
|
e8d2b95aaa | ||
|
|
6be1ddfe87 | ||
|
|
5b518d4a4c | ||
|
|
1c6db468e1 | ||
|
|
519e36379b | ||
|
|
4726598deb | ||
|
|
1e57e7e70b | ||
|
|
6f1777d37d | ||
|
|
d6d9066eea | ||
|
|
2c7e7f859b | ||
|
|
97e5d23d98 | ||
|
|
a8cbef7bd4 | ||
|
|
569143a7ee | ||
|
|
3e8f0c3aae | ||
|
|
11620ba2b6 | ||
|
|
073ee7e963 | ||
|
|
9620689bd0 | ||
|
|
5a145d7f8e | ||
|
|
0ba2fa296a | ||
|
|
2a5fc22dd7 | ||
|
|
e739c4d627 | ||
|
|
87066c5d93 | ||
|
|
39ab2eb10f | ||
|
|
048f12948d | ||
|
|
ed4a46d585 | ||
|
|
4857a853b3 | ||
|
|
cfda0a17b1 | ||
|
|
84a1c560cc | ||
|
|
cf8e130bb8 | ||
|
|
33070e3c51 | ||
|
|
d4a646c973 | ||
|
|
38ad546634 | ||
|
|
001094afa5 | ||
|
|
dd4e170fb0 | ||
|
|
10b3b6fe1e | ||
|
|
c15e88a3d3 | ||
|
|
673ccb5024 | ||
|
|
7297cb5c3f | ||
|
|
f995c44d0b | ||
|
|
f81a7479c7 | ||
|
|
f5ab723ac2 | ||
|
|
2dddc79a47 | ||
|
|
b28cd881de | ||
|
|
b013efadda | ||
|
|
a61566063b | ||
|
|
9f6ec38ac5 | ||
|
|
5110b975e9 | ||
|
|
56ad93cdb3 | ||
|
|
fb7d1d94ab | ||
|
|
78de1c2bc2 | ||
|
|
8ad21b68ef | ||
|
|
58f7d02460 | ||
|
|
7c641af280 | ||
|
|
8ebb62188b | ||
|
|
a1b8f736c2 | ||
|
|
0347ff5304 | ||
|
|
037f38ac6b | ||
|
|
5370e67444 | ||
|
|
87db9124d0 | ||
|
|
8476b5e01f | ||
|
|
f1eb553487 | ||
|
|
4378a6b0c7 | ||
|
|
d5ca8e9c96 | ||
|
|
bbcf7ba8a7 | ||
|
|
f29f77a1d5 | ||
|
|
a3f8b2272c | ||
|
|
008a61823d | ||
|
|
a60f1a3e92 | ||
|
|
ebb6e669e2 | ||
|
|
fdd61d3caf | ||
|
|
72ac272962 | ||
|
|
4395737cc5 | ||
|
|
f3aef2c10b | ||
|
|
df1b75d88a | ||
|
|
0b5fb69664 | ||
|
|
ff2a75476b | ||
|
|
ea515c199c | ||
|
|
12f0cdb484 | ||
|
|
f2cd220e22 | ||
|
|
a0a3629e4c | ||
|
|
8263c6b725 | ||
|
|
b8b3620ade | ||
|
|
e55faa02d5 | ||
|
|
99fc0d1f81 | ||
|
|
19fe7ce214 | ||
|
|
ca26588c32 | ||
|
|
e2c807e303 | ||
|
|
64efadfc81 | ||
|
|
fb90eede52 | ||
|
|
48fda987fb | ||
|
|
8e85fd57b6 | ||
|
|
3f475aed03 | ||
|
|
12a11766d9 | ||
|
|
0e90700ce9 | ||
|
|
b7f202d645 | ||
|
|
f0f12ca83f | ||
|
|
b14d8f0051 | ||
|
|
5fd8c56324 | ||
|
|
8abef1d8cc | ||
|
|
0c8c74c0ac | ||
|
|
0b40414d23 | ||
|
|
d4b8190f55 | ||
|
|
0ce7ea0b61 | ||
|
|
817c4cb9d6 | ||
|
|
d05b894d69 | ||
|
|
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 | ||
|
|
1cbc74761a | ||
|
|
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 | ||
|
|
bce44866c2 | ||
|
|
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 | ||
|
|
f43ef3ad59 | ||
|
|
4c71c5b088 | ||
|
|
54d0b70f01 | ||
|
|
5a0f07a6b2 | ||
|
|
a4bf967f65 | ||
|
|
77feb0db3a | ||
|
|
33c634c0e2 | ||
|
|
be24e25ae4 | ||
|
|
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 | ||
|
|
33e5bb7d0a | ||
|
|
0cf63cd715 | ||
|
|
5dc7bf5b0e | ||
|
|
c4c66aa640 | ||
|
|
f64be72a98 | ||
|
|
a3ed2bdcac | ||
|
|
996b8bedac | ||
|
|
a05a785e22 | ||
|
|
b470602317 | ||
|
|
cf8ab02d0e | ||
|
|
60043fff59 | ||
|
|
ca9c96647e | ||
|
|
70e6585669 | ||
|
|
d0cb7a79f9 | ||
|
|
cfd24de72a | ||
|
|
54acfe3e39 | ||
|
|
12ad6af8c3 | ||
|
|
cf24e1014a | ||
|
|
2f8b479fdd |
@@ -2,6 +2,7 @@
|
||||
# when unset: 1 (true) - dont unset this, just for development
|
||||
DEBUG=0
|
||||
SQL_DEBUG=0
|
||||
DEBUG_TOOLBAR=0
|
||||
|
||||
# HTTP port to bind to
|
||||
# TANDOOR_PORT=8080
|
||||
@@ -97,7 +98,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 +127,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 ?
|
||||
@@ -157,6 +158,7 @@ REVERSE_PROXY_AUTH=0
|
||||
#AUTH_LDAP_BIND_PASSWORD=
|
||||
#AUTH_LDAP_USER_SEARCH_BASE_DN=
|
||||
#AUTH_LDAP_TLS_CACERTFILE=
|
||||
#AUTH_LDAP_START_TLS=
|
||||
|
||||
# Enables exporting PDF (see export docs)
|
||||
# Disabled by default, uncomment to enable
|
||||
|
||||
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@@ -14,3 +14,8 @@ updates:
|
||||
directory: "/vue/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
|
||||
138
.github/workflows/build-docker.yml
vendored
Normal file
138
.github/workflows/build-docker.yml
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
name: Build Docker Container
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
name: Build ${{ matrix.name }} Container
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'TandoorRecipes'
|
||||
continue-on-error: ${{ matrix.continue-on-error }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Standard build config
|
||||
- name: Standard
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
suffix: ""
|
||||
continue-on-error: false
|
||||
# Raspi build config
|
||||
- name: Raspi
|
||||
dockerfile: Dockerfile-raspi
|
||||
platforms: linux/arm/v7
|
||||
suffix: "-raspi"
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get version number
|
||||
id: get_version
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" = refs/tags/* ]]; then
|
||||
echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||
elif [[ "$GITHUB_REF" = refs/heads/beta ]]; then
|
||||
echo VERSION=beta >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo VERSION=develop >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Update Version number
|
||||
- name: Update version file
|
||||
uses: DamianReeves/write-file-action@v1.2
|
||||
with:
|
||||
path: recipes/version.py
|
||||
contents: |
|
||||
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
|
||||
BUILD_REF = '${{ github.sha }}'
|
||||
write-mode: overwrite
|
||||
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: yarn
|
||||
cache-dependency-path: vue/yarn.lock
|
||||
- name: Install dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn build
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
vabene1111/recipes
|
||||
ghcr.io/TandoorRecipes/recipes
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=${{ matrix.suffix }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=ref,event=branch
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
pull: true
|
||||
push: ${{ github.secret_source == 'Actions' }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
notify-stable:
|
||||
name: Notify Stable
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-container
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
# Send stable discord notification
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: '🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}'
|
||||
|
||||
notify-beta:
|
||||
name: Notify Beta
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-container
|
||||
if: github.ref == 'refs/heads/beta'
|
||||
steps:
|
||||
# Send beta discord notification
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_BETA_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: '🚀 The BETA Image has been updated! 🥳'
|
||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on: [push]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -12,15 +12,15 @@ jobs:
|
||||
python-version: ['3.10']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
- 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
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
with:
|
||||
languages: python, javascript
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
languages: javascript, python
|
||||
|
||||
48
.github/workflows/docker-publish-beta-raspi.yml
vendored
48
.github/workflows/docker-publish-beta-raspi.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: publish beta raspi image docker
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'beta'
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository_owner == 'TandoorRecipes'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
# Update Version number
|
||||
- name: Update version file
|
||||
uses: DamianReeves/write-file-action@v1.0
|
||||
with:
|
||||
path: recipes/version.py
|
||||
contents: |
|
||||
VERSION_NUMBER = 'beta'
|
||||
BUILD_REF = '${{ github.sha }}'
|
||||
write-mode: overwrite
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- name: Install dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn install
|
||||
- name: Build dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn build
|
||||
# Build container
|
||||
- name: Build and publish image
|
||||
uses: ilteoood/docker_buildx@master
|
||||
with:
|
||||
publish: true
|
||||
imageName: vabene1111/recipes
|
||||
tag: beta-raspi
|
||||
dockerFile: Dockerfile-raspi
|
||||
platform: linux/arm/v7
|
||||
dockerUser: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
|
||||
# Send discord notification
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_BETA_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: '🚀 The BETA Image has been updated! 🥳'
|
||||
47
.github/workflows/docker-publish-beta.yml
vendored
47
.github/workflows/docker-publish-beta.yml
vendored
@@ -1,47 +0,0 @@
|
||||
name: publish beta image docker
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'beta'
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository_owner == 'TandoorRecipes'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
# Update Version number
|
||||
- name: Update version file
|
||||
uses: DamianReeves/write-file-action@v1.0
|
||||
with:
|
||||
path: recipes/version.py
|
||||
contents: |
|
||||
VERSION_NUMBER = 'beta'
|
||||
BUILD_REF = '${{ github.sha }}'
|
||||
write-mode: overwrite
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- name: Install dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn install
|
||||
- name: Build dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn build
|
||||
# Build container
|
||||
- name: Build and publish image
|
||||
uses: ilteoood/docker_buildx@master
|
||||
with:
|
||||
publish: true
|
||||
imageName: vabene1111/recipes
|
||||
tag: beta
|
||||
platform: linux/amd64,linux/arm64
|
||||
dockerUser: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
|
||||
# Send discord notification
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_BETA_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: '🚀 The BETA Image has been updated! 🥳'
|
||||
42
.github/workflows/docker-publish-dev.yml
vendored
42
.github/workflows/docker-publish-dev.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: publish dev image docker
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
- '*/*'
|
||||
- '!master'
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository_owner == 'TandoorRecipes'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
# Update Version number
|
||||
- name: Update version file
|
||||
uses: DamianReeves/write-file-action@v1.0
|
||||
with:
|
||||
path: recipes/version.py
|
||||
contents: |
|
||||
VERSION_NUMBER = 'develop'
|
||||
BUILD_REF = '${{ github.sha }}'
|
||||
write-mode: overwrite
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- name: Clear Cache
|
||||
working-directory: ./vue
|
||||
run: yarn cache clean --all
|
||||
- name: Install dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn install
|
||||
- name: Build dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn build
|
||||
# Build container
|
||||
- name: Publish to Registry
|
||||
uses: elgohr/Publish-Docker-Github-Action@2.13
|
||||
with:
|
||||
name: vabene1111/recipes
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -1,45 +0,0 @@
|
||||
name: publish latest raspi image docker
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository_owner == 'TandoorRecipes'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Get version number
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}-raspi
|
||||
# Update Version number
|
||||
- name: Update version file
|
||||
uses: DamianReeves/write-file-action@v1.0
|
||||
with:
|
||||
path: recipes/version.py
|
||||
contents: |
|
||||
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}-raspi'
|
||||
BUILD_REF = '${{ github.sha }}'
|
||||
write-mode: overwrite
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- name: Install dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn install
|
||||
- name: Build dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn build
|
||||
# Build container
|
||||
- name: Build and publish image
|
||||
uses: ilteoood/docker_buildx@master
|
||||
with:
|
||||
publish: true
|
||||
imageName: vabene1111/recipes
|
||||
dockerFile: Dockerfile-raspi
|
||||
platform: linux/arm/v7
|
||||
tag: latest-raspi
|
||||
dockerUser: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
|
||||
44
.github/workflows/docker-publish-latest.yml
vendored
44
.github/workflows/docker-publish-latest.yml
vendored
@@ -1,44 +0,0 @@
|
||||
name: publish latest image docker
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository_owner == 'TandoorRecipes'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Get version number
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
||||
# Update Version number
|
||||
- name: Update version file
|
||||
uses: DamianReeves/write-file-action@v1.0
|
||||
with:
|
||||
path: recipes/version.py
|
||||
contents: |
|
||||
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
|
||||
BUILD_REF = '${{ github.sha }}'
|
||||
write-mode: overwrite
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- name: Install dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn install
|
||||
- name: Build dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn build
|
||||
# Build container
|
||||
- name: Build and publish image
|
||||
uses: ilteoood/docker_buildx@master
|
||||
with:
|
||||
publish: true
|
||||
imageName: vabene1111/recipes
|
||||
platform: linux/amd64,linux/arm64
|
||||
tag: latest
|
||||
dockerUser: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -1,47 +0,0 @@
|
||||
name: publish tagged raspi release docker
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository_owner == 'TandoorRecipes'
|
||||
runs-on: ubuntu-latest
|
||||
name: Build image job
|
||||
steps:
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@master
|
||||
- name: Get version number
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
||||
# Update Version number
|
||||
- name: Update version file
|
||||
uses: DamianReeves/write-file-action@v1.0
|
||||
with:
|
||||
path: recipes/version.py
|
||||
contents: |
|
||||
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
|
||||
BUILD_REF = '${{ github.sha }}'
|
||||
write-mode: overwrite
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- name: Install dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn install
|
||||
- name: Build dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn build
|
||||
# Build container
|
||||
- name: Build and publish image
|
||||
uses: ilteoood/docker_buildx@master
|
||||
with:
|
||||
publish: true
|
||||
imageName: vabene1111/recipes
|
||||
dockerFile: Dockerfile-raspi
|
||||
platform: linux/arm/v7
|
||||
tag: ${{ steps.get_version.outputs.VERSION }}-raspi
|
||||
dockerUser: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
|
||||
53
.github/workflows/docker-publish-release.yml
vendored
53
.github/workflows/docker-publish-release.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: publish tagged release docker
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository_owner == 'TandoorRecipes'
|
||||
runs-on: ubuntu-latest
|
||||
name: Build image job
|
||||
steps:
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@master
|
||||
- name: Get version number
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
||||
# Update Version number
|
||||
- name: Update version file
|
||||
uses: DamianReeves/write-file-action@v1.0
|
||||
with:
|
||||
path: recipes/version.py
|
||||
contents: |
|
||||
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
|
||||
BUILD_REF = '${{ github.sha }}'
|
||||
write-mode: overwrite
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- name: Install dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn install
|
||||
- name: Build dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn build
|
||||
# Build container
|
||||
- name: Build and publish image
|
||||
uses: ilteoood/docker_buildx@master
|
||||
with:
|
||||
publish: true
|
||||
imageName: vabene1111/recipes
|
||||
platform: linux/amd64,linux/arm64
|
||||
tag: ${{ steps.get_version.outputs.VERSION }}
|
||||
dockerUser: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
|
||||
# Send discord notification
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: '🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}'
|
||||
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
@@ -9,8 +9,8 @@ jobs:
|
||||
if: github.repository_owner == 'TandoorRecipes'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: pip install mkdocs-material mkdocs-include-markdown-plugin
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -78,9 +78,11 @@ postgresql/
|
||||
|
||||
/docker-compose.override.yml
|
||||
vue/node_modules
|
||||
plugins
|
||||
.vscode/
|
||||
vetur.config.js
|
||||
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="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
|
||||
@@ -71,8 +71,7 @@ Because of that there are several ways you can support us
|
||||
- **Let us host for you** We are offering a [hosted version](https://app.tandoor.dev) where all profits support us and the development of tandoor (currently only available in germany).
|
||||
|
||||
## Contributing
|
||||
|
||||
You can help out with the ongoing development by looking for potential bugs in our code base, or by contributing new features. We are always welcoming new pull requests containing bug fixes, refactors and new features. We have a list of tasks and bugs on our issue tracker on Github. Please comment on issues if you want to contribute with, to avoid duplicating effort.
|
||||
Contributions are welcome but please read [this](https://docs.tandoor.dev/contribute/#contributing-code) **BEFORE** contributing anything!
|
||||
|
||||
## Your Feedback
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -3,9 +3,9 @@ from collections import Counter
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
|
||||
from django.core.cache import cache
|
||||
from django.core.cache import caches
|
||||
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value, When, FilteredRelation)
|
||||
from django.core.cache import cache, caches
|
||||
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value,
|
||||
When)
|
||||
from django.db.models.functions import Coalesce, Lower, Substr
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.translation import gettext as _
|
||||
@@ -20,7 +20,8 @@ from recipes import settings
|
||||
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
|
||||
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
||||
class RecipeSearch():
|
||||
_postgres = settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
|
||||
_postgres = settings.DATABASES['default']['ENGINE'] in [
|
||||
'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
|
||||
|
||||
def __init__(self, request, **params):
|
||||
self._request = request
|
||||
@@ -45,7 +46,8 @@ class RecipeSearch():
|
||||
cache.set(CACHE_KEY, self._search_prefs, timeout=10)
|
||||
else:
|
||||
self._search_prefs = SearchPreference()
|
||||
self._string = self._params.get('query').strip() if self._params.get('query', None) else None
|
||||
self._string = self._params.get('query').strip(
|
||||
) if self._params.get('query', None) else None
|
||||
self._rating = self._params.get('rating', None)
|
||||
self._keywords = {
|
||||
'or': self._params.get('keywords_or', None) or self._params.get('keywords', None),
|
||||
@@ -74,7 +76,8 @@ class RecipeSearch():
|
||||
self._random = str2bool(self._params.get('random', False))
|
||||
self._new = str2bool(self._params.get('new', False))
|
||||
self._num_recent = int(self._params.get('num_recent', 0))
|
||||
self._include_children = str2bool(self._params.get('include_children', None))
|
||||
self._include_children = str2bool(
|
||||
self._params.get('include_children', None))
|
||||
self._timescooked = self._params.get('timescooked', None)
|
||||
self._cookedon = self._params.get('cookedon', None)
|
||||
self._createdon = self._params.get('createdon', None)
|
||||
@@ -95,18 +98,24 @@ class RecipeSearch():
|
||||
self._search_type = self._search_prefs.search or 'plain'
|
||||
if self._string:
|
||||
if self._postgres:
|
||||
self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
|
||||
self._unaccent_include = self._search_prefs.unaccent.values_list(
|
||||
'field', flat=True)
|
||||
else:
|
||||
self._unaccent_include = []
|
||||
self._icontains_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
|
||||
self._istartswith_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
|
||||
self._icontains_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
|
||||
self._istartswith_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
|
||||
self._trigram_include = None
|
||||
self._fulltext_include = None
|
||||
self._trigram = False
|
||||
if self._postgres and self._string:
|
||||
self._language = DICTIONARY.get(translation.get_language(), 'simple')
|
||||
self._trigram_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
|
||||
self._fulltext_include = self._search_prefs.fulltext.values_list('field', flat=True) or None
|
||||
self._language = DICTIONARY.get(
|
||||
translation.get_language(), 'simple')
|
||||
self._trigram_include = [
|
||||
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
|
||||
self._fulltext_include = self._search_prefs.fulltext.values_list(
|
||||
'field', flat=True) or None
|
||||
|
||||
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
|
||||
self._trigram = True
|
||||
@@ -182,8 +191,10 @@ class RecipeSearch():
|
||||
# otherwise sort by the remaining order_by attributes or favorite by default
|
||||
else:
|
||||
order += default_order
|
||||
order[:] = [Lower('name').asc() if x == 'name' else x for x in order]
|
||||
order[:] = [Lower('name').desc() if x == '-name' else x for x in order]
|
||||
order[:] = [Lower('name').asc() if x ==
|
||||
'name' else x for x in order]
|
||||
order[:] = [Lower('name').desc() if x ==
|
||||
'-name' else x for x in order]
|
||||
self.orderby = order
|
||||
|
||||
def string_filters(self, string=None):
|
||||
@@ -200,21 +211,28 @@ class RecipeSearch():
|
||||
for f in self._filters:
|
||||
query_filter |= f
|
||||
|
||||
self._queryset = self._queryset.filter(query_filter).distinct() # this creates duplicate records which can screw up other aggregates, see makenow for workaround
|
||||
# this creates duplicate records which can screw up other aggregates, see makenow for workaround
|
||||
self._queryset = self._queryset.filter(query_filter).distinct()
|
||||
if self._fulltext_include:
|
||||
if self._fuzzy_match is None:
|
||||
self._queryset = self._queryset.annotate(score=Coalesce(Max(self.search_rank), 0.0))
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=Coalesce(Max(self.search_rank), 0.0))
|
||||
else:
|
||||
self._queryset = self._queryset.annotate(rank=Coalesce(Max(self.search_rank), 0.0))
|
||||
self._queryset = self._queryset.annotate(
|
||||
rank=Coalesce(Max(self.search_rank), 0.0))
|
||||
|
||||
if self._fuzzy_match is not None:
|
||||
simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity')
|
||||
simularity = self._fuzzy_match.filter(
|
||||
pk=OuterRef('pk')).values('simularity')
|
||||
if not self._fulltext_include:
|
||||
self._queryset = self._queryset.annotate(score=Coalesce(Subquery(simularity), 0.0))
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=Coalesce(Subquery(simularity), 0.0))
|
||||
else:
|
||||
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
|
||||
self._queryset = self._queryset.annotate(
|
||||
simularity=Coalesce(Subquery(simularity), 0.0))
|
||||
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
|
||||
self._queryset = self._queryset.annotate(score=F('rank') + F('simularity'))
|
||||
self._queryset = self._queryset.annotate(
|
||||
score=F('rank') + F('simularity'))
|
||||
else:
|
||||
query_filter = Q()
|
||||
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
|
||||
@@ -223,7 +241,8 @@ class RecipeSearch():
|
||||
|
||||
def _cooked_on_filter(self, cooked_date=None):
|
||||
if self._sort_includes('lastcooked') or cooked_date:
|
||||
lessthan = self._sort_includes('-lastcooked') or '-' in (cooked_date or [])[:1]
|
||||
lessthan = self._sort_includes(
|
||||
'-lastcooked') or '-' in (cooked_date or [])[:1]
|
||||
if lessthan:
|
||||
default = timezone.now() - timedelta(days=100000)
|
||||
else:
|
||||
@@ -233,32 +252,41 @@ class RecipeSearch():
|
||||
if cooked_date is None:
|
||||
return
|
||||
|
||||
cooked_date = date(*[int(x) for x in cooked_date.split('-') if x != ''])
|
||||
cooked_date = date(*[int(x)
|
||||
for x in cooked_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
||||
self._queryset = self._queryset.filter(
|
||||
lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
|
||||
self._queryset = self._queryset.filter(
|
||||
lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
|
||||
|
||||
def _created_on_filter(self, created_date=None):
|
||||
if created_date is None:
|
||||
return
|
||||
lessthan = '-' in created_date[:1]
|
||||
created_date = date(*[int(x) for x in created_date.split('-') if x != ''])
|
||||
created_date = date(*[int(x)
|
||||
for x in created_date.split('-') if x != ''])
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(created_at__date__lte=created_date)
|
||||
self._queryset = self._queryset.filter(
|
||||
created_at__date__lte=created_date)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(created_at__date__gte=created_date)
|
||||
self._queryset = self._queryset.filter(
|
||||
created_at__date__gte=created_date)
|
||||
|
||||
def _updated_on_filter(self, updated_date=None):
|
||||
if updated_date is None:
|
||||
return
|
||||
lessthan = '-' in updated_date[:1]
|
||||
updated_date = date(*[int(x) for x in updated_date.split('-') if x != ''])
|
||||
updated_date = date(*[int(x)
|
||||
for x in updated_date.split('-') if x != ''])
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(updated_at__date__lte=updated_date)
|
||||
self._queryset = self._queryset.filter(
|
||||
updated_at__date__lte=updated_date)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(updated_at__date__gte=updated_date)
|
||||
self._queryset = self._queryset.filter(
|
||||
updated_at__date__gte=updated_date)
|
||||
|
||||
def _viewed_on_filter(self, viewed_date=None):
|
||||
if self._sort_includes('lastviewed') or viewed_date:
|
||||
@@ -268,12 +296,15 @@ class RecipeSearch():
|
||||
if viewed_date is None:
|
||||
return
|
||||
lessthan = '-' in viewed_date[:1]
|
||||
viewed_date = date(*[int(x) for x in viewed_date.split('-') if x != ''])
|
||||
viewed_date = date(*[int(x)
|
||||
for x in viewed_date.split('-') if x != ''])
|
||||
|
||||
if lessthan:
|
||||
self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
self._queryset = self._queryset.filter(
|
||||
lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
self._queryset = self._queryset.filter(
|
||||
lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
|
||||
|
||||
def _new_recipes(self, new_days=7):
|
||||
# TODO make new days a user-setting
|
||||
@@ -293,27 +324,32 @@ class RecipeSearch():
|
||||
|
||||
num_recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space).values(
|
||||
'recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
|
||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
||||
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(
|
||||
pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
|
||||
|
||||
def _favorite_recipes(self, times_cooked=None):
|
||||
if self._sort_includes('favorite') or times_cooked:
|
||||
less_than = '-' in (times_cooked or []) or not self._sort_includes('-favorite')
|
||||
less_than = '-' in (times_cooked or []
|
||||
) and not self._sort_includes('-favorite')
|
||||
if less_than:
|
||||
default = 1000
|
||||
else:
|
||||
default = 0
|
||||
favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk')
|
||||
).values('recipe').annotate(count=Count('pk', distinct=True)).values('count')
|
||||
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
self._queryset = self._queryset.annotate(
|
||||
favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
if times_cooked is None:
|
||||
return
|
||||
|
||||
if times_cooked == '0':
|
||||
self._queryset = self._queryset.filter(favorite=0)
|
||||
elif less_than:
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(times_cooked[1:])).exclude(favorite=0)
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(
|
||||
times_cooked.replace('-', ''))).exclude(favorite=0)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(favorite__gte=int(times_cooked))
|
||||
self._queryset = self._queryset.filter(
|
||||
favorite__gte=int(times_cooked))
|
||||
|
||||
def keyword_filters(self, **kwargs):
|
||||
if all([kwargs[x] is None for x in kwargs]):
|
||||
@@ -346,7 +382,8 @@ class RecipeSearch():
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_and)
|
||||
if 'not' in kw_filter:
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
|
||||
def food_filters(self, **kwargs):
|
||||
if all([kwargs[x] is None for x in kwargs]):
|
||||
@@ -360,7 +397,8 @@ class RecipeSearch():
|
||||
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
|
||||
if 'or' in fd_filter:
|
||||
if self._include_children:
|
||||
f_or = Q(steps__ingredients__food__in=Food.include_descendants(foods))
|
||||
f_or = Q(
|
||||
steps__ingredients__food__in=Food.include_descendants(foods))
|
||||
else:
|
||||
f_or = Q(steps__ingredients__food__in=foods)
|
||||
|
||||
@@ -372,7 +410,8 @@ class RecipeSearch():
|
||||
recipes = Recipe.objects.all()
|
||||
for food in foods:
|
||||
if self._include_children:
|
||||
f_and = Q(steps__ingredients__food__in=food.get_descendants_and_self())
|
||||
f_and = Q(
|
||||
steps__ingredients__food__in=food.get_descendants_and_self())
|
||||
else:
|
||||
f_and = Q(steps__ingredients__food=food)
|
||||
if 'not' in fd_filter:
|
||||
@@ -380,7 +419,8 @@ class RecipeSearch():
|
||||
else:
|
||||
self._queryset = self._queryset.filter(f_and)
|
||||
if 'not' in fd_filter:
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
|
||||
def unit_filters(self, units=None, operator=True):
|
||||
if operator != True:
|
||||
@@ -389,7 +429,8 @@ class RecipeSearch():
|
||||
return
|
||||
if not isinstance(units, list):
|
||||
units = [units]
|
||||
self._queryset = self._queryset.filter(steps__ingredients__unit__in=units)
|
||||
self._queryset = self._queryset.filter(
|
||||
steps__ingredients__unit__in=units)
|
||||
|
||||
def rating_filter(self, rating=None):
|
||||
if rating or self._sort_includes('rating'):
|
||||
@@ -399,14 +440,16 @@ class RecipeSearch():
|
||||
else:
|
||||
default = 0
|
||||
# TODO make ratings a settings user-only vs all-users
|
||||
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
|
||||
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(
|
||||
cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
|
||||
if rating is None:
|
||||
return
|
||||
|
||||
if rating == '0':
|
||||
self._queryset = self._queryset.filter(rating=0)
|
||||
elif lessthan:
|
||||
self._queryset = self._queryset.filter(rating__lte=int(rating[1:])).exclude(rating=0)
|
||||
self._queryset = self._queryset.filter(
|
||||
rating__lte=int(rating[1:])).exclude(rating=0)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(rating__gte=int(rating))
|
||||
|
||||
@@ -434,11 +477,14 @@ class RecipeSearch():
|
||||
recipes = Recipe.objects.all()
|
||||
for book in kwargs[bk_filter]:
|
||||
if 'not' in bk_filter:
|
||||
recipes = recipes.filter(recipebookentry__book__id=book)
|
||||
recipes = recipes.filter(
|
||||
recipebookentry__book__id=book)
|
||||
else:
|
||||
self._queryset = self._queryset.filter(recipebookentry__book__id=book)
|
||||
self._queryset = self._queryset.filter(
|
||||
recipebookentry__book__id=book)
|
||||
if 'not' in bk_filter:
|
||||
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
|
||||
self._queryset = self._queryset.exclude(
|
||||
id__in=recipes.values('id'))
|
||||
|
||||
def step_filters(self, steps=None, operator=True):
|
||||
if operator != True:
|
||||
@@ -446,7 +492,7 @@ class RecipeSearch():
|
||||
if not steps:
|
||||
return
|
||||
if not isinstance(steps, list):
|
||||
steps = [unistepsts]
|
||||
steps = [steps]
|
||||
self._queryset = self._queryset.filter(steps__id__in=steps)
|
||||
|
||||
def build_fulltext_filters(self, string=None):
|
||||
@@ -457,20 +503,25 @@ class RecipeSearch():
|
||||
rank = []
|
||||
if 'name' in self._fulltext_include:
|
||||
vectors.append('name_search_vector')
|
||||
rank.append(SearchRank('name_search_vector', self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('name_search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
if 'description' in self._fulltext_include:
|
||||
vectors.append('desc_search_vector')
|
||||
rank.append(SearchRank('desc_search_vector', self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('desc_search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
if 'steps__instruction' in self._fulltext_include:
|
||||
vectors.append('steps__search_vector')
|
||||
rank.append(SearchRank('steps__search_vector', self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('steps__search_vector',
|
||||
self.search_query, cover_density=True))
|
||||
if 'keywords__name' in self._fulltext_include:
|
||||
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
|
||||
vectors.append('keywords__name__unaccent')
|
||||
rank.append(SearchRank('keywords__name__unaccent', self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('keywords__name__unaccent',
|
||||
self.search_query, cover_density=True))
|
||||
if 'steps__ingredients__food__name' in self._fulltext_include:
|
||||
vectors.append('steps__ingredients__food__name__unaccent')
|
||||
rank.append(SearchRank('steps__ingredients__food__name', self.search_query, cover_density=True))
|
||||
rank.append(SearchRank('steps__ingredients__food__name',
|
||||
self.search_query, cover_density=True))
|
||||
|
||||
for r in rank:
|
||||
if self.search_rank is None:
|
||||
@@ -478,7 +529,8 @@ class RecipeSearch():
|
||||
else:
|
||||
self.search_rank += r
|
||||
# modifying queryset will annotation creates duplicate results
|
||||
self._filters.append(Q(id__in=Recipe.objects.annotate(vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
|
||||
self._filters.append(Q(id__in=Recipe.objects.annotate(
|
||||
vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
|
||||
|
||||
def build_text_filters(self, string=None):
|
||||
if not string:
|
||||
@@ -510,23 +562,30 @@ class RecipeSearch():
|
||||
def _makenow_filter(self, missing=None):
|
||||
if missing is None or (type(missing) == bool and missing == False):
|
||||
return
|
||||
shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
|
||||
shopping_users = [
|
||||
*self._request.user.get_shopping_share(), self._request.user]
|
||||
|
||||
onhand_filter = (
|
||||
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
|
||||
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users) # or substitute food onhand
|
||||
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
|
||||
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
||||
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
|
||||
# or substitute food onhand
|
||||
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users)
|
||||
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
|
||||
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
||||
)
|
||||
makenow_recipes = Recipe.objects.annotate(
|
||||
count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
|
||||
count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
|
||||
count_food=Count('steps__ingredients__food__pk', filter=Q(
|
||||
steps__ingredients__food__isnull=False), distinct=True),
|
||||
count_onhand=Count('steps__ingredients__food__pk',
|
||||
filter=onhand_filter, distinct=True),
|
||||
count_ignore_shopping=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True,
|
||||
steps__ingredients__food__recipe__isnull=True), distinct=True),
|
||||
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users), then=Value(1)), default=Value(0)),
|
||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)), default=Value(0))
|
||||
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(
|
||||
shopping_users), then=Value(1)), default=Value(0)),
|
||||
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(
|
||||
shopping_users), then=Value(1)), default=Value(0))
|
||||
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood=missing)
|
||||
self._queryset = self._queryset.distinct().filter(id__in=makenow_recipes.values('id'))
|
||||
self._queryset = self._queryset.distinct().filter(
|
||||
id__in=makenow_recipes.values('id'))
|
||||
|
||||
@staticmethod
|
||||
def __children_substitute_filter(shopping_users=None):
|
||||
@@ -547,7 +606,8 @@ class RecipeSearch():
|
||||
@staticmethod
|
||||
def __sibling_substitute_filter(shopping_users=None):
|
||||
sibling_onhand_subquery = Food.objects.filter(
|
||||
path__startswith=Substr(OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
|
||||
path__startswith=Substr(
|
||||
OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
|
||||
depth=OuterRef('depth'),
|
||||
onhand_users__in=shopping_users
|
||||
)
|
||||
@@ -586,7 +646,8 @@ class RecipeFacet():
|
||||
self.Recent = self._cache.get('Recent', None)
|
||||
|
||||
if self._queryset is not None:
|
||||
self._recipe_list = list(self._queryset.values_list('id', flat=True))
|
||||
self._recipe_list = list(
|
||||
self._queryset.values_list('id', flat=True))
|
||||
self._search_params = {
|
||||
'keyword_list': self._request.query_params.getlist('keywords', []),
|
||||
'food_list': self._request.query_params.getlist('foods', []),
|
||||
@@ -618,7 +679,8 @@ class RecipeFacet():
|
||||
'Books': self.Books
|
||||
|
||||
}
|
||||
caches['default'].set(self._SEARCH_CACHE_KEY, self._cache, self._cache_timeout)
|
||||
caches['default'].set(self._SEARCH_CACHE_KEY,
|
||||
self._cache, self._cache_timeout)
|
||||
|
||||
def get_facets(self, from_cache=False):
|
||||
if from_cache:
|
||||
@@ -655,13 +717,16 @@ class RecipeFacet():
|
||||
def get_keywords(self):
|
||||
if self.Keywords is None:
|
||||
if self._search_params['search_keywords_or']:
|
||||
keywords = Keyword.objects.filter(space=self._request.space).distinct()
|
||||
keywords = Keyword.objects.filter(
|
||||
space=self._request.space).distinct()
|
||||
else:
|
||||
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
|
||||
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(
|
||||
depth=1)).filter(space=self._request.space).distinct()
|
||||
|
||||
# set keywords to root objects only
|
||||
keywords = self._keyword_queryset(keywords)
|
||||
self.Keywords = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.Keywords = [{**x, 'children': None}
|
||||
if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.set_cache('Keywords', self.Keywords)
|
||||
return self.Keywords
|
||||
|
||||
@@ -669,28 +734,28 @@ class RecipeFacet():
|
||||
if self.Foods is None:
|
||||
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
||||
if self._search_params['search_foods_or']:
|
||||
foods = Food.objects.filter(space=self._request.space).distinct()
|
||||
foods = Food.objects.filter(
|
||||
space=self._request.space).distinct()
|
||||
else:
|
||||
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct()
|
||||
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(
|
||||
depth=1)).filter(space=self._request.space).distinct()
|
||||
|
||||
# set keywords to root objects only
|
||||
foods = self._food_queryset(foods)
|
||||
|
||||
self.Foods = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
||||
self.Foods = [{**x, 'children': None}
|
||||
if x['numchild'] > 0 else x for x in list(foods)]
|
||||
self.set_cache('Foods', self.Foods)
|
||||
return self.Foods
|
||||
|
||||
def get_books(self):
|
||||
if self.Books is None:
|
||||
self.Books = []
|
||||
return self.Books
|
||||
|
||||
def get_ratings(self):
|
||||
if self.Ratings is None:
|
||||
if not self._request.space.demo and self._request.space.show_facet_count:
|
||||
if self._queryset is None:
|
||||
self._queryset = Recipe.objects.filter(id__in=self._recipe_list)
|
||||
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
|
||||
self._queryset = Recipe.objects.filter(
|
||||
id__in=self._recipe_list)
|
||||
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(
|
||||
cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
|
||||
self.Ratings = dict(Counter(r.rating for r in rating_qs))
|
||||
else:
|
||||
self.Rating = {}
|
||||
@@ -715,10 +780,13 @@ class RecipeFacet():
|
||||
foods = self._food_queryset(food.get_children(), food)
|
||||
deep_search = self.Foods
|
||||
for node in nodes:
|
||||
index = next((i for i, x in enumerate(deep_search) if x["id"] == node.id), None)
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == node.id), None)
|
||||
deep_search = deep_search[index]['children']
|
||||
index = next((i for i, x in enumerate(deep_search) if x["id"] == food.id), None)
|
||||
deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == food.id), None)
|
||||
deep_search[index]['children'] = [
|
||||
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
|
||||
self.set_cache('Foods', self.Foods)
|
||||
return self.get_facets()
|
||||
|
||||
@@ -731,10 +799,13 @@ class RecipeFacet():
|
||||
keywords = self._keyword_queryset(keyword.get_children(), keyword)
|
||||
deep_search = self.Keywords
|
||||
for node in nodes:
|
||||
index = next((i for i, x in enumerate(deep_search) if x["id"] == node.id), None)
|
||||
index = next((i for i, x in enumerate(
|
||||
deep_search) if x["id"] == node.id), None)
|
||||
deep_search = deep_search[index]['children']
|
||||
index = next((i for i, x in enumerate(deep_search) if x["id"] == keyword.id), None)
|
||||
deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
index = next((i for i, x in enumerate(deep_search)
|
||||
if x["id"] == keyword.id), None)
|
||||
deep_search[index]['children'] = [
|
||||
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
|
||||
self.set_cache('Keywords', self.Keywords)
|
||||
return self.get_facets()
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import random
|
||||
# import random
|
||||
import re
|
||||
from html import unescape
|
||||
from unicodedata import decomposition
|
||||
|
||||
from django.core.cache import caches
|
||||
from django.utils.dateparse import parse_duration
|
||||
from django.utils.translation import gettext as _
|
||||
from isodate import parse_duration as iso_parse_duration
|
||||
@@ -10,9 +10,12 @@ from isodate.isoerror import ISO8601Error
|
||||
from pytube import YouTube
|
||||
from recipe_scrapers._utils import get_host_name, get_minutes
|
||||
|
||||
from cookbook.helper import recipe_url_import as helper
|
||||
# from cookbook.helper import recipe_url_import as helper
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.models import Keyword
|
||||
from cookbook.models import Automation, Keyword
|
||||
|
||||
# from unicodedata import decomposition
|
||||
|
||||
|
||||
# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
|
||||
|
||||
@@ -121,7 +124,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 Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space)
|
||||
@@ -139,42 +148,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
|
||||
|
||||
|
||||
@@ -224,10 +249,27 @@ def parse_description(description):
|
||||
|
||||
|
||||
def clean_instruction_string(instruction):
|
||||
normalized_string = normalize_string(instruction)
|
||||
# handle HTML tags that can be converted to markup
|
||||
normalized_string = instruction \
|
||||
.replace("<nobr>", "**") \
|
||||
.replace("</nobr>", "**") \
|
||||
.replace("<strong>", "**") \
|
||||
.replace("</strong>", "**")
|
||||
normalized_string = normalize_string(normalized_string)
|
||||
normalized_string = normalized_string.replace('\n', ' \n')
|
||||
normalized_string = normalized_string.replace(' \n \n', '\n\n')
|
||||
return normalized_string
|
||||
|
||||
# handle unsupported, special UTF8 character in Thermomix-specific instructions,
|
||||
# that happen in nearly every recipe on Cookidoo, Zaubertopf Club, Rezeptwelt
|
||||
# and in Thermomix-specific recipes on many other sites
|
||||
return normalized_string \
|
||||
.replace("", _('reverse rotation')) \
|
||||
.replace("", _('careful rotation')) \
|
||||
.replace("", _('knead')) \
|
||||
.replace("Andicken ", _('thicken')) \
|
||||
.replace("Erwärmen ", _('warm up')) \
|
||||
.replace("Fermentieren ", _('ferment')) \
|
||||
.replace("Sous-vide ", _("sous-vide"))
|
||||
|
||||
|
||||
def parse_instructions(instructions):
|
||||
@@ -299,6 +341,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]
|
||||
|
||||
|
||||
@@ -322,10 +369,28 @@ def parse_time(recipe_time):
|
||||
|
||||
def parse_keywords(keyword_json, space):
|
||||
keywords = []
|
||||
keyword_aliases = {}
|
||||
# retrieve keyword automation cache if it exists, otherwise build from database
|
||||
KEYWORD_CACHE_KEY = f'automation_keyword_alias_{space.pk}'
|
||||
if c := caches['default'].get(KEYWORD_CACHE_KEY, None):
|
||||
self.food_aliases = c
|
||||
caches['default'].touch(KEYWORD_CACHE_KEY, 30)
|
||||
else:
|
||||
for a in Automation.objects.filter(space=space, disabled=False, type=Automation.KEYWORD_ALIAS).only('param_1', 'param_2').order_by('order').all():
|
||||
keyword_aliases[a.param_1] = a.param_2
|
||||
caches['default'].set(KEYWORD_CACHE_KEY, keyword_aliases, 30)
|
||||
|
||||
# keywords as list
|
||||
for kw in keyword_json:
|
||||
kw = normalize_string(kw)
|
||||
# if alias exists use that instead
|
||||
|
||||
if len(kw) != 0:
|
||||
if keyword_aliases:
|
||||
try:
|
||||
kw = keyword_aliases[kw]
|
||||
except KeyError:
|
||||
pass
|
||||
if k := Keyword.objects.filter(name=kw, space=space).first():
|
||||
keywords.append({'label': str(k), 'name': k.name, 'id': k.id})
|
||||
else:
|
||||
@@ -396,3 +461,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()
|
||||
|
||||
@@ -2,24 +2,54 @@ import json
|
||||
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Ingredient, Recipe, Step
|
||||
|
||||
from cookbook.models import Ingredient, Recipe, Step, Keyword, Comment, CookLog
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
class OpenEats(Integration):
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
recipe = Recipe.objects.create(name=file['name'].strip(), created_by=self.request.user, internal=True,
|
||||
|
||||
description = file['info']
|
||||
description_max_length = Recipe._meta.get_field('description').max_length
|
||||
if len(description) > description_max_length:
|
||||
description = description[0:description_max_length]
|
||||
|
||||
recipe = Recipe.objects.create(name=file['name'].strip(), description=description, created_by=self.request.user, internal=True,
|
||||
servings=file['servings'], space=self.request.space, waiting_time=file['cook_time'], working_time=file['prep_time'])
|
||||
|
||||
instructions = ''
|
||||
if file["info"] != '':
|
||||
instructions += file["info"]
|
||||
|
||||
if file["directions"] != '':
|
||||
instructions += file["directions"]
|
||||
|
||||
if file["source"] != '':
|
||||
instructions += file["source"]
|
||||
instructions += '\n' + _('Recipe source:') + f'[{file["source"]}]({file["source"]})'
|
||||
|
||||
cuisine_keyword, created = Keyword.objects.get_or_create(name="Cuisine", space=self.request.space)
|
||||
if file["cuisine"] != '':
|
||||
keyword, created = Keyword.objects.get_or_create(name=file["cuisine"].strip(), space=self.request.space)
|
||||
if created:
|
||||
keyword.move(cuisine_keyword, pos="last-child")
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
course_keyword, created = Keyword.objects.get_or_create(name="Course", space=self.request.space)
|
||||
if file["course"] != '':
|
||||
keyword, created = Keyword.objects.get_or_create(name=file["course"].strip(), space=self.request.space)
|
||||
if created:
|
||||
keyword.move(course_keyword, pos="last-child")
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
for tag in file["tags"]:
|
||||
keyword, created = Keyword.objects.get_or_create(name=tag.strip(), space=self.request.space)
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
for comment in file['comments']:
|
||||
Comment.objects.create(recipe=recipe, text=comment['text'], created_by=self.request.user)
|
||||
CookLog.objects.create(recipe=recipe, rating=comment['rating'], created_by=self.request.user, space=self.request.space)
|
||||
|
||||
if file["photo"] != '':
|
||||
recipe.image = f'recipes/openeats-import/{file["photo"]}'
|
||||
recipe.save()
|
||||
|
||||
step = Step.objects.create(instruction=instructions, space=self.request.space,)
|
||||
|
||||
@@ -38,6 +68,9 @@ class OpenEats(Integration):
|
||||
recipe_json = json.loads(file.read())
|
||||
recipe_dict = {}
|
||||
ingredient_group_dict = {}
|
||||
cuisine_group_dict = {}
|
||||
course_group_dict = {}
|
||||
tag_group_dict = {}
|
||||
|
||||
for o in recipe_json:
|
||||
if o['model'] == 'recipe.recipe':
|
||||
@@ -50,11 +83,27 @@ class OpenEats(Integration):
|
||||
'cook_time': o['fields']['cook_time'],
|
||||
'servings': o['fields']['servings'],
|
||||
'ingredients': [],
|
||||
'photo': o['fields']['photo'],
|
||||
'cuisine': o['fields']['cuisine'],
|
||||
'course': o['fields']['course'],
|
||||
'tags': o['fields']['tags'],
|
||||
'comments': [],
|
||||
}
|
||||
if o['model'] == 'ingredient.ingredientgroup':
|
||||
ingredient_group_dict[o['pk']] = o['fields']['recipe']
|
||||
if o['model'] == 'recipe_groups.cuisine':
|
||||
cuisine_group_dict[o['pk']] = o['fields']['title']
|
||||
if o['model'] == 'recipe_groups.course':
|
||||
course_group_dict[o['pk']] = o['fields']['title']
|
||||
if o['model'] == 'recipe_groups.tag':
|
||||
tag_group_dict[o['pk']] = o['fields']['title']
|
||||
|
||||
for o in recipe_json:
|
||||
if o['model'] == 'rating.rating':
|
||||
recipe_dict[o['fields']['recipe']]["comments"].append({
|
||||
"text": o['fields']['comment'],
|
||||
"rating": o['fields']['rating']
|
||||
})
|
||||
if o['model'] == 'ingredient.ingredient':
|
||||
ingredient = {
|
||||
'food': o['fields']['title'],
|
||||
@@ -63,6 +112,15 @@ class OpenEats(Integration):
|
||||
}
|
||||
recipe_dict[ingredient_group_dict[o['fields']['ingredient_group']]]['ingredients'].append(ingredient)
|
||||
|
||||
for k, r in recipe_dict.items():
|
||||
if r["cuisine"] in cuisine_group_dict:
|
||||
r["cuisine"] = cuisine_group_dict[r["cuisine"]]
|
||||
if r["course"] in course_group_dict:
|
||||
r["course"] = course_group_dict[r["course"]]
|
||||
for index in range(len(r["tags"])):
|
||||
if r["tags"][index] in tag_group_dict:
|
||||
r["tags"][index] = tag_group_dict[r["tags"][index]]
|
||||
|
||||
return list(recipe_dict.values())
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
|
||||
@@ -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.
@@ -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-05-10 15:32+0000\n"
|
||||
"Last-Translator: zeon <zeonbg@gmail.com>\n"
|
||||
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
|
||||
"Last-Translator: noxonad <noxonad@proton.me>\n"
|
||||
"Language-Team: Bulgarian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/bg/>\n"
|
||||
"Language: bg\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
|
||||
@@ -1433,7 +1433,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\index.html:29
|
||||
msgid "Search recipe ..."
|
||||
msgstr "Търсете рецепта..."
|
||||
msgstr "Търсете рецепта ..."
|
||||
|
||||
#: .\cookbook\templates\index.html:44
|
||||
msgid "New Recipe"
|
||||
@@ -1818,7 +1818,7 @@ msgid ""
|
||||
msgstr ""
|
||||
" \n"
|
||||
" Пълнотекстови търсения се опитват да нормализират предоставените "
|
||||
"думи, за да съответстват на често срещани варианти. Например: 'вили, "
|
||||
"думи, за да съответстват на често срещани варианти. Например: 'вили, "
|
||||
"'вилица', 'вилици' всички ще се нормализират до 'вилиц'.\n"
|
||||
" Има няколко налични метода, описани по-долу, които ще "
|
||||
"контролират как поведението при търсене трябва да реагира, когато се търсят "
|
||||
|
||||
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-03-25 11:32+0000\n"
|
||||
"Last-Translator: Matěj Kubla <matykubla@gmail.com>\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."
|
||||
@@ -551,7 +553,7 @@ msgstr "Cesta musí být v následujícím formátu"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:27
|
||||
msgid "Sync Now!"
|
||||
msgstr "Zahájit synchronizaci"
|
||||
msgstr "Zahájit synchronizaci!"
|
||||
|
||||
#: .\cookbook\templates\batch\waiting.html:4
|
||||
#: .\cookbook\templates\batch\waiting.html:10
|
||||
@@ -1034,7 +1036,7 @@ msgstr "Tento text je kurzívou"
|
||||
#: .\cookbook\templates\markdown_info.html:61
|
||||
#: .\cookbook\templates\markdown_info.html:77
|
||||
msgid "Blockquotes are also possible"
|
||||
msgstr "Lze použít i kvotace "
|
||||
msgstr "Lze použít i kvotace"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:84
|
||||
msgid "Lists"
|
||||
@@ -1104,8 +1106,8 @@ msgid ""
|
||||
"rel=\"noreferrer noopener\" target=\"_blank\">this one.</a>"
|
||||
msgstr ""
|
||||
"Ruční vytváření tabulek pomocí značek je složité. Doporučujeme použít "
|
||||
"například <a href=\"https://www.tablesgenerator.com/markdown_tables\" "
|
||||
"rel=\"noreferrer noopener\" target=\"_blank\">tento tabulkový editor</a>."
|
||||
"například <a href=\"https://www.tablesgenerator.com/markdown_tables\" rel="
|
||||
"\"noreferrer noopener\" target=\"_blank\">tento tabulkový editor.</a>"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:155
|
||||
#: .\cookbook\templates\markdown_info.html:157
|
||||
@@ -1254,22 +1256,36 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" <p>Modul jídelníčku umožňuje plánovat jídlo pomocí receptů i poznámek.</p>\n"
|
||||
" <p>Jednoduše vyberte recept ze seznamu naposledy navštívených receptů, nebo ho vyhledejte\n"
|
||||
" s přetáhněte na požadovaný den v rozvrhu. Můžete také přidat poznámku s popiskem\n"
|
||||
" a poté přetáhnout recept pro vytvoření plánu s vlatními popisky. Vytvořením samotné poznámky\n"
|
||||
" je možné přetažením pole poznámky do rozvrhu.</p>\n"
|
||||
" <p>Kliknutím na recept zobrazíte detailní náhled. Odtud lze také přidat položky\n"
|
||||
" do nákupního seznamu. Do nákupního seznamu můžete také přidat všechny recepty na daný den\n"
|
||||
" kliknutím na ikonu nákupního košíku na horní straně tabulky.</p>\n"
|
||||
" <p>V běžném případě se jídelníček plánuje hromadně, proto můžete v nastavení definovat\n"
|
||||
" se kterými uživateli si přejete jídelníčky sdílet.\n"
|
||||
" <p>Modul jídelníčku umožňuje plánovat jídlo "
|
||||
"pomocí receptů i poznámek.</p>\n"
|
||||
" <p>Jednoduše vyberte recept ze seznamu naposledy "
|
||||
"navštívených receptů, nebo ho vyhledejte\n"
|
||||
" s přetáhněte na požadovaný den v rozvrhu. "
|
||||
"Můžete také přidat poznámku s popiskem\n"
|
||||
" a poté přetáhnout recept pro vytvoření plánu "
|
||||
"s vlatními popisky. Vytvořením samotné poznámky\n"
|
||||
" je možné přetažením pole poznámky do "
|
||||
"rozvrhu.</p>\n"
|
||||
" <p>Kliknutím na recept zobrazíte detailní "
|
||||
"náhled. Odtud lze také přidat položky\n"
|
||||
" do nákupního seznamu. Do nákupního seznamu "
|
||||
"můžete také přidat všechny recepty na daný den\n"
|
||||
" kliknutím na ikonu nákupního košíku na horní "
|
||||
"straně tabulky.</p>\n"
|
||||
" <p>V běžném případě se jídelníček plánuje "
|
||||
"hromadně, proto můžete v nastavení definovat\n"
|
||||
" se kterými uživateli si přejete jídelníčky "
|
||||
"sdílet.\n"
|
||||
" </p>\n"
|
||||
" <p>Můžete také upravovat typy jídel, které si přejete naplánovat. Pokud budete sdílet jídelníček \n"
|
||||
" <p>Můžete také upravovat typy jídel, které si "
|
||||
"přejete naplánovat. Pokud budete sdílet jídelníček \n"
|
||||
" s někým, kdo\n"
|
||||
" má přidána jiná jídla, jeho typy jídel se objeví i ve vašem seznamu. Pro předcházení\n"
|
||||
" má přidána jiná jídla, jeho typy jídel se "
|
||||
"objeví i ve vašem seznamu. Pro předcházení\n"
|
||||
" duplicitám (např. Ostatní, Jiná)\n"
|
||||
" pojmenujte váš typ jídla stejně, jako uživatel se kterým své seznamy sdílíte. Tím budou seznamy sloučeny.</p>\n"
|
||||
" pojmenujte váš typ jídla stejně, jako "
|
||||
"uživatel se kterým své seznamy sdílíte. Tím budou seznamy\n"
|
||||
" sloučeny.</p>\n"
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\meal_plan_entry.html:6
|
||||
@@ -1331,12 +1347,12 @@ msgstr "Obrázek receptu"
|
||||
#: .\cookbook\templates\recipes_table.html:46
|
||||
#: .\cookbook\templates\url_import.html:55
|
||||
msgid "Preparation time ca."
|
||||
msgstr "Doba přípravy cca"
|
||||
msgstr "Doba přípravy cca."
|
||||
|
||||
#: .\cookbook\templates\recipes_table.html:52
|
||||
#: .\cookbook\templates\url_import.html:60
|
||||
msgid "Waiting time ca."
|
||||
msgstr "Doba čekání cca"
|
||||
msgstr "Doba čekání cca."
|
||||
|
||||
#: .\cookbook\templates\recipes_table.html:55
|
||||
msgid "External"
|
||||
@@ -1384,7 +1400,7 @@ msgid ""
|
||||
" in the following examples:"
|
||||
msgstr ""
|
||||
"Použijte tajný klíč jako autorizační hlavičku definovanou slovním klíčem, "
|
||||
"jak je uvedeno v následujících příkladech."
|
||||
"jak je uvedeno v následujících příkladech:"
|
||||
|
||||
#: .\cookbook\templates\settings.html:94
|
||||
msgid "or"
|
||||
@@ -1806,7 +1822,7 @@ msgstr "Import není pro tohoto poskytovatele implementován!"
|
||||
|
||||
#: .\cookbook\views\import_export.py:58
|
||||
msgid "Exporting is not implemented for this provider"
|
||||
msgstr "Eport není pro tohoto poskytovatele implementován!"
|
||||
msgstr "Export není pro tohoto poskytovatele implementován!"
|
||||
|
||||
#: .\cookbook\views\lists.py:42
|
||||
msgid "Import Log"
|
||||
@@ -1838,7 +1854,7 @@ msgstr "Komentář uložen!"
|
||||
|
||||
#: .\cookbook\views\views.py:152
|
||||
msgid "This recipe is already linked to the book!"
|
||||
msgstr "Tento recept už v kuchařce existuje."
|
||||
msgstr "Tento recept už v kuchařce existuje!"
|
||||
|
||||
#: .\cookbook\views\views.py:158
|
||||
msgid "Bookmark saved!"
|
||||
|
||||
Binary file not shown.
@@ -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-04-12 11:55+0000\n"
|
||||
"Last-Translator: noxonad <noxonad@proton.me>\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
|
||||
@@ -1806,7 +1806,7 @@ msgid ""
|
||||
msgstr ""
|
||||
" \n"
|
||||
" Heltekstsøgning forsøger at normalisere de givne ord så de "
|
||||
"matcher stammevarianter. F.eks: 'skeen', 'skeer' og 'sket' vil alt "
|
||||
"matcher stammevarianter. F.eks: 'skeen', 'skeer' og 'sket' vil alt "
|
||||
"normaliseres til 'ske'.\n"
|
||||
" Der er flere metoder tilgængelige, beskrevet herunder, som vil "
|
||||
"bestemme hvordan søgningen skal opfører sig når flere søgeord er angivet.\n"
|
||||
@@ -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
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.
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.
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
|
||||
"PO-Revision-Date: 2021-04-11 15:23+0000\n"
|
||||
"Last-Translator: Allan Nordhøy <epost@anotheragency.no>\n"
|
||||
"PO-Revision-Date: 2023-04-17 20:55+0000\n"
|
||||
"Last-Translator: Espen Sellevåg <buskmenn.drammer03@icloud.com>\n"
|
||||
"Language-Team: Norwegian Bokmål <http://translate.tandoor.dev/projects/"
|
||||
"tandoor/recipes-backend/nb_NO/>\n"
|
||||
"Language: nb_NO\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.5.3\n"
|
||||
"X-Generator: Weblate 4.15\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:91
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:219
|
||||
@@ -34,19 +34,23 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:46
|
||||
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
|
||||
msgstr ""
|
||||
msgstr "Standard enhet når ny ingrediens legges til en oppskrift."
|
||||
|
||||
#: .\cookbook\forms.py:47
|
||||
msgid ""
|
||||
"Enables support for fractions in ingredient amounts (e.g. convert decimals "
|
||||
"to fractions automatically)"
|
||||
msgstr ""
|
||||
"Aktiverer støtte for deler av ingrediensmengde (konverterer feks. desimaler "
|
||||
"til deler automatisk)"
|
||||
|
||||
#: .\cookbook\forms.py:48
|
||||
msgid ""
|
||||
"Users with whom newly created meal plan/shopping list entries should be "
|
||||
"shared by default."
|
||||
msgstr ""
|
||||
"Brukere som oppretter nye måltidsplaner/handlelister, deler disse "
|
||||
"oppføringene som standard."
|
||||
|
||||
#: .\cookbook\forms.py:49
|
||||
msgid "Show recently viewed recipes on search page."
|
||||
@@ -58,7 +62,7 @@ msgstr "Antall desimaler ingredienser skal avrundes til."
|
||||
|
||||
#: .\cookbook\forms.py:51
|
||||
msgid "If you want to be able to create and see comments underneath recipes."
|
||||
msgstr ""
|
||||
msgstr "Hvis du ønsker å opprette og se kommentarer under oppskrifter."
|
||||
|
||||
#: .\cookbook\forms.py:53
|
||||
msgid ""
|
||||
@@ -67,6 +71,11 @@ 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 vil deaktivere automatisk synkronisering. Når en handleliste vises, "
|
||||
"oppdateres listen med oppgitt antall sekunders mellomrom for å synkronisere "
|
||||
"endringer fra andre brukere. Nyttig dersom flere brukere handler samtidig. "
|
||||
"Datatrafikk oppstår når aktiv. Hvis verdien er lavere enn grensen, "
|
||||
"tilbakestilles den ved lagring."
|
||||
|
||||
#: .\cookbook\forms.py:56
|
||||
msgid "Makes the navbar stick to the top of the page."
|
||||
@@ -100,11 +109,11 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:97 .\cookbook\forms.py:317
|
||||
msgid "Path"
|
||||
msgstr ""
|
||||
msgstr "Sti"
|
||||
|
||||
#: .\cookbook\forms.py:98
|
||||
msgid "Storage UID"
|
||||
msgstr ""
|
||||
msgstr "Lagring UID"
|
||||
|
||||
#: .\cookbook\forms.py:121
|
||||
msgid "Default"
|
||||
@@ -129,7 +138,6 @@ msgid "Old Unit"
|
||||
msgstr "Gammel enhet"
|
||||
|
||||
#: .\cookbook\forms.py:156
|
||||
#, fuzzy
|
||||
msgid "Unit that should be replaced."
|
||||
msgstr "Enhet som skal erstattes."
|
||||
|
||||
@@ -204,12 +212,11 @@ msgstr ""
|
||||
#: .\cookbook\views\views.py:112 .\cookbook\views\views.py:116
|
||||
#: .\cookbook\views\views.py:184
|
||||
msgid "You do not have the required permissions to view this page!"
|
||||
msgstr "Du har ikke påkrevd tilgang for å vise denne siden."
|
||||
msgstr "Du har ikke påkrevd tilgang for å vise denne siden!"
|
||||
|
||||
#: .\cookbook\helper\permission_helper.py:141
|
||||
#, fuzzy
|
||||
msgid "You are not logged in and therefore cannot view this page!"
|
||||
msgstr "Du er ikke innlogget og kan derfor ikke vise siden."
|
||||
msgstr "Du er ikke innlogget og kan derfor ikke vise siden!"
|
||||
|
||||
#: .\cookbook\helper\permission_helper.py:145
|
||||
#: .\cookbook\helper\permission_helper.py:167
|
||||
@@ -379,7 +386,7 @@ msgstr "Finner ikke siden du leter etter."
|
||||
|
||||
#: .\cookbook\templates\404.html:33
|
||||
msgid "Take me Home"
|
||||
msgstr ""
|
||||
msgstr "Tilbake til Startsiden"
|
||||
|
||||
#: .\cookbook\templates\404.html:35
|
||||
msgid "Report a Bug"
|
||||
@@ -388,12 +395,12 @@ msgstr "Rapporter en feil"
|
||||
#: .\cookbook\templates\account\login.html:7
|
||||
#: .\cookbook\templates\base.html:170
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
msgstr "Logg inn"
|
||||
|
||||
#: .\cookbook\templates\account\login.html:13
|
||||
#: .\cookbook\templates\account\login.html:28
|
||||
msgid "Sign In"
|
||||
msgstr ""
|
||||
msgstr "Opprett bruker"
|
||||
|
||||
#: .\cookbook\templates\account\login.html:38
|
||||
msgid "Social Login"
|
||||
@@ -401,7 +408,7 @@ msgstr "Sosial innlogging"
|
||||
|
||||
#: .\cookbook\templates\account\login.html:39
|
||||
msgid "You can use any of the following providers to sign in."
|
||||
msgstr ""
|
||||
msgstr "Velg en av følgende leverandører for å logge på."
|
||||
|
||||
#: .\cookbook\templates\account\logout.html:5
|
||||
#: .\cookbook\templates\account\logout.html:9
|
||||
@@ -416,20 +423,20 @@ msgstr "Er du sikker på at du vil logge ut?"
|
||||
#: .\cookbook\templates\account\password_reset.html:5
|
||||
#: .\cookbook\templates\account\password_reset_done.html:5
|
||||
msgid "Password Reset"
|
||||
msgstr ""
|
||||
msgstr "Nullstill passord"
|
||||
|
||||
#: .\cookbook\templates\account\password_reset.html:9
|
||||
#: .\cookbook\templates\account\password_reset_done.html:9
|
||||
msgid "Password reset is not implemented for the time being!"
|
||||
msgstr ""
|
||||
msgstr "Det er foreløpig ikke implementert funksjon for å nullstille passord!"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:5
|
||||
msgid "Register"
|
||||
msgstr ""
|
||||
msgstr "Registrer"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:9
|
||||
msgid "Create your Account"
|
||||
msgstr "Opprett din konto"
|
||||
msgstr "Opprett konto"
|
||||
|
||||
#: .\cookbook\templates\account\signup.html:14
|
||||
msgid "Create User"
|
||||
@@ -442,11 +449,11 @@ msgstr "API-dokumentasjon"
|
||||
|
||||
#: .\cookbook\templates\base.html:78
|
||||
msgid "Utensils"
|
||||
msgstr ""
|
||||
msgstr "Redskaper"
|
||||
|
||||
#: .\cookbook\templates\base.html:88
|
||||
msgid "Shopping"
|
||||
msgstr ""
|
||||
msgstr "Handle"
|
||||
|
||||
#: .\cookbook\templates\base.html:102 .\cookbook\views\delete.py:84
|
||||
#: .\cookbook\views\edit.py:93 .\cookbook\views\lists.py:26
|
||||
@@ -456,27 +463,27 @@ msgstr "Nøkkelord"
|
||||
|
||||
#: .\cookbook\templates\base.html:104
|
||||
msgid "Batch Edit"
|
||||
msgstr ""
|
||||
msgstr "Oppdatere flere"
|
||||
|
||||
#: .\cookbook\templates\base.html:109
|
||||
msgid "Storage Data"
|
||||
msgstr ""
|
||||
msgstr "Datalagring"
|
||||
|
||||
#: .\cookbook\templates\base.html:113
|
||||
msgid "Storage Backends"
|
||||
msgstr ""
|
||||
msgstr "Lagringsplasser"
|
||||
|
||||
#: .\cookbook\templates\base.html:115
|
||||
msgid "Configure Sync"
|
||||
msgstr ""
|
||||
msgstr "Konfigurer synkronisering"
|
||||
|
||||
#: .\cookbook\templates\base.html:117
|
||||
msgid "Discovered Recipes"
|
||||
msgstr ""
|
||||
msgstr "Oppdagede oppskrifter"
|
||||
|
||||
#: .\cookbook\templates\base.html:119
|
||||
msgid "Discovery Log"
|
||||
msgstr ""
|
||||
msgstr "Logg Oppdagelser"
|
||||
|
||||
#: .\cookbook\templates\base.html:121 .\cookbook\templates\stats.html:10
|
||||
msgid "Statistics"
|
||||
@@ -484,7 +491,7 @@ msgstr "Statistikk"
|
||||
|
||||
#: .\cookbook\templates\base.html:123
|
||||
msgid "Units & Ingredients"
|
||||
msgstr ""
|
||||
msgstr "Enheter & Ingredienser"
|
||||
|
||||
#: .\cookbook\templates\base.html:125
|
||||
msgid "Import Recipe"
|
||||
@@ -521,58 +528,61 @@ msgid "API Browser"
|
||||
msgstr "API-utforsker"
|
||||
|
||||
#: .\cookbook\templates\base.html:165
|
||||
#, fuzzy
|
||||
msgid "Logout"
|
||||
msgstr "Logg ut"
|
||||
|
||||
#: .\cookbook\templates\batch\edit.html:6
|
||||
msgid "Batch edit Category"
|
||||
msgstr ""
|
||||
msgstr "Oppdater flere kategorier"
|
||||
|
||||
#: .\cookbook\templates\batch\edit.html:15
|
||||
msgid "Batch edit Recipes"
|
||||
msgstr ""
|
||||
msgstr "Oppdater flere oppskrifter"
|
||||
|
||||
#: .\cookbook\templates\batch\edit.html:20
|
||||
msgid "Add the specified keywords to all recipes containing a word"
|
||||
msgstr ""
|
||||
msgstr "Legg til spesifikt nøkkelord til alle oppskrifter som inneholder et ord"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:76
|
||||
msgid "Sync"
|
||||
msgstr ""
|
||||
msgstr "Synkronisering"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:10
|
||||
msgid "Manage watched Folders"
|
||||
msgstr ""
|
||||
msgstr "Behandle overvåkede mapper"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:14
|
||||
msgid ""
|
||||
"On this Page you can manage all storage folder locations that should be "
|
||||
"monitored and synced."
|
||||
msgstr ""
|
||||
"Her kan du behandle alle lagringsmapper og plasseringer for monitorering og "
|
||||
"synkronisering."
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:16
|
||||
msgid "The path must be in the following format"
|
||||
msgstr ""
|
||||
msgstr "Stien må være i følgende format"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:27
|
||||
msgid "Sync Now!"
|
||||
msgstr ""
|
||||
msgstr "Synkroniser nå!"
|
||||
|
||||
#: .\cookbook\templates\batch\waiting.html:4
|
||||
#: .\cookbook\templates\batch\waiting.html:10
|
||||
msgid "Importing Recipes"
|
||||
msgstr ""
|
||||
msgstr "Importerer oppskrifter"
|
||||
|
||||
#: .\cookbook\templates\batch\waiting.html:23
|
||||
msgid ""
|
||||
"This can take a few minutes, depending on the number of recipes in sync, "
|
||||
"please wait."
|
||||
msgstr ""
|
||||
"Dette kan ta noen minutter, avhenging av antall oppskrifter som skal "
|
||||
"synkroniseres. Vennligst vent."
|
||||
|
||||
#: .\cookbook\templates\books.html:5 .\cookbook\templates\books.html:11
|
||||
msgid "Recipe Books"
|
||||
msgstr ""
|
||||
msgstr "Oppskriftsbøker"
|
||||
|
||||
#: .\cookbook\templates\books.html:15
|
||||
msgid "New Book"
|
||||
@@ -584,32 +594,32 @@ msgstr "av"
|
||||
|
||||
#: .\cookbook\templates\books.html:34
|
||||
msgid "Toggle Recipes"
|
||||
msgstr ""
|
||||
msgstr "Veksle oppskrifter"
|
||||
|
||||
#: .\cookbook\templates\books.html:54
|
||||
#: .\cookbook\templates\meal_plan_entry.html:48
|
||||
#: .\cookbook\templates\recipes_table.html:64
|
||||
msgid "Last cooked"
|
||||
msgstr ""
|
||||
msgstr "Forrige tilbereding"
|
||||
|
||||
#: .\cookbook\templates\books.html:71
|
||||
msgid "There are no recipes in this book yet."
|
||||
msgstr ""
|
||||
msgstr "Det er foreløpig ingen oppskrifter i denne boken."
|
||||
|
||||
#: .\cookbook\templates\export.html:6 .\cookbook\templates\test2.html:6
|
||||
msgid "Export Recipes"
|
||||
msgstr ""
|
||||
msgstr "Eksporter oppskrifter"
|
||||
|
||||
#: .\cookbook\templates\export.html:14 .\cookbook\templates\export.html:20
|
||||
#: .\cookbook\templates\shopping_list.html:347
|
||||
#: .\cookbook\templates\test2.html:14 .\cookbook\templates\test2.html:20
|
||||
msgid "Export"
|
||||
msgstr ""
|
||||
msgstr "Eksporter"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:5
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:9
|
||||
msgid "Import new Recipe"
|
||||
msgstr ""
|
||||
msgstr "Importer ny oppskrift"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_import_recipe.html:14
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:389
|
||||
@@ -635,29 +645,29 @@ msgstr "Beskrivelse"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:72
|
||||
msgid "Waiting Time"
|
||||
msgstr ""
|
||||
msgstr "Ventetid"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:78
|
||||
msgid "Servings Text"
|
||||
msgstr ""
|
||||
msgstr "Porsjon beskrivelse"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:89
|
||||
msgid "Select Keywords"
|
||||
msgstr ""
|
||||
msgstr "Velg nøkkelord"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:90
|
||||
#: .\cookbook\templates\url_import.html:212
|
||||
msgid "Add Keyword"
|
||||
msgstr ""
|
||||
msgstr "Legg til nøkkelord"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:108
|
||||
msgid "Nutrition"
|
||||
msgstr ""
|
||||
msgstr "Næringsinnhold"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:112
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:162
|
||||
msgid "Delete Step"
|
||||
msgstr ""
|
||||
msgstr "Fjern trinn"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:116
|
||||
msgid "Calories"
|
||||
@@ -678,15 +688,15 @@ msgstr "Proteiner"
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:146
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:454
|
||||
msgid "Step"
|
||||
msgstr ""
|
||||
msgstr "Trinn"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:167
|
||||
msgid "Show as header"
|
||||
msgstr ""
|
||||
msgstr "Vis som overskrift"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:173
|
||||
msgid "Hide as header"
|
||||
msgstr ""
|
||||
msgstr "Skjul overskrift"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:178
|
||||
msgid "Move Up"
|
||||
@@ -698,15 +708,15 @@ msgstr "Flytt nedover"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:192
|
||||
msgid "Step Name"
|
||||
msgstr ""
|
||||
msgstr "Trinn navn"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:196
|
||||
msgid "Step Type"
|
||||
msgstr ""
|
||||
msgstr "Trinn type"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:207
|
||||
msgid "Step time in Minutes"
|
||||
msgstr ""
|
||||
msgstr "Trinn tid i minutter"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:261
|
||||
#: .\cookbook\templates\shopping_list.html:183
|
||||
@@ -740,7 +750,7 @@ msgstr "Velg mat"
|
||||
#: .\cookbook\templates\meal_plan.html:256
|
||||
#: .\cookbook\templates\url_import.html:171
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
msgstr "Notis"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:319
|
||||
msgid "Delete Ingredient"
|
||||
@@ -748,7 +758,7 @@ msgstr "Slett ingrediens"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:325
|
||||
msgid "Make Header"
|
||||
msgstr ""
|
||||
msgstr "Bruk som overskrift"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:331
|
||||
msgid "Make Ingredient"
|
||||
@@ -756,15 +766,15 @@ msgstr "Opprett ingrediens"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:337
|
||||
msgid "Disable Amount"
|
||||
msgstr ""
|
||||
msgstr "Deaktiver mengde"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:343
|
||||
msgid "Enable Amount"
|
||||
msgstr ""
|
||||
msgstr "Aktiver mengde"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:348
|
||||
msgid "Copy Template Reference"
|
||||
msgstr ""
|
||||
msgstr "Kopier mal-referanse"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:374
|
||||
#: .\cookbook\templates\url_import.html:196
|
||||
@@ -773,29 +783,28 @@ msgstr "Instruksjoner"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:387
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:418
|
||||
#, fuzzy
|
||||
msgid "Save & View"
|
||||
msgstr "Lagre og vis"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:391
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:424
|
||||
msgid "Add Step"
|
||||
msgstr ""
|
||||
msgstr "Legg til trinn"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:394
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:428
|
||||
msgid "Add Nutrition"
|
||||
msgstr ""
|
||||
msgstr "Legg til næringsinnhold"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:396
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:430
|
||||
msgid "Remove Nutrition"
|
||||
msgstr ""
|
||||
msgstr "Fjern næringsinnhold"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:398
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:433
|
||||
msgid "View Recipe"
|
||||
msgstr ""
|
||||
msgstr "Vis oppskrift"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:400
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:435
|
||||
@@ -804,11 +813,11 @@ msgstr "Slett oppskrift"
|
||||
|
||||
#: .\cookbook\templates\forms\edit_internal_recipe.html:441
|
||||
msgid "Steps"
|
||||
msgstr ""
|
||||
msgstr "Trinn"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:15
|
||||
msgid "Edit Ingredients"
|
||||
msgstr ""
|
||||
msgstr "Rediger ingrediens"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:16
|
||||
msgid ""
|
||||
@@ -820,54 +829,61 @@ msgid ""
|
||||
"them.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Følgende skjema kan brukes dersom, tilfeldigvis, to eller flere "
|
||||
"enheter eller ingredienser er opprettet,\n"
|
||||
" og burde være identiske.\n"
|
||||
" Det slår sammen to enheter eller ingredienser og oppdaterer alle "
|
||||
"oppskrifter som inneholder disse.\n"
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:24
|
||||
#: .\cookbook\templates\stats.html:26
|
||||
msgid "Units"
|
||||
msgstr ""
|
||||
msgstr "Enheter"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:26
|
||||
msgid "Are you sure that you want to merge these two units?"
|
||||
msgstr ""
|
||||
msgstr "Er du sikker på at du vil slå sammen disse enhetene?"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:31
|
||||
#: .\cookbook\templates\forms\ingredients.html:40
|
||||
msgid "Merge"
|
||||
msgstr "Flett"
|
||||
msgstr "Slå sammen"
|
||||
|
||||
#: .\cookbook\templates\forms\ingredients.html:36
|
||||
msgid "Are you sure that you want to merge these two ingredients?"
|
||||
msgstr ""
|
||||
msgstr "Er du sikker på at du vil slå sammen disse ingrediensene?"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:18
|
||||
#, python-format
|
||||
msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
|
||||
msgstr ""
|
||||
msgstr "Er du sikker på at du vil slette %(title)s: <b>%(object)s</b> "
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:21
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
msgstr "Bekreft"
|
||||
|
||||
#: .\cookbook\templates\generic\edit_template.html:30
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
msgstr "Vis"
|
||||
|
||||
#: .\cookbook\templates\generic\edit_template.html:34
|
||||
msgid "Delete original file"
|
||||
msgstr ""
|
||||
msgstr "Slett opprinnelig fil"
|
||||
|
||||
#: .\cookbook\templates\generic\list_template.html:6
|
||||
#: .\cookbook\templates\generic\list_template.html:12
|
||||
msgid "List"
|
||||
msgstr ""
|
||||
msgstr "Liste"
|
||||
|
||||
#: .\cookbook\templates\generic\list_template.html:25
|
||||
msgid "Filter"
|
||||
msgstr ""
|
||||
msgstr "Filtrer"
|
||||
|
||||
#: .\cookbook\templates\generic\list_template.html:30
|
||||
msgid "Import all"
|
||||
msgstr ""
|
||||
msgstr "Importer alle"
|
||||
|
||||
#: .\cookbook\templates\generic\new_template.html:6
|
||||
#: .\cookbook\templates\generic\new_template.html:14
|
||||
@@ -891,19 +907,19 @@ msgstr "Vis logg"
|
||||
|
||||
#: .\cookbook\templates\history.html:24
|
||||
msgid "Cook Log"
|
||||
msgstr ""
|
||||
msgstr "Tilberedingslogg"
|
||||
|
||||
#: .\cookbook\templates\import.html:6 .\cookbook\templates\test.html:6
|
||||
msgid "Import Recipes"
|
||||
msgstr ""
|
||||
msgstr "Importer oppskrifter"
|
||||
|
||||
#: .\cookbook\templates\include\log_cooking.html:7
|
||||
msgid "Log Recipe Cooking"
|
||||
msgstr ""
|
||||
msgstr "Loggfør tilberedt oppskrift"
|
||||
|
||||
#: .\cookbook\templates\include\log_cooking.html:13
|
||||
msgid "All fields are optional and can be left empty."
|
||||
msgstr ""
|
||||
msgstr "Alle felt er valgfri og kan stå tomme."
|
||||
|
||||
#: .\cookbook\templates\include\log_cooking.html:19
|
||||
msgid "Rating"
|
||||
@@ -943,44 +959,53 @@ msgid ""
|
||||
"can be used.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" <b>Passord og nøkkelfeltene</b> er lagret som <b>ren tekst</b> i "
|
||||
"databasen.\n"
|
||||
" Dette er nødvendig for å kunne utføre API-forespørsler, men det øker "
|
||||
"samtidig risiko for\n"
|
||||
" uønsket tilgang til dem.<br/>\n"
|
||||
" For å begrense kosekvensene av uønsket tilgang, kan nøkler eller "
|
||||
"kontoer med begrenset tilgang benyttes.\n"
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\index.html:29
|
||||
msgid "Search recipe ..."
|
||||
msgstr ""
|
||||
msgstr "Søk etter oppskrift..."
|
||||
|
||||
#: .\cookbook\templates\index.html:44
|
||||
msgid "New Recipe"
|
||||
msgstr ""
|
||||
msgstr "Ny oppskrift"
|
||||
|
||||
#: .\cookbook\templates\index.html:47
|
||||
msgid "Website Import"
|
||||
msgstr ""
|
||||
msgstr "Importer fra nettside"
|
||||
|
||||
#: .\cookbook\templates\index.html:53
|
||||
msgid "Advanced Search"
|
||||
msgstr ""
|
||||
msgstr "Avansert søk"
|
||||
|
||||
#: .\cookbook\templates\index.html:57
|
||||
msgid "Reset Search"
|
||||
msgstr ""
|
||||
msgstr "Nullstill søk"
|
||||
|
||||
#: .\cookbook\templates\index.html:85
|
||||
msgid "Last viewed"
|
||||
msgstr ""
|
||||
msgstr "Sist sett"
|
||||
|
||||
#: .\cookbook\templates\index.html:87 .\cookbook\templates\meal_plan.html:178
|
||||
#: .\cookbook\templates\stats.html:22
|
||||
msgid "Recipes"
|
||||
msgstr ""
|
||||
msgstr "Oppskrifter"
|
||||
|
||||
#: .\cookbook\templates\index.html:94
|
||||
msgid "Log in to view recipes"
|
||||
msgstr ""
|
||||
msgstr "Logg inn for å se oppskrifter"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:5
|
||||
#: .\cookbook\templates\markdown_info.html:13
|
||||
msgid "Markdown Info"
|
||||
msgstr ""
|
||||
msgstr "Markdown informasjon"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:14
|
||||
msgid ""
|
||||
@@ -997,43 +1022,56 @@ msgid ""
|
||||
"below.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Markdown er et lettvekts markup språk som benyttes for å formatere "
|
||||
"ren tekst.\n"
|
||||
" Denne siden bruker biblioteket <a href=\"https://python-markdown."
|
||||
"github.io/\" target=\"_blank\">Python Markdown</a> for\n"
|
||||
" å konvertere teksten din til velformatert HTML. Fullstendig "
|
||||
"dokumentasjon for markdown finner du\n"
|
||||
" <a href=\"https://daringfireball.net/projects/markdown/syntax\" "
|
||||
"target=\"_blank\">her</a>.\n"
|
||||
" En ufullstendig, men sannsynligvis tilstrekkelig dokumentasjon "
|
||||
"finner du under her.\n"
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:25
|
||||
msgid "Headers"
|
||||
msgstr ""
|
||||
msgstr "Overskrifter"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:54
|
||||
msgid "Formatting"
|
||||
msgstr ""
|
||||
msgstr "Formatering"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:56
|
||||
#: .\cookbook\templates\markdown_info.html:72
|
||||
msgid "Line breaks are inserted by adding two spaces after the end of a line"
|
||||
msgstr ""
|
||||
"Linjeskift er satt inn ved å sette inn to mellomrom på slutten av en linje"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:57
|
||||
#: .\cookbook\templates\markdown_info.html:73
|
||||
msgid "or by leaving a blank line inbetween."
|
||||
msgstr ""
|
||||
msgstr "eller ved å sette inn en tom linje mellom."
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:59
|
||||
#: .\cookbook\templates\markdown_info.html:74
|
||||
msgid "This text is bold"
|
||||
msgstr ""
|
||||
msgstr "Denne teksten er Fet"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:60
|
||||
#: .\cookbook\templates\markdown_info.html:75
|
||||
msgid "This text is italic"
|
||||
msgstr ""
|
||||
msgstr "Denne teksten er Kursiv"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:61
|
||||
#: .\cookbook\templates\markdown_info.html:77
|
||||
msgid "Blockquotes are also possible"
|
||||
msgstr ""
|
||||
msgstr "Det er også mulig å sitere avsnitt"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:84
|
||||
msgid "Lists"
|
||||
msgstr ""
|
||||
msgstr "Lister"
|
||||
|
||||
#: .\cookbook\templates\markdown_info.html:85
|
||||
msgid ""
|
||||
@@ -1264,7 +1302,7 @@ msgstr ""
|
||||
#: .\cookbook\templates\no_groups_info.html:5
|
||||
#: .\cookbook\templates\no_groups_info.html:12
|
||||
msgid "No Permissions"
|
||||
msgstr "Ingen tilganger."
|
||||
msgstr "Ingen tilgang"
|
||||
|
||||
#: .\cookbook\templates\no_groups_info.html:17
|
||||
msgid "You do not have any groups and therefor cannot use this application."
|
||||
@@ -1298,12 +1336,11 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\offline.html:6
|
||||
msgid "Offline"
|
||||
msgstr "Frakoblet."
|
||||
msgstr "Frakoblet"
|
||||
|
||||
#: .\cookbook\templates\offline.html:19
|
||||
#, fuzzy
|
||||
msgid "You are currently offline!"
|
||||
msgstr "Du er ikke tilkoblet Internett."
|
||||
msgstr "Du er ikke tilkoblet!"
|
||||
|
||||
#: .\cookbook\templates\offline.html:20
|
||||
msgid ""
|
||||
@@ -1366,7 +1403,7 @@ msgstr "Stil"
|
||||
|
||||
#: .\cookbook\templates\settings.html:79
|
||||
msgid "API Token"
|
||||
msgstr "API-symbol"
|
||||
msgstr "API nøkkel"
|
||||
|
||||
#: .\cookbook\templates\settings.html:80
|
||||
msgid ""
|
||||
@@ -1389,9 +1426,8 @@ msgid "Cookbook Setup"
|
||||
msgstr "Kokeboksoppsett"
|
||||
|
||||
#: .\cookbook\templates\setup.html:14
|
||||
#, fuzzy
|
||||
msgid "Setup"
|
||||
msgstr "Sett opp"
|
||||
msgstr "Installering"
|
||||
|
||||
#: .\cookbook\templates\setup.html:15
|
||||
msgid ""
|
||||
@@ -1424,11 +1460,11 @@ msgstr "Mengde"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:226
|
||||
msgid "Supermarket"
|
||||
msgstr "Matbutikk"
|
||||
msgstr "Butikk"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:236
|
||||
msgid "Select Supermarket"
|
||||
msgstr "Velg matbutikk"
|
||||
msgstr "Velg butikk"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:260
|
||||
msgid "Select User"
|
||||
@@ -1540,7 +1576,6 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\system.html:49 .\cookbook\templates\system.html:64
|
||||
#: .\cookbook\templates\system.html:80 .\cookbook\templates\system.html:95
|
||||
#, fuzzy
|
||||
msgid "Ok"
|
||||
msgstr "OK"
|
||||
|
||||
|
||||
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-04-12 11:55+0000\n"
|
||||
"Last-Translator: noxonad <noxonad@proton.me>\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"
|
||||
@@ -2208,7 +2208,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\url_import.html:38
|
||||
msgid "URL"
|
||||
msgstr ""
|
||||
msgstr "URL"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:40
|
||||
msgid "App"
|
||||
@@ -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 ""
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -8,17 +8,17 @@ 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: 2023-04-12 11:55+0000\n"
|
||||
"Last-Translator: noxonad <noxonad@proton.me>\n"
|
||||
"Language-Team: Russian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/ru/>\n"
|
||||
"Language: ru\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"
|
||||
"X-Generator: Weblate 4.10.1\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\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"
|
||||
@@ -860,7 +861,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\base.html:220
|
||||
msgid "GitHub"
|
||||
msgstr ""
|
||||
msgstr "GitHub"
|
||||
|
||||
#: .\cookbook\templates\base.html:224
|
||||
msgid "API Browser"
|
||||
@@ -1936,7 +1937,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\space.html:106
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
msgstr "пользователь"
|
||||
|
||||
#: .\cookbook\templates\space.html:107
|
||||
msgid "guest"
|
||||
|
||||
Binary file not shown.
@@ -8,17 +8,17 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"PO-Revision-Date: 2022-02-02 15:31+0000\n"
|
||||
"Last-Translator: Mario Dvorsek <mario.dvorsek@gmail.com>\n"
|
||||
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
|
||||
"Last-Translator: noxonad <noxonad@proton.me>\n"
|
||||
"Language-Team: Slovenian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/sl/>\n"
|
||||
"Language: sl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
|
||||
"%100==4 ? 2 : 3;\n"
|
||||
"X-Generator: Weblate 4.10.1\n"
|
||||
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || "
|
||||
"n%100==4 ? 2 : 3;\n"
|
||||
"X-Generator: Weblate 4.15\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
|
||||
#: .\cookbook\templates\forms\ingredients.html:34
|
||||
@@ -2107,7 +2107,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\url_import.html:36
|
||||
msgid "URL"
|
||||
msgstr ""
|
||||
msgstr "URL"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:38
|
||||
msgid "App"
|
||||
|
||||
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
Binary file not shown.
@@ -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-04-12 11:55+0000\n"
|
||||
"Last-Translator: noxonad <noxonad@proton.me>\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
|
||||
@@ -1089,7 +1091,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\base.html:311
|
||||
msgid "GitHub"
|
||||
msgstr ""
|
||||
msgstr "GitHub"
|
||||
|
||||
#: .\cookbook\templates\base.html:313
|
||||
msgid "Translate Tandoor"
|
||||
@@ -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
Binary file not shown.
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user