mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-25 11:19:39 -05:00
Compare commits
589 Commits
2.0.0-beta
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc40b5220b | ||
|
|
0046233b6f | ||
|
|
78ede7b601 | ||
|
|
7e7e133604 | ||
|
|
b0ec569a00 | ||
|
|
7674084ae0 | ||
|
|
798e2ac48b | ||
|
|
714d4a32a9 | ||
|
|
66b5097872 | ||
|
|
c1d4fed142 | ||
|
|
c0d172574d | ||
|
|
1cc0806729 | ||
|
|
09487a0e94 | ||
|
|
89e58edcad | ||
|
|
887d7fe9f0 | ||
|
|
14696e3ce8 | ||
|
|
dd56bb4b35 | ||
|
|
8ec0ba9541 | ||
|
|
c105c9190e | ||
|
|
9c1700adb9 | ||
|
|
b43a87a7e3 | ||
|
|
01e78baecf | ||
|
|
0ee241524d | ||
|
|
84fd3055ea | ||
|
|
2bd60a6f13 | ||
|
|
e0196f17da | ||
|
|
fe75052baa | ||
|
|
0a3b750294 | ||
|
|
fcd3918b5f | ||
|
|
e7aac06ca7 | ||
|
|
67e1f57723 | ||
|
|
10581329e8 | ||
|
|
553c06f291 | ||
|
|
e0cbfd824c | ||
|
|
a5a522d378 | ||
|
|
8502bb235b | ||
|
|
42f2ad624f | ||
|
|
3b95bf40da | ||
|
|
81a6837b06 | ||
|
|
a95e352250 | ||
|
|
a4ca66d287 | ||
|
|
ec30b81ae5 | ||
|
|
92211b1f51 | ||
|
|
8eeea42057 | ||
|
|
7e76a71ccc | ||
|
|
b397c94f0a | ||
|
|
9552564e59 | ||
|
|
4ecf323e4f | ||
|
|
d5d5c2c52b | ||
|
|
7ffabfe711 | ||
|
|
49e0b5b962 | ||
|
|
a05f1ece24 | ||
|
|
748b91bb8a | ||
|
|
bd2e9cc3d9 | ||
|
|
c40bb20a7a | ||
|
|
b377d2cd35 | ||
|
|
dc0e91d0f9 | ||
|
|
5f12907544 | ||
|
|
889ddac7dc | ||
|
|
b369e2618a | ||
|
|
5a4e0204c9 | ||
|
|
bfc2e96b54 | ||
|
|
f065ef80aa | ||
|
|
61c14b8b05 | ||
|
|
35d5d64809 | ||
|
|
63c711d18c | ||
|
|
59e3ea70d1 | ||
|
|
6771662a9f | ||
|
|
9b792a1393 | ||
|
|
862957c121 | ||
|
|
bdcbafd52f | ||
|
|
5e454a5212 | ||
|
|
20bea63997 | ||
|
|
8a265772c0 | ||
|
|
6febb4e3e8 | ||
|
|
04f9167fd8 | ||
|
|
8f29e01daf | ||
|
|
e810363b22 | ||
|
|
b5a2120bdf | ||
|
|
643fcbad9b | ||
|
|
4a3b834463 | ||
|
|
003149133a | ||
|
|
a43de0ca4d | ||
|
|
e05aaed75c | ||
|
|
4984e3e31b | ||
|
|
11dce4c6ad | ||
|
|
8d0d338ea2 | ||
|
|
d22b5a4a39 | ||
|
|
d09e629415 | ||
|
|
53ef2ef99f | ||
|
|
d7b26d1b29 | ||
|
|
602f0a8bf0 | ||
|
|
673d12d233 | ||
|
|
6359245925 | ||
|
|
a7c4822322 | ||
|
|
e94419f320 | ||
|
|
01f46483ff | ||
|
|
d6da5688af | ||
|
|
680ae39201 | ||
|
|
2472ee9c26 | ||
|
|
4428b06d4a | ||
|
|
e9c38d7d5e | ||
|
|
6f28d58807 | ||
|
|
88db611f0a | ||
|
|
f3302b4014 | ||
|
|
d4bb161275 | ||
|
|
32f1538938 | ||
|
|
029baea4c7 | ||
|
|
38d1b7cef5 | ||
|
|
856f417d1b | ||
|
|
85821bcc94 | ||
|
|
2345af8fd6 | ||
|
|
51107c64ee | ||
|
|
81983c5ae2 | ||
|
|
f7713a43a7 | ||
|
|
ffd951a7f4 | ||
|
|
319ac8e191 | ||
|
|
e292b72e34 | ||
|
|
4e795ecf55 | ||
|
|
e3c2a66723 | ||
|
|
eec3e97f97 | ||
|
|
3f481d6922 | ||
|
|
0810ab7210 | ||
|
|
abd621145c | ||
|
|
7d218aa93d | ||
|
|
1b41bd9115 | ||
|
|
aea247b4a3 | ||
|
|
e2843bb02f | ||
|
|
e3aa3e1137 | ||
|
|
da1187b03a | ||
|
|
f9ed79978c | ||
|
|
920a3ed4a3 | ||
|
|
2077eae142 | ||
|
|
b1ef35e415 | ||
|
|
0a687d840c | ||
|
|
6a3034b966 | ||
|
|
3d7afbfe4f | ||
|
|
02e43730bd | ||
|
|
6adf077ee5 | ||
|
|
d73ffa46ff | ||
|
|
8572f338ad | ||
|
|
920ec8e74b | ||
|
|
2328bf2342 | ||
|
|
85620a1431 | ||
|
|
0037858885 | ||
|
|
9df3ff0028 | ||
|
|
0a43272126 | ||
|
|
ff96eb194f | ||
|
|
6b69c4184b | ||
|
|
e90e21181c | ||
|
|
5237228673 | ||
|
|
ecb3172085 | ||
|
|
b4f4e9fd2b | ||
|
|
6d0f3b99c8 | ||
|
|
cdb94ae628 | ||
|
|
0d589444fd | ||
|
|
95fa420c3a | ||
|
|
dd4dc1083f | ||
|
|
04f889b742 | ||
|
|
67d374c071 | ||
|
|
8d749e351d | ||
|
|
417ffcab5d | ||
|
|
d4f654554b | ||
|
|
c8115545b8 | ||
|
|
6dbf0871ec | ||
|
|
f1c5c8bc43 | ||
|
|
22e0108992 | ||
|
|
e2e05c8d1d | ||
|
|
b02b36812d | ||
|
|
0c0012aab8 | ||
|
|
e562883da3 | ||
|
|
a81bc335cc | ||
|
|
ebee1ccd4b | ||
|
|
b1104b4581 | ||
|
|
f5e952d88c | ||
|
|
968fcc3936 | ||
|
|
73d3d87217 | ||
|
|
9050f648f9 | ||
|
|
a4a9e104b5 | ||
|
|
3ed85ea0c4 | ||
|
|
da8ceb7abe | ||
|
|
5ff3a6bb2e | ||
|
|
3ed750b330 | ||
|
|
0315911802 | ||
|
|
1fe96f2b3d | ||
|
|
11761c0b15 | ||
|
|
a7b0a1ab30 | ||
|
|
e4fcae3b00 | ||
|
|
b0401639f1 | ||
|
|
5f8770f502 | ||
|
|
aaa627d3b6 | ||
|
|
e77734f696 | ||
|
|
73df6bb961 | ||
|
|
7d187b638e | ||
|
|
68bb750f8c | ||
|
|
1e35035540 | ||
|
|
561ba2f1da | ||
|
|
bd600301f9 | ||
|
|
cf5483a4d9 | ||
|
|
ba417c49dd | ||
|
|
0259b1dc08 | ||
|
|
34553dadd7 | ||
|
|
535b88c8db | ||
|
|
71eb8818b5 | ||
|
|
0dc94a817f | ||
|
|
4df862c7f3 | ||
|
|
69f013c980 | ||
|
|
23c420dda8 | ||
|
|
63daf1e958 | ||
|
|
52b44eacdd | ||
|
|
fa7cc12b99 | ||
|
|
64d2108ef6 | ||
|
|
dccfdcc11c | ||
|
|
974e72631d | ||
|
|
70f31b8553 | ||
|
|
3cb980c0e7 | ||
|
|
b8a403b7c1 | ||
|
|
b037d90220 | ||
|
|
ad32e457fa | ||
|
|
8e2726caeb | ||
|
|
e693737c57 | ||
|
|
9f239c06d3 | ||
|
|
0f551c5f88 | ||
|
|
eb224a769d | ||
|
|
4515eba9d7 | ||
|
|
30b37bf0b6 | ||
|
|
f17207e56e | ||
|
|
2cba0e18af | ||
|
|
ec6e81316a | ||
|
|
b72897b222 | ||
|
|
bca1ebbf99 | ||
|
|
f0342d4568 | ||
|
|
81f62de500 | ||
|
|
f783949a61 | ||
|
|
820fad1b5c | ||
|
|
1169abd942 | ||
|
|
48e175f58f | ||
|
|
5450e18342 | ||
|
|
ea590f8e49 | ||
|
|
13626ca11b | ||
|
|
f53fe1e3c4 | ||
|
|
d177316b47 | ||
|
|
338db1fac2 | ||
|
|
377619473c | ||
|
|
000962c5bb | ||
|
|
9228c1d59f | ||
|
|
27007de7a0 | ||
|
|
29c99b66a1 | ||
|
|
bc179f430d | ||
|
|
58c412ad95 | ||
|
|
4f248afe76 | ||
|
|
f722d24eaa | ||
|
|
723b74509f | ||
|
|
ad4b1393dd | ||
|
|
04bab7072c | ||
|
|
6391cee9eb | ||
|
|
14884fc0d4 | ||
|
|
f2191f79dd | ||
|
|
c2533d9ea2 | ||
|
|
db72fdb1bb | ||
|
|
78252662cb | ||
|
|
4e078bf477 | ||
|
|
2e9e226fe0 | ||
|
|
18cfbd80ab | ||
|
|
4d284b4fff | ||
|
|
b1128dd134 | ||
|
|
3aebf58406 | ||
|
|
f3816a77df | ||
|
|
e4183d79ab | ||
|
|
f4aa1a083f | ||
|
|
ed5508b576 | ||
|
|
040e247487 | ||
|
|
5d28c7b17d | ||
|
|
15b2df07f2 | ||
|
|
ed8f97e9e0 | ||
|
|
034f68fc28 | ||
|
|
0158087a0b | ||
|
|
cb6bfd741d | ||
|
|
afeee5f7cb | ||
|
|
b43d6e08d4 | ||
|
|
1188624376 | ||
|
|
9ac837c969 | ||
|
|
fc4b017d30 | ||
|
|
4636ac28f9 | ||
|
|
397912e87f | ||
|
|
d0b860e623 | ||
|
|
8a90ed1274 | ||
|
|
163c2a53b6 | ||
|
|
286d707347 | ||
|
|
98d308aee9 | ||
|
|
a7c5240227 | ||
|
|
75fcff8e70 | ||
|
|
2f27cf4deb | ||
|
|
686b595f45 | ||
|
|
0f9f9e8f7c | ||
|
|
7be7c5b954 | ||
|
|
0853a9ec64 | ||
|
|
fa3daee965 | ||
|
|
aba45657c3 | ||
|
|
e6abdf8cd4 | ||
|
|
6cedde7b2d | ||
|
|
741e9eb370 | ||
|
|
7db523d8c4 | ||
|
|
41f0060c43 | ||
|
|
5572833f64 | ||
|
|
780e441a3b | ||
|
|
c4fd2d0b4e | ||
|
|
1c6618f452 | ||
|
|
8c96a75a1e | ||
|
|
44baa8322c | ||
|
|
0fbb95438a | ||
|
|
c56dd9563c | ||
|
|
0008b7c975 | ||
|
|
524f086cc5 | ||
|
|
8550387e0c | ||
|
|
1618f8df79 | ||
|
|
22dfb2a410 | ||
|
|
f099e2e5d3 | ||
|
|
6973c65142 | ||
|
|
774c05e76f | ||
|
|
b08c39e284 | ||
|
|
ae036cfa9a | ||
|
|
37628c1735 | ||
|
|
530a6db35c | ||
|
|
2930093da0 | ||
|
|
b7e63a466b | ||
|
|
a01f86a14e | ||
|
|
9704268fdc | ||
|
|
84cc4c1165 | ||
|
|
5cb70becb8 | ||
|
|
5f99abf459 | ||
|
|
4a8ddce391 | ||
|
|
9a14a87c27 | ||
|
|
c01634f9bd | ||
|
|
f055df3b4d | ||
|
|
a83f474d70 | ||
|
|
63d358df36 | ||
|
|
e70548fcc0 | ||
|
|
17b03905e6 | ||
|
|
90403e6a13 | ||
|
|
db400cae25 | ||
|
|
0f8eee4e0f | ||
|
|
1f532f6276 | ||
|
|
b32715e493 | ||
|
|
0d19e12118 | ||
|
|
96e5213fa6 | ||
|
|
44c567d20b | ||
|
|
3c920593cf | ||
|
|
1d90f8b6f1 | ||
|
|
6b1217ec35 | ||
|
|
a71564a424 | ||
|
|
76c2e144fc | ||
|
|
981353380c | ||
|
|
96a520b1af | ||
|
|
05f537dc6b | ||
|
|
948d8da3b1 | ||
|
|
f8e4b39d88 | ||
|
|
6c498f7dac | ||
|
|
d25702b717 | ||
|
|
aca18fcbe0 | ||
|
|
98b57d2854 | ||
|
|
5e1c804fd1 | ||
|
|
a35c92439c | ||
|
|
eed09a7891 | ||
|
|
a30deb4bae | ||
|
|
45a567856a | ||
|
|
7065d96f90 | ||
|
|
f8cd42dec9 | ||
|
|
8d736c0f88 | ||
|
|
8183e350c9 | ||
|
|
4438bfcb89 | ||
|
|
f42b2cfd31 | ||
|
|
09131e8eae | ||
|
|
f5f001b3d2 | ||
|
|
7f8587922d | ||
|
|
a3460bc023 | ||
|
|
5faa74a75d | ||
|
|
65dbc643d3 | ||
|
|
f0b169647b | ||
|
|
d786ee09fa | ||
|
|
a46f3958fe | ||
|
|
6c17937313 | ||
|
|
a26835ccc4 | ||
|
|
86fc4aa2d0 | ||
|
|
4bd3da451d | ||
|
|
0003405e98 | ||
|
|
b586794337 | ||
|
|
460cb43113 | ||
|
|
5128fcc9eb | ||
|
|
243ff8601c | ||
|
|
97f8d46afb | ||
|
|
e469ebf35e | ||
|
|
e04c729476 | ||
|
|
d98bf9155d | ||
|
|
e98d00a962 | ||
|
|
cf5f896cec | ||
|
|
e8d616ac98 | ||
|
|
7a22d43959 | ||
|
|
6b68f48227 | ||
|
|
115f18889a | ||
|
|
0aaffb7545 | ||
|
|
087cbdade8 | ||
|
|
7e55115a3a | ||
|
|
31ee55a113 | ||
|
|
61be55e4b7 | ||
|
|
e3f695bde1 | ||
|
|
0fb3d22f6a | ||
|
|
7ba5187ecf | ||
|
|
168c0f3a0d | ||
|
|
1179e226ab | ||
|
|
bed22c055d | ||
|
|
c25a1df480 | ||
|
|
d1df772218 | ||
|
|
cbdd23020b | ||
|
|
10f8a56343 | ||
|
|
006c5b3af8 | ||
|
|
562a0dceae | ||
|
|
cde03a0f33 | ||
|
|
b42285a9a5 | ||
|
|
f4d4a5b714 | ||
|
|
ee7d611086 | ||
|
|
e51fda5f20 | ||
|
|
bee759e166 | ||
|
|
5802dfd0a5 | ||
|
|
c18ce7635d | ||
|
|
942a8a6119 | ||
|
|
4015edde90 | ||
|
|
1c32940f5c | ||
|
|
447ffa9fe2 | ||
|
|
8480234592 | ||
|
|
2e0345a4a8 | ||
|
|
49fc0cf80f | ||
|
|
c67ecb6e31 | ||
|
|
b4f12c4e84 | ||
|
|
0b2adf5249 | ||
|
|
7dcb5884d9 | ||
|
|
35bd550101 | ||
|
|
707abfacb0 | ||
|
|
ed4f4c77e8 | ||
|
|
c492fb513b | ||
|
|
310b8e04e1 | ||
|
|
efeae4debc | ||
|
|
6bc25c32ff | ||
|
|
7f2b0438fe | ||
|
|
8481f8c658 | ||
|
|
d842795c25 | ||
|
|
58dd700207 | ||
|
|
1331d2cb6d | ||
|
|
ad2a613fd8 | ||
|
|
0565189580 | ||
|
|
5aa351b885 | ||
|
|
d5226eb5cf | ||
|
|
9ead1d0022 | ||
|
|
67342c3ba9 | ||
|
|
3fecd82cd0 | ||
|
|
a033c4290f | ||
|
|
b6597af0d7 | ||
|
|
af6ed4bd24 | ||
|
|
cc4bddb3fe | ||
|
|
95a9df9c05 | ||
|
|
c44de28c2c | ||
|
|
9f1b87fa4f | ||
|
|
b96e0bab11 | ||
|
|
fe97fb371b | ||
|
|
bb7df960cc | ||
|
|
c3c7d803dc | ||
|
|
99ce3327cc | ||
|
|
d1949df23d | ||
|
|
119b47c3c4 | ||
|
|
8b50c59ad3 | ||
|
|
e2ac65467b | ||
|
|
c5cc492f0a | ||
|
|
8c73b5254c | ||
|
|
4b0315ffd3 | ||
|
|
6a96f5b7c5 | ||
|
|
8875dd4083 | ||
|
|
7299f265d3 | ||
|
|
cac186f63c | ||
|
|
fd4c571e48 | ||
|
|
db99450475 | ||
|
|
ec50add571 | ||
|
|
1fc3746619 | ||
|
|
debf4c124a | ||
|
|
0e071255e5 | ||
|
|
4d0b8c690b | ||
|
|
85b3e0a0a6 | ||
|
|
d8573ce16f | ||
|
|
7f7e3180fa | ||
|
|
57cc6feef0 | ||
|
|
fb198a80d2 | ||
|
|
5b324a86dc | ||
|
|
f633274bef | ||
|
|
65b2eb6d7e | ||
|
|
0c509ec02e | ||
|
|
babcddeeb1 | ||
|
|
f23b282f27 | ||
|
|
fcfedd3026 | ||
|
|
efd65c1024 | ||
|
|
6f4f5381ff | ||
|
|
95b63f5180 | ||
|
|
f33a52a94c | ||
|
|
90baf26eb8 | ||
|
|
9119d773f1 | ||
|
|
4ea5cdb8b9 | ||
|
|
f36e5f1d89 | ||
|
|
bce95ff604 | ||
|
|
0f0a5b32cd | ||
|
|
0bd0b794df | ||
|
|
5267ac12b0 | ||
|
|
02678ffe30 | ||
|
|
2907e29a11 | ||
|
|
9d49c4d550 | ||
|
|
e2c6eec628 | ||
|
|
63716e4397 | ||
|
|
27e5955c78 | ||
|
|
e9e6cdccca | ||
|
|
8c8096e348 | ||
|
|
9fcbbc17e8 | ||
|
|
0a2f83cf85 | ||
|
|
01fff0783f | ||
|
|
7ccdb90f9b | ||
|
|
c2e522d9f2 | ||
|
|
92578dd6a2 | ||
|
|
3103f28fc8 | ||
|
|
a5df1275ec | ||
|
|
a4308f9864 | ||
|
|
21526fb676 | ||
|
|
5dc3116c44 | ||
|
|
2a6a87ec16 | ||
|
|
8149b05185 | ||
|
|
61afbbdfbe | ||
|
|
a37455ccda | ||
|
|
6d711aff41 | ||
|
|
d4adb975ec | ||
|
|
9b581d58bd | ||
|
|
79db8a2fe0 | ||
|
|
f722d4751b | ||
|
|
368ed2aaf3 | ||
|
|
50400e1d20 | ||
|
|
750115cab5 | ||
|
|
9d8acdc41f | ||
|
|
7ab36f1a7a | ||
|
|
b8d0e32550 | ||
|
|
d9f0889b36 | ||
|
|
35f40f175c | ||
|
|
291ff86c42 | ||
|
|
d2b0aeab52 | ||
|
|
3cab6e538e | ||
|
|
db67ab6b30 | ||
|
|
b5b31b3dc6 | ||
|
|
a15dd2ccbc | ||
|
|
62cc54f9f5 | ||
|
|
75c5bba7e5 | ||
|
|
642a0493af | ||
|
|
8d8e0be328 | ||
|
|
744b588cea | ||
|
|
d3a21b9ff0 | ||
|
|
3a9c40c566 | ||
|
|
387e0a5250 | ||
|
|
4ea28ba22a | ||
|
|
20660f547c | ||
|
|
2ee63d8568 | ||
|
|
2179d7d1f7 | ||
|
|
034d59373f | ||
|
|
d1ad0ade0f | ||
|
|
991089c17a | ||
|
|
54960d8480 | ||
|
|
5fcfe09bb6 | ||
|
|
01c4974507 | ||
|
|
2d57e0dab2 | ||
|
|
d52e5408c0 | ||
|
|
fdce69daf4 | ||
|
|
cb3ffcb12d | ||
|
|
d7342a349b | ||
|
|
794bbed833 | ||
|
|
0b335e80a6 | ||
|
|
2716d72e31 | ||
|
|
fb1de15de6 | ||
|
|
2180f11768 | ||
|
|
1083b7521e | ||
|
|
5a0a5b09a1 | ||
|
|
e698d14ec3 | ||
|
|
0caf2fe77f | ||
|
|
c079f49d71 | ||
|
|
8490ac01cc | ||
|
|
84477ef52a | ||
|
|
b789573de3 | ||
|
|
d5d8e7ce63 | ||
|
|
c7a49458b9 |
@@ -1,7 +1,7 @@
|
||||
FROM python:3.10-alpine3.18
|
||||
FROM python:3.13-alpine3.22
|
||||
|
||||
#Install all dependencies.
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git yarn
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git yarn libgcc libstdc++ nginx tini envsubst nodejs npm
|
||||
|
||||
#Print all logs without buffering it.
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
@@ -19,8 +19,10 @@ RUN \
|
||||
if [ `apk --print-arch` = "armv7" ]; then \
|
||||
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
|
||||
fi
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev && \
|
||||
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
|
||||
pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt && \
|
||||
rm -rf /tmp/pip-tmp && \
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl rust && \
|
||||
python -m pip install --upgrade pip && \
|
||||
pip debug -v && \
|
||||
pip install wheel==0.45.1 && \
|
||||
pip install setuptools_rust==1.10.2 && \
|
||||
pip install -r /tmp/pip-tmp/requirements.txt --no-cache-dir &&\
|
||||
apk --purge del .build-deps
|
||||
@@ -29,3 +29,4 @@ vue/babel.config*
|
||||
vue/package.json
|
||||
vue/tsconfig.json
|
||||
vue/src/utils/openapi
|
||||
venv
|
||||
45
.github/workflows/build-docker.yml
vendored
45
.github/workflows/build-docker.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
suffix: ""
|
||||
continue-on-error: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Get version number
|
||||
id: get_version
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# Build Vue 3 frontend
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: yarn
|
||||
@@ -74,9 +74,8 @@ jobs:
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=${{ matrix.suffix }}
|
||||
# disable latest for tagged releases while in beta
|
||||
# type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
@@ -94,34 +93,34 @@ jobs:
|
||||
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:
|
||||
# - name: Set tag name
|
||||
# run: |
|
||||
# # Strip "refs/tags/" prefix
|
||||
# echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
# # Send stable discord notification
|
||||
# - name: Discord notification
|
||||
# env:
|
||||
# DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
# uses: Ilshidur/action-discord@0.3.2
|
||||
# with:
|
||||
# args: '🚀 Version {{ VERSION }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ VERSION }}'
|
||||
notify-stable:
|
||||
name: Notify Stable
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-container
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Set tag name
|
||||
run: |
|
||||
# Strip "refs/tags/" prefix
|
||||
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
# Send stable discord notification
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.4.0
|
||||
with:
|
||||
args: '🚀 Version {{ VERSION }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ VERSION }}'
|
||||
|
||||
notify-beta:
|
||||
name: Notify Beta
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-container
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
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
|
||||
uses: Ilshidur/action-discord@0.4.0
|
||||
with:
|
||||
args: '🚀 The Tandoor 2 Image has been updated! 🥳'
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -12,15 +12,15 @@ jobs:
|
||||
python-version: ["3.12"]
|
||||
node-version: ["22"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: awalsh128/cache-apt-pkgs-action@v1.4.3
|
||||
- uses: actions/checkout@v5
|
||||
- uses: awalsh128/cache-apt-pkgs-action@v1.6.0
|
||||
with:
|
||||
packages: libsasl2-dev python3-dev libxml2-dev libxmlsec1-dev libxslt-dev libxmlsec1-openssl libxslt-dev libldap2-dev libssl-dev gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl
|
||||
version: 1.0
|
||||
|
||||
# Setup python & dependencies
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "pip"
|
||||
@@ -38,12 +38,12 @@ jobs:
|
||||
./cookbook/static
|
||||
./staticfiles
|
||||
key: |
|
||||
${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.node-version }}-collectstatic-${{ hashFiles('**/*.css', '**/*.js', 'vue/src/*') }}
|
||||
${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.node-version }}-collectstatic-${{ hashFiles('**/*.css', '**/*.js', 'vue3/src/*') }}
|
||||
|
||||
# Build Vue frontend & Dependencies
|
||||
- name: Set up Node ${{ matrix.node-version }}
|
||||
if: steps.django_cache.outputs.cache-hit != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "yarn"
|
||||
|
||||
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@v4
|
||||
uses: actions/checkout@v5
|
||||
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@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
# 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@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
languages: javascript, python
|
||||
|
||||
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
@@ -12,8 +12,8 @@ jobs:
|
||||
if: github.repository_owner == 'TandoorRecipes' && ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: pip install mkdocs-material mkdocs-include-markdown-plugin
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -89,3 +89,6 @@ venv/
|
||||
.idea/easy-i18n.xml
|
||||
cookbook/static/vue3
|
||||
vue3/node_modules
|
||||
cookbook/tests/other/docs/reports/tests/tests.html
|
||||
cookbook/tests/other/docs/reports/tests/pytest.xml
|
||||
vue3/src/plugins
|
||||
|
||||
62
.vscode/tasks.json
vendored
62
.vscode/tasks.json
vendored
@@ -14,28 +14,16 @@
|
||||
},
|
||||
{
|
||||
"label": "Setup Dev Server",
|
||||
"dependsOn": ["Run Migrations", "Yarn Build"]
|
||||
"dependsOn": ["Run Migrations"]
|
||||
},
|
||||
{
|
||||
"label": "Run Dev Server",
|
||||
"type": "shell",
|
||||
"type": "shell",
|
||||
"dependsOn": ["Setup Dev Server"],
|
||||
"command": "python3 manage.py runserver"
|
||||
"command": "DEBUG=1 python3 manage.py runserver"
|
||||
},
|
||||
{
|
||||
"label": "Yarn Install",
|
||||
"dependsOn": ["Yarn Install - Vue", "Yarn Install - Vue3"]
|
||||
},
|
||||
{
|
||||
"label": "Yarn Install - Vue",
|
||||
"type": "shell",
|
||||
"command": "yarn install --force",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Yarn Install - Vue3",
|
||||
"type": "shell",
|
||||
"command": "yarn install --force",
|
||||
"options": {
|
||||
@@ -44,18 +32,6 @@
|
||||
},
|
||||
{
|
||||
"label": "Generate API",
|
||||
"dependsOn": ["Generate API - Vue", "Generate API - Vue3"]
|
||||
},
|
||||
{
|
||||
"label": "Generate API - Vue",
|
||||
"type": "shell",
|
||||
"command": "openapi-generator-cli generate -g typescript-axios -i http://127.0.0.1:8000/openapi/",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue/src/utils/openapi"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Generate API - Vue3",
|
||||
"type": "shell",
|
||||
"command": "openapi-generator-cli generate -g typescript-fetch -i http://127.0.0.1:8000/openapi/",
|
||||
"options": {
|
||||
@@ -63,43 +39,19 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Yarn Serve",
|
||||
"label": "Yarn Dev",
|
||||
"type": "shell",
|
||||
"command": "yarn serve",
|
||||
"dependsOn": ["Yarn Install - Vue"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Vite Serve",
|
||||
"type": "shell",
|
||||
"command": "vite",
|
||||
"dependsOn": ["Yarn Install - Vue3"],
|
||||
"command": "yarn dev",
|
||||
"dependsOn": ["Yarn Install"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Yarn Build",
|
||||
"dependsOn": ["Yarn Build - Vue", "Vite Build - Vue3"],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "Yarn Build - Vue",
|
||||
"type": "shell",
|
||||
"command": "yarn build",
|
||||
"dependsOn": ["Yarn Install - Vue"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "Vite Build - Vue3",
|
||||
"type": "shell",
|
||||
"command": "vite build",
|
||||
"dependsOn": ["Yarn Install - Vue3"],
|
||||
"dependsOn": ["Yarn Install"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/vue3"
|
||||
},
|
||||
|
||||
31
Dockerfile
31
Dockerfile
@@ -1,15 +1,14 @@
|
||||
FROM python:3.13-alpine3.21
|
||||
FROM python:3.13-alpine3.22
|
||||
|
||||
#Install all dependencies.
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git libgcc libstdc++
|
||||
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git libgcc libstdc++ nginx tini envsubst nodejs npm
|
||||
|
||||
#Print all logs without buffering it.
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
ENV DOCKER true
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
DOCKER=true
|
||||
|
||||
#This port will be used by gunicorn.
|
||||
EXPOSE 8080
|
||||
EXPOSE 80 8080
|
||||
|
||||
#Create app dir and install requirements.
|
||||
RUN mkdir /opt/recipes
|
||||
@@ -17,29 +16,27 @@ WORKDIR /opt/recipes
|
||||
|
||||
COPY requirements.txt ./
|
||||
|
||||
RUN \
|
||||
if [ `apk --print-arch` = "armv7" ]; then \
|
||||
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
|
||||
fi
|
||||
|
||||
# remove Development dependencies from requirements.txt
|
||||
RUN sed -i '/# Development/,$d' requirements.txt
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl && \
|
||||
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl rust && \
|
||||
python -m venv venv && \
|
||||
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
|
||||
venv/bin/pip debug -v && \
|
||||
venv/bin/pip install wheel==0.45.1 && \
|
||||
venv/bin/pip install setuptools_rust==1.10.2 && \
|
||||
if [ `apk --print-arch` = "aarch64" ]; then \
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y; \
|
||||
fi &&\
|
||||
venv/bin/pip install -r requirements.txt --no-cache-dir &&\
|
||||
apk --purge del .build-deps
|
||||
|
||||
#Copy project and execute it.
|
||||
COPY . ./
|
||||
|
||||
# delete default nginx config and link it to tandoors config
|
||||
# create symlinks to access and error log to show them on stdout
|
||||
RUN rm -rf /etc/nginx/http.d && \
|
||||
ln -s /opt/recipes/http.d /etc/nginx/http.d && \
|
||||
ln -sf /dev/stdout /var/log/nginx/access.log && \
|
||||
ln -sf /dev/stderr /var/log/nginx/error.log
|
||||
|
||||
# commented for now https://github.com/TandoorRecipes/recipes/issues/3478
|
||||
#HEALTHCHECK --interval=30s \
|
||||
# --timeout=5s \
|
||||
@@ -53,4 +50,4 @@ RUN /opt/recipes/venv/bin/python version.py
|
||||
RUN find . -type d -name ".git" | xargs rm -rf
|
||||
|
||||
RUN chmod +x boot.sh
|
||||
ENTRYPOINT ["/opt/recipes/boot.sh"]
|
||||
ENTRYPOINT ["/sbin/tini", "--", "/opt/recipes/boot.sh"]
|
||||
|
||||
18
README.md
18
README.md
@@ -15,23 +15,26 @@
|
||||
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
|
||||
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
|
||||
<a href="https://app.tandoor.dev/accounts/login/?demo" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
|
||||
<a href="https://app.tandoor.dev/e/demo-auto-login/" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://tandoor.dev" target="_blank" rel="noopener noreferrer">Website</a> •
|
||||
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
|
||||
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Docs</a> •
|
||||
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a> •
|
||||
<a href="https://app.tandoor.dev/e/demo-auto-login/" target="_blank" rel="noopener noreferrer">Demo</a> •
|
||||
<a href="https://community.tandoor.dev" target="_blank" rel="noopener noreferrer">Community</a> •
|
||||
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
## Core Features
|
||||
|
||||
- 🥗 **Manage your recipes** - Manage your ever growing recipe collection
|
||||
- 📆 **Plan** - multiple meals for each day
|
||||
- 🛒 **Shopping lists** - via the meal plan or straight from recipes
|
||||
- 🪄 **use AI** to recognize images, sort recipe steps, find nutrition facts and more
|
||||
- 📚 **Cookbooks** - collect recipes into books
|
||||
- 👪 **Share and collaborate** on recipes with friends and family
|
||||
|
||||
@@ -61,12 +64,13 @@ a public page.
|
||||
|
||||
Documentation can be found [here](https://docs.tandoor.dev/).
|
||||
|
||||
## Support our work
|
||||
## ❤️ Support our work ❤️
|
||||
Tandoor is developed by volunteers in their free time just because its fun. That said earning
|
||||
some money with the project allows us to spend more time on it and thus make improvements we otherwise couldn't.
|
||||
Because of that there are several ways you can support us
|
||||
|
||||
- **GitHub Sponsors** You can sponsor contributors of this project on GitHub: [vabene1111](https://github.com/sponsors/vabene1111)
|
||||
- **Patron** You can sponsor contributors of this project on Patron: [vabene111](https://www.patreon.com/cw/vabene1111)
|
||||
- **Host at Hetzner** We have been very happy customers of Hetzner for multiple years for all of our projects. If you want to get into self-hosting or are tired of the expensive big providers, their cloud servers are a great place to get started. When you sign up via our [referral link](https://hetzner.cloud/?ref=ISdlrLmr9kGj) you will get 20€ worth of cloud credits and we get a small kickback too.
|
||||
- **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).
|
||||
|
||||
@@ -81,13 +85,13 @@ Share some information on how you use Tandoor to help me improve the application
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="https://discord.gg/RhzBrfWgtp">Discord</a></td>
|
||||
<td>We have a public Discord server that anyone can join. This is where all our developers and contributors hang out and where we make announcements</td>
|
||||
<td><a href="https://community.tandoor.dev">Community</a></td>
|
||||
<td>Get support, share best practices, discuss feature ideas, and meet other Tandoor users.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><a href="https://twitter.com/TandoorRecipes">Twitter</a></td>
|
||||
<td>You can follow our Twitter account to get updates on new features or releases</td>
|
||||
<td><a href="https://discord.gg/RhzBrfWgtp">Discord</a></td>
|
||||
<td>We have a public Discord server that anyone can join. This is where all our developers and contributors hang out and where we make announcements</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
41
boot.sh
Normal file → Executable file
41
boot.sh
Normal file → Executable file
@@ -1,23 +1,30 @@
|
||||
#!/bin/sh
|
||||
source venv/bin/activate
|
||||
|
||||
TANDOOR_PORT="${TANDOOR_PORT:-8080}"
|
||||
# these are envsubst in the nginx config, make sure they default to something sensible when unset
|
||||
export TANDOOR_PORT="${TANDOOR_PORT:-80}"
|
||||
export MEDIA_ROOT=${MEDIA_ROOT:-/opt/recipes/mediafiles};
|
||||
export STATIC_ROOT=${STATIC_ROOT:-/opt/recipes/staticfiles};
|
||||
|
||||
GUNICORN_WORKERS="${GUNICORN_WORKERS:-3}"
|
||||
GUNICORN_THREADS="${GUNICORN_THREADS:-2}"
|
||||
GUNICORN_LOG_LEVEL="${GUNICORN_LOG_LEVEL:-'info'}"
|
||||
NGINX_CONF_FILE=/opt/recipes/nginx/conf.d/Recipes.conf
|
||||
|
||||
PLUGINS_BUILD="${PLUGINS_BUILD:-0}"
|
||||
|
||||
display_warning() {
|
||||
echo "[WARNING]"
|
||||
echo -e "$1"
|
||||
}
|
||||
|
||||
echo "Checking configuration..."
|
||||
# prepare nginx config
|
||||
envsubst '$MEDIA_ROOT $STATIC_ROOT $TANDOOR_PORT' < /opt/recipes/http.d/Recipes.conf.template > /opt/recipes/http.d/Recipes.conf
|
||||
|
||||
# Nginx config file must exist if gunicorn is not active
|
||||
if [ ! -f "$NGINX_CONF_FILE" ] && [ $GUNICORN_MEDIA -eq 0 ]; then
|
||||
display_warning "Nginx configuration file could not be found at the default location!\nPath: ${NGINX_CONF_FILE}"
|
||||
fi
|
||||
# start nginx early to display error pages
|
||||
echo "Starting nginx"
|
||||
nginx
|
||||
|
||||
echo "Checking configuration..."
|
||||
|
||||
# SECRET_KEY (or a valid file at SECRET_KEY_FILE) must be set in .env file
|
||||
|
||||
@@ -79,23 +86,23 @@ echo "Database is ready"
|
||||
|
||||
echo "Migrating database"
|
||||
|
||||
|
||||
python manage.py migrate
|
||||
|
||||
if [ "${PLUGINS_BUILD}" -eq 1 ]; then
|
||||
echo "Running yarn build at startup because PLUGINS_BUILD is enabled"
|
||||
python plugin.py
|
||||
fi
|
||||
|
||||
echo "Collecting static files, this may take a while..."
|
||||
|
||||
python manage.py collectstatic_js_reverse
|
||||
python manage.py collectstatic --noinput
|
||||
python manage.py collectstatic --noinput --clear
|
||||
|
||||
echo "Done"
|
||||
|
||||
chmod -R 755 /opt/recipes/mediafiles
|
||||
chmod -R 755 ${MEDIA_ROOT:-/opt/recipes/mediafiles}
|
||||
|
||||
ipv6_disable=$(cat /sys/module/ipv6/parameters/disable)
|
||||
|
||||
# Check if IPv6 is enabled, only then run gunicorn with ipv6 support
|
||||
if [ "$ipv6_disable" -eq 0 ]; then
|
||||
exec gunicorn -b "[::]:$TANDOOR_PORT" --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi
|
||||
else
|
||||
exec gunicorn -b ":$TANDOOR_PORT" --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi
|
||||
fi
|
||||
echo "Starting gunicorn"
|
||||
exec gunicorn --bind unix:/run/tandoor.sock --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --timeout ${GUNICORN_TIMEOUT:-30} --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from .models import (BookmarkletImport, Comment, CookLog, CustomFilter, Food, Im
|
||||
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
|
||||
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
|
||||
TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
|
||||
ViewLog, ConnectorConfig)
|
||||
ViewLog, ConnectorConfig, AiProvider, AiLog)
|
||||
|
||||
admin.site.login = secure_admin_login(admin.site.login)
|
||||
|
||||
@@ -90,6 +90,20 @@ class SearchPreferenceAdmin(admin.ModelAdmin):
|
||||
admin.site.register(SearchPreference, SearchPreferenceAdmin)
|
||||
|
||||
|
||||
class AiProviderAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'space', 'model_name',)
|
||||
search_fields = ('name', 'space', 'model_name',)
|
||||
|
||||
|
||||
admin.site.register(AiProvider, AiProviderAdmin)
|
||||
|
||||
|
||||
class AiLogAdmin(admin.ModelAdmin):
|
||||
list_display = ('ai_provider', 'function', 'credit_cost', 'created_by', 'created_at',)
|
||||
|
||||
admin.site.register(AiLog, AiLogAdmin)
|
||||
|
||||
|
||||
class StorageAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'method')
|
||||
search_fields = ('name',)
|
||||
|
||||
@@ -26,6 +26,7 @@ class ImportExportBase(forms.Form):
|
||||
PAPRIKA = 'PAPRIKA'
|
||||
NEXTCLOUD = 'NEXTCLOUD'
|
||||
MEALIE = 'MEALIE'
|
||||
MEALIE1 = 'MEALIE1'
|
||||
CHOWDOWN = 'CHOWDOWN'
|
||||
SAFFRON = 'SAFFRON'
|
||||
CHEFTAP = 'CHEFTAP'
|
||||
@@ -46,7 +47,7 @@ class ImportExportBase(forms.Form):
|
||||
PDF = 'PDF'
|
||||
GOURMET = 'GOURMET'
|
||||
|
||||
type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'),
|
||||
type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (MEALIE1, 'Mealie1'), (CHOWDOWN, 'Chowdown'),
|
||||
(SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'), (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'),
|
||||
@@ -75,6 +76,11 @@ class ImportForm(ImportExportBase):
|
||||
files = MultipleFileField(required=True)
|
||||
duplicates = forms.BooleanField(help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'),
|
||||
required=False)
|
||||
meal_plans = forms.BooleanField(required=False)
|
||||
shopping_lists = forms.BooleanField(required=False)
|
||||
nutrition_per_serving = forms.BooleanField(required=False) # some managers (e.g. mealie) do not specify what the nutrition's relate to so we let the user choose
|
||||
|
||||
|
||||
class ExportForm(ImportExportBase):
|
||||
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False)
|
||||
all = forms.BooleanField(required=False)
|
||||
|
||||
85
cookbook/helper/ai_helper.py
Normal file
85
cookbook/helper/ai_helper.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.utils import timezone
|
||||
from django.db.models import Sum
|
||||
from litellm import CustomLogger
|
||||
|
||||
from cookbook.models import AiLog
|
||||
from recipes import settings
|
||||
|
||||
|
||||
def get_monthly_token_usage(space):
|
||||
"""
|
||||
returns the number of credits the space has used in the current month
|
||||
"""
|
||||
token_usage = AiLog.objects.filter(space=space, credits_from_balance=False, created_at__month=timezone.now().month).aggregate(Sum('credit_cost'))['credit_cost__sum']
|
||||
if token_usage is None:
|
||||
token_usage = 0
|
||||
return token_usage
|
||||
|
||||
|
||||
def has_monthly_token(space):
|
||||
"""
|
||||
checks if the monthly credit limit has been exceeded
|
||||
"""
|
||||
return get_monthly_token_usage(space) < space.ai_credits_monthly
|
||||
|
||||
|
||||
def can_perform_ai_request(space):
|
||||
return (has_monthly_token(space) or space.ai_credits_balance > 0) and space.ai_enabled
|
||||
|
||||
|
||||
class AiCallbackHandler(CustomLogger):
|
||||
space = None
|
||||
user = None
|
||||
ai_provider = None
|
||||
function = None
|
||||
|
||||
def __init__(self, space, user, ai_provider, function):
|
||||
super().__init__()
|
||||
self.space = space
|
||||
self.user = user
|
||||
self.ai_provider = ai_provider
|
||||
self.function = function
|
||||
|
||||
def log_pre_api_call(self, model, messages, kwargs):
|
||||
pass
|
||||
|
||||
def log_post_api_call(self, kwargs, response_obj, start_time, end_time):
|
||||
pass
|
||||
|
||||
def log_success_event(self, kwargs, response_obj, start_time, end_time):
|
||||
self.create_ai_log(kwargs, response_obj, start_time, end_time)
|
||||
|
||||
def log_failure_event(self, kwargs, response_obj, start_time, end_time):
|
||||
self.create_ai_log(kwargs, response_obj, start_time, end_time)
|
||||
|
||||
def create_ai_log(self, kwargs, response_obj, start_time, end_time):
|
||||
credit_cost = 0
|
||||
credits_from_balance = False
|
||||
if self.ai_provider.log_credit_cost:
|
||||
credit_cost = kwargs.get("response_cost", 0) * 100
|
||||
|
||||
if (not has_monthly_token(self.space)) and self.space.ai_credits_balance > 0:
|
||||
remaining_balance = self.space.ai_credits_balance - Decimal(str(credit_cost))
|
||||
if remaining_balance < 0:
|
||||
remaining_balance = 0
|
||||
if settings.HOSTED and self.space.ai_credits_monthly == 0:
|
||||
self.space.ai_enabled = False
|
||||
|
||||
self.space.ai_credits_balance = remaining_balance
|
||||
credits_from_balance = True
|
||||
self.space.save()
|
||||
|
||||
AiLog.objects.create(
|
||||
created_by=self.user,
|
||||
space=self.space,
|
||||
ai_provider=self.ai_provider,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
input_tokens=response_obj['usage']['prompt_tokens'],
|
||||
output_tokens=response_obj['usage']['completion_tokens'],
|
||||
function=self.function,
|
||||
credit_cost=credit_cost,
|
||||
credits_from_balance=credits_from_balance,
|
||||
)
|
||||
@@ -109,7 +109,7 @@ class AutomationEngine:
|
||||
Moves a string that should never be treated as a unit to next token and optionally replaced with default unit
|
||||
e.g. NEVER_UNIT: param1: egg, param2: None would modify ['1', 'egg', 'white'] to ['1', '', 'egg', 'white']
|
||||
or NEVER_UNIT: param1: egg, param2: pcs would modify ['1', 'egg', 'yolk'] to ['1', 'pcs', 'egg', 'yolk']
|
||||
:param1 string: string that should never be considered a unit, will be moved to token[2]
|
||||
:param1 tokens: string that should never be considered a unit, will be moved to token[2]
|
||||
:param2 (optional) unit as string: will insert unit string into token[1]
|
||||
:return: unit as string (possibly changed by automation)
|
||||
"""
|
||||
@@ -135,7 +135,7 @@ class AutomationEngine:
|
||||
new_unit = self.never_unit[tokens[1].lower()]
|
||||
never_unit = True
|
||||
except KeyError:
|
||||
return tokens
|
||||
return tokens, never_unit
|
||||
else:
|
||||
if a := Automation.objects.annotate(param_1_lower=Lower('param_1')).filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1_lower__in=[
|
||||
tokens[1].lower(), alt_unit.lower()], disabled=False).order_by('order').first():
|
||||
@@ -144,7 +144,7 @@ class AutomationEngine:
|
||||
|
||||
if never_unit:
|
||||
tokens.insert(1, new_unit)
|
||||
return tokens
|
||||
return tokens, never_unit
|
||||
|
||||
def apply_transpose_automation(self, string):
|
||||
"""
|
||||
|
||||
22
cookbook/helper/batch_edit_helper.py
Normal file
22
cookbook/helper/batch_edit_helper.py
Normal file
@@ -0,0 +1,22 @@
|
||||
def add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
|
||||
"""
|
||||
given a model, the base and related field and the base and related ids, bulk create relation objects
|
||||
"""
|
||||
relation_objects = []
|
||||
for b in base_ids:
|
||||
for r in related_ids:
|
||||
relation_objects.append(relation_model(**{base_field_name: b, related_field_name: r}))
|
||||
relation_model.objects.bulk_create(relation_objects, ignore_conflicts=True, unique_fields=(base_field_name, related_field_name,))
|
||||
|
||||
|
||||
def remove_from_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
|
||||
relation_model.objects.filter(**{f'{base_field_name}__in': base_ids, f'{related_field_name}__in': related_ids}).delete()
|
||||
|
||||
|
||||
def remove_all_from_relation(relation_model, base_field_name, base_ids):
|
||||
relation_model.objects.filter(**{f'{base_field_name}__in': base_ids}).delete()
|
||||
|
||||
|
||||
def set_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids):
|
||||
remove_all_from_relation(relation_model, base_field_name, base_ids)
|
||||
add_to_relation(relation_model, base_field_name, base_ids, related_field_name, related_ids)
|
||||
@@ -37,7 +37,7 @@ def get_filetype(name):
|
||||
|
||||
def is_file_type_allowed(filename, image_only=False):
|
||||
is_file_allowed = False
|
||||
allowed_file_types = ['.pdf', '.docx', '.xlsx', '.css']
|
||||
allowed_file_types = ['.pdf', '.docx', '.xlsx', '.css', '.mp4', '.mov']
|
||||
allowed_image_types = ['.png', '.jpg', '.jpeg', '.gif', '.webp']
|
||||
check_list = allowed_image_types
|
||||
if not image_only:
|
||||
@@ -77,6 +77,8 @@ def handle_image(request, image_object, filetype):
|
||||
file_format = 'JPEG'
|
||||
if filetype == '.png':
|
||||
file_format = 'PNG'
|
||||
if filetype == '.webp':
|
||||
file_format = 'WEBP'
|
||||
|
||||
if (image_object.size / 1000) > 500: # if larger than 500 kb compress
|
||||
if filetype == '.jpeg' or filetype == '.jpg':
|
||||
@@ -84,7 +86,6 @@ def handle_image(request, image_object, filetype):
|
||||
if filetype == '.png':
|
||||
return rescale_image_png(image_object)
|
||||
else:
|
||||
print('STripping image')
|
||||
return strip_image_meta(image_object, file_format)
|
||||
|
||||
# TODO webp and gifs bypass the scaling and metadata checks, fix
|
||||
|
||||
@@ -176,7 +176,6 @@ class IngredientParser:
|
||||
# if something like this is detected move it to the beginning so the parser can handle it
|
||||
if len(ingredient) < 1000 and re.search(r'^([^\W\d_])+(.)*[1-9](\d)*\s*([^\W\d_])+', ingredient):
|
||||
match = re.search(r'[1-9](\d)*\s*([^\W\d_])+', ingredient)
|
||||
print(f'reording from {ingredient} to {ingredient[match.start():match.end()] + " " + ingredient.replace(ingredient[match.start():match.end()], "")}')
|
||||
ingredient = ingredient[match.start():match.end()] + ' ' + ingredient.replace(ingredient[match.start():match.end()], '')
|
||||
|
||||
# if the string contains parenthesis early on remove it and place it at the end
|
||||
@@ -211,39 +210,46 @@ class IngredientParser:
|
||||
# three arguments if it already has a unit there can't be
|
||||
# a fraction for the amount
|
||||
if len(tokens) > 2:
|
||||
never_unit_applied = False
|
||||
if not self.ignore_rules:
|
||||
tokens = self.automation.apply_never_unit_automation(tokens)
|
||||
try:
|
||||
if unit is not None:
|
||||
# a unit is already found, no need to try the second argument for a fraction
|
||||
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except
|
||||
raise ValueError
|
||||
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
|
||||
amount += self.parse_fraction(tokens[1])
|
||||
# assume that units can't end with a comma
|
||||
if len(tokens) > 3 and not tokens[2].endswith(','):
|
||||
# try to use third argument as unit and everything else as food, use everything as food if it fails
|
||||
try:
|
||||
food, note = self.parse_food(tokens[3:])
|
||||
unit = tokens[2]
|
||||
except ValueError:
|
||||
tokens, never_unit_applied = self.automation.apply_never_unit_automation(tokens)
|
||||
|
||||
if never_unit_applied:
|
||||
unit = tokens[1]
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
else:
|
||||
try:
|
||||
if unit is not None:
|
||||
# a unit is already found, no need to try the second argument for a fraction
|
||||
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except
|
||||
raise ValueError
|
||||
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
|
||||
if tokens[1]:
|
||||
amount += self.parse_fraction(tokens[1])
|
||||
# assume that units can't end with a comma
|
||||
if len(tokens) > 3 and not tokens[2].endswith(','):
|
||||
# try to use third argument as unit and everything else as food, use everything as food if it fails
|
||||
try:
|
||||
food, note = self.parse_food(tokens[3:])
|
||||
unit = tokens[2]
|
||||
except ValueError:
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
else:
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
else:
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
except ValueError:
|
||||
# assume that units can't end with a comma
|
||||
if not tokens[1].endswith(','):
|
||||
# try to use second argument as unit and everything else as food, use everything as food if it fails
|
||||
try:
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
if unit is None:
|
||||
unit = tokens[1]
|
||||
else:
|
||||
note = tokens[1]
|
||||
except ValueError:
|
||||
except ValueError:
|
||||
# assume that units can't end with a comma
|
||||
if not tokens[1].endswith(','):
|
||||
# try to use second argument as unit and everything else as food, use everything as food if it fails
|
||||
try:
|
||||
food, note = self.parse_food(tokens[2:])
|
||||
if unit is None:
|
||||
unit = tokens[1]
|
||||
else:
|
||||
note = tokens[1]
|
||||
except ValueError:
|
||||
food, note = self.parse_food(tokens[1:])
|
||||
else:
|
||||
food, note = self.parse_food(tokens[1:])
|
||||
else:
|
||||
food, note = self.parse_food(tokens[1:])
|
||||
else:
|
||||
# only two arguments, first one is the amount
|
||||
# which means this is the food
|
||||
@@ -264,6 +270,7 @@ class IngredientParser:
|
||||
|
||||
if food and not self.ignore_rules:
|
||||
food = self.automation.apply_food_automation(food)
|
||||
|
||||
if len(food) > Food._meta.get_field('name').max_length: # test if food name is to long
|
||||
# try splitting it at a space and taking only the first arg
|
||||
if len(food.split()) > 1 and len(food.split()[0]) < Food._meta.get_field('name').max_length:
|
||||
|
||||
@@ -51,10 +51,10 @@ class OpenDataImporter:
|
||||
for field in field_list:
|
||||
if isinstance(getattr(obj, field), float) or isinstance(getattr(obj, field), Decimal):
|
||||
if abs(float(getattr(obj, field)) - float(existing_obj[field])) > 0.001: # convert both to float and check if basically equal
|
||||
print(f'comparing FLOAT {obj} failed because field {field} is not equal ({getattr(obj, field)} != {existing_obj[field]})')
|
||||
#print(f'comparing FLOAT {obj} failed because field {field} is not equal ({getattr(obj, field)} != {existing_obj[field]})')
|
||||
return False
|
||||
elif getattr(obj, field) != existing_obj[field]:
|
||||
print(f'comparing {obj} failed because field {field} is not equal ({getattr(obj, field)} != {existing_obj[field]})')
|
||||
#print(f'comparing {obj} failed because field {field} is not equal ({getattr(obj, field)} != {existing_obj[field]})')
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -342,7 +342,7 @@ class OpenDataImporter:
|
||||
'name': self.data[datatype][k]['name'],
|
||||
'plural_name': self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
|
||||
'supermarket_category_id': self.slug_id_cache['category'][self.data[datatype][k]['store_category']] if self.data[datatype][k]['store_category'] in self.slug_id_cache['category'] else None,
|
||||
'fdc_id': re.sub(r'\D', '', self.data[datatype][k]['fdc_id']) if self.data[datatype][k]['fdc_id'] != '' else None,
|
||||
'fdc_id': re.sub(r'\D', '', str(self.data[datatype][k]['fdc_id'])) if self.data[datatype][k]['fdc_id'] != '' else None,
|
||||
'open_data_slug': k,
|
||||
'properties_food_unit_id': None,
|
||||
'space_id': self.request.space.id,
|
||||
|
||||
@@ -3,17 +3,19 @@ import inspect
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.contrib.auth.models import Group
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope
|
||||
from oauth2_provider.models import AccessToken
|
||||
from rest_framework import permissions
|
||||
from rest_framework.permissions import SAFE_METHODS
|
||||
|
||||
from cookbook.models import Recipe, ShareLink, UserSpace
|
||||
import random
|
||||
from cookbook.models import Recipe, ShareLink, UserSpace, Space
|
||||
|
||||
|
||||
def get_allowed_groups(groups_required):
|
||||
@@ -331,6 +333,25 @@ class CustomRecipePermission(permissions.BasePermission):
|
||||
or has_group_permission(request.user, ['user'])) and obj.space == request.space
|
||||
|
||||
|
||||
class CustomAiProviderPermission(permissions.BasePermission):
|
||||
"""
|
||||
Custom permission class for the AiProvider api endpoint
|
||||
users: can read all
|
||||
admins: can read and write
|
||||
superusers: can read and write + write providers without a space
|
||||
"""
|
||||
message = _('You do not have the required permissions to view this page!')
|
||||
|
||||
def has_permission(self, request, view): # user is either at least a user and the request is safe
|
||||
return (has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS) or (has_group_permission(request.user, ['admin']) or request.user.is_superuser)
|
||||
|
||||
# editing of global providers allowed for superusers, space providers by admins and users can read only access
|
||||
def has_object_permission(self, request, view, obj):
|
||||
return ((obj.space is None and request.user.is_superuser)
|
||||
or (obj.space == request.space and has_group_permission(request.user, ['admin']))
|
||||
or (obj.space == request.space and has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS))
|
||||
|
||||
|
||||
class CustomUserPermission(permissions.BasePermission):
|
||||
"""
|
||||
Custom permission class for user api endpoint
|
||||
@@ -437,3 +458,36 @@ class IsReadOnlyDRF(permissions.BasePermission):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return request.method in SAFE_METHODS
|
||||
|
||||
|
||||
class IsCreateDRF(permissions.BasePermission):
|
||||
message = 'You cannot interact with this object, you can only create'
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return request.method == 'POST'
|
||||
|
||||
|
||||
def create_space_for_user(user, name=None):
|
||||
with scopes_disabled():
|
||||
if not name:
|
||||
name = f"{user.username}'s Space"
|
||||
|
||||
if Space.objects.filter(name=name).exists():
|
||||
name = f'{name} #{random.randrange(1, 10 ** 5)}'
|
||||
|
||||
created_space = Space(name=name,
|
||||
created_by=user,
|
||||
max_file_storage_mb=settings.SPACE_DEFAULT_MAX_FILES,
|
||||
max_recipes=settings.SPACE_DEFAULT_MAX_RECIPES,
|
||||
max_users=settings.SPACE_DEFAULT_MAX_USERS,
|
||||
allow_sharing=settings.SPACE_DEFAULT_ALLOW_SHARING,
|
||||
ai_enabled=settings.SPACE_AI_ENABLED,
|
||||
ai_credits_monthly=settings.SPACE_AI_CREDITS_MONTHLY,
|
||||
space_setup_completed=False, )
|
||||
created_space.save()
|
||||
|
||||
UserSpace.objects.filter(user=user).update(active=False)
|
||||
user_space = UserSpace.objects.create(space=created_space, user=user, active=True)
|
||||
user_space.groups.add(Group.objects.filter(name='admin').get())
|
||||
|
||||
return user_space
|
||||
|
||||
@@ -48,7 +48,7 @@ class FoodPropertyHelper:
|
||||
found_property = False
|
||||
# if food has a value for the given property type (no matter if conversion is possible)
|
||||
has_property_value = False
|
||||
if i.food.properties_food_amount == 0 or i.food.properties_food_unit is None and not (i.amount == 0 or i.no_amount): # if food is configured incorrectly
|
||||
if (i.food.properties_food_amount == 0 or i.food.properties_food_unit is None) and not (i.amount == 0 or i.no_amount): # if food is configured incorrectly
|
||||
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': None}
|
||||
computed_properties[pt.id]['missing_value'] = True
|
||||
else:
|
||||
@@ -56,18 +56,25 @@ class FoodPropertyHelper:
|
||||
if p.property_type == pt and p.property_amount is not None:
|
||||
has_property_value = True
|
||||
for c in conversions:
|
||||
if c.unit == i.food.properties_food_unit:
|
||||
if c.unit == i.food.properties_food_unit and i.food.properties_food_amount != 0:
|
||||
found_property = True
|
||||
computed_properties[pt.id]['total_value'] += (c.amount / i.food.properties_food_amount) * p.property_amount
|
||||
computed_properties[pt.id]['food_values'] = self.add_or_create(
|
||||
computed_properties[p.property_type.id]['food_values'], c.food.id, (c.amount / i.food.properties_food_amount) * p.property_amount, c.food)
|
||||
if not found_property:
|
||||
if i.amount == 0 or i.no_amount: # don't count ingredients without an amount as missing
|
||||
computed_properties[pt.id]['missing_value'] = computed_properties[pt.id]['missing_value'] or False # don't override if another food was already missing
|
||||
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': 0}
|
||||
# if no amount and food does not exist yet add it but don't count as missing
|
||||
if i.amount == 0 or i.no_amount:
|
||||
if i.food.id not in computed_properties[pt.id]['food_values']:
|
||||
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': 0}
|
||||
# if amount is present but unit is missing indicate it in the result
|
||||
elif i.unit is None:
|
||||
if i.food.id not in computed_properties[pt.id]['food_values']:
|
||||
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': 0}
|
||||
computed_properties[pt.id]['food_values'][i.food.id]['missing_unit'] = True
|
||||
else:
|
||||
computed_properties[pt.id]['missing_value'] = True
|
||||
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': None}
|
||||
if i.food.id not in computed_properties[pt.id]['food_values']:
|
||||
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': None}
|
||||
if has_property_value and i.unit is not None:
|
||||
computed_properties[pt.id]['food_values'][i.food.id]['missing_conversion'] = {'base_unit': {'id': i.unit.id, 'name': i.unit.name}, 'converted_unit': {'id': i.food.properties_food_unit.id, 'name': i.food.properties_food_unit.name}}
|
||||
|
||||
@@ -77,8 +84,12 @@ class FoodPropertyHelper:
|
||||
# TODO move to central helper ? --> use defaultdict
|
||||
@staticmethod
|
||||
def add_or_create(d, key, value, food):
|
||||
if key in d and d[key]['value']:
|
||||
d[key]['value'] += value
|
||||
if key in d:
|
||||
# value can be None if a previous instance of the same food was missing a conversion
|
||||
if d[key]['value']:
|
||||
d[key]['value'] += value
|
||||
else:
|
||||
d[key]['value'] = value
|
||||
else:
|
||||
d[key] = {'id': food.id, 'food': {'id': food.id, 'name': food.name}, 'value': value}
|
||||
return d
|
||||
|
||||
@@ -288,7 +288,7 @@ class RecipeSearch():
|
||||
|
||||
def _updated_on_filter(self):
|
||||
if self._updatedon:
|
||||
self._queryset = self._queryset.filter(updated_at__date__date=self._updatedon)
|
||||
self._queryset = self._queryset.filter(updated_at__date=self._updatedon)
|
||||
elif self._updatedon_lte:
|
||||
self._queryset = self._queryset.filter(updated_at__date__lte=self._updatedon_lte)
|
||||
elif self._updatedon_gte:
|
||||
@@ -324,7 +324,7 @@ class RecipeSearch():
|
||||
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):
|
||||
if self._sort_includes('favorite') or self._timescooked or self._timescooked_gte or self._timescooked_lte:
|
||||
if self._sort_includes('favorite') or self._timescooked is not None or self._timescooked_gte is not None or self._timescooked_lte is not None:
|
||||
less_than = self._timescooked_lte and not self._sort_includes('-favorite')
|
||||
if less_than:
|
||||
default = 1000
|
||||
@@ -338,11 +338,11 @@ class RecipeSearch():
|
||||
)
|
||||
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
|
||||
|
||||
if self._timescooked:
|
||||
self._queryset = self._queryset.filter(favorite=0)
|
||||
elif self._timescooked_lte:
|
||||
if self._timescooked is not None:
|
||||
self._queryset = self._queryset.filter(favorite=self._timescooked)
|
||||
elif self._timescooked_lte is not None:
|
||||
self._queryset = self._queryset.filter(favorite__lte=int(self._timescooked_lte)).exclude(favorite=0)
|
||||
elif self._timescooked_gte:
|
||||
elif self._timescooked_gte is not None:
|
||||
self._queryset = self._queryset.filter(favorite__gte=int(self._timescooked_gte))
|
||||
|
||||
def keyword_filters(self, **kwargs):
|
||||
|
||||
@@ -69,15 +69,8 @@ def get_from_scraper(scrape, request):
|
||||
recipe_json['description'] = parse_description(description)
|
||||
recipe_json['description'] = automation_engine.apply_regex_replace_automation(recipe_json['description'], Automation.DESCRIPTION_REPLACE)
|
||||
|
||||
# assign servings attributes
|
||||
try:
|
||||
# dont use scrape.yields() as this will always return "x servings" or "x items", should be improved in scrapers directly
|
||||
servings = scrape.schema.data.get('recipeYield') or 1
|
||||
except Exception:
|
||||
servings = 1
|
||||
|
||||
recipe_json['servings'] = parse_servings(servings)
|
||||
recipe_json['servings_text'] = parse_servings_text(servings)
|
||||
recipe_json['servings'] = parse_servings(scrape.schema.data.get('recipeYield'))
|
||||
recipe_json['servings_text'] = parse_servings_text(scrape.schema.data.get('recipeYield'))
|
||||
|
||||
# assign time attributes
|
||||
try:
|
||||
@@ -155,7 +148,7 @@ def get_from_scraper(scrape, request):
|
||||
|
||||
# assign steps
|
||||
try:
|
||||
for i in parse_instructions(scrape.instructions()):
|
||||
for i in parse_instructions(scrape.instructions_list()):
|
||||
recipe_json['steps'].append({
|
||||
'instruction': i,
|
||||
'ingredients': [],
|
||||
@@ -177,11 +170,11 @@ def get_from_scraper(scrape, request):
|
||||
for x in scrape.ingredients():
|
||||
if x.strip() != '':
|
||||
try:
|
||||
amount, unit, ingredient, note = ingredient_parser.parse(x)
|
||||
amount, unit, food, note = ingredient_parser.parse(x)
|
||||
ingredient = {
|
||||
'amount': amount,
|
||||
'food': {
|
||||
'name': ingredient,
|
||||
'name': food,
|
||||
},
|
||||
'unit': None,
|
||||
'note': note,
|
||||
@@ -315,14 +308,29 @@ def clean_instruction_string(instruction):
|
||||
# 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"))
|
||||
normalized_string = normalized_string \
|
||||
.replace(u"\uE003", _('reverse rotation')) \
|
||||
.replace(u"\uE002", _('careful rotation')) \
|
||||
.replace(u"\uE001", _('knead')) \
|
||||
.replace(u"\uE031", _('thicken')) \
|
||||
.replace(u"\uE019", _('warm up')) \
|
||||
.replace(u"\uE02E", _('ferment')) \
|
||||
.replace(u"\uE018", _('slow cook')) \
|
||||
.replace(u"\uE033", _('egg boiler')) \
|
||||
.replace(u"\uE016", _('kettle')) \
|
||||
.replace(u"\uE01E", _('blend')) \
|
||||
.replace(u"\uE011", _('pre-clean')) \
|
||||
.replace(u"\uE026", _('high temperature')) \
|
||||
.replace(u"\uE00D", _('rice cooker')) \
|
||||
.replace(u"\uE00C", _('caramelize')) \
|
||||
.replace(u"\uE038", _('peeler')) \
|
||||
.replace(u"\uE037", _('slicer')) \
|
||||
.replace(u"\uE036", _('grater')) \
|
||||
.replace(u"\uE04C", _('spiralizer')) \
|
||||
.replace(u"\uE02D", _("sous-vide"))
|
||||
|
||||
|
||||
return normalized_string
|
||||
|
||||
|
||||
def parse_instructions(instructions):
|
||||
@@ -391,7 +399,7 @@ def parse_servings(servings):
|
||||
def parse_servings_text(servings):
|
||||
if isinstance(servings, str):
|
||||
try:
|
||||
servings = re.sub("\\d+", '', servings).strip()
|
||||
servings = re.sub("\\d+", '', servings, 1).strip()
|
||||
except Exception:
|
||||
servings = ''
|
||||
if isinstance(servings, list):
|
||||
@@ -403,6 +411,8 @@ def parse_servings_text(servings):
|
||||
|
||||
|
||||
def parse_time(recipe_time):
|
||||
if not recipe_time:
|
||||
return 0
|
||||
if type(recipe_time) not in [int, float]:
|
||||
try:
|
||||
recipe_time = float(re.search(r'\d+', recipe_time).group())
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
from django.contrib.auth.models import Group
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
|
||||
from psycopg2.errors import UniqueViolation
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
import random
|
||||
|
||||
from cookbook.helper.permission_helper import create_space_for_user
|
||||
from cookbook.models import Space, UserSpace
|
||||
from cookbook.views import views
|
||||
from recipes import settings
|
||||
|
||||
@@ -34,16 +41,28 @@ class ScopeMiddleware:
|
||||
if request.path.startswith(prefix + '/switch-space/'):
|
||||
return self.get_response(request)
|
||||
|
||||
with scopes_disabled():
|
||||
if request.user.userspace_set.count() == 0 and not reverse('account_logout') in request.path:
|
||||
return views.space_overview(request)
|
||||
if request.path.startswith(prefix + '/invite/'):
|
||||
return self.get_response(request)
|
||||
|
||||
# get active user space, if for some reason more than one space is active select first (group permission checks will fail, this is not intended at this point)
|
||||
user_space = request.user.userspace_set.filter(active=True).first()
|
||||
|
||||
if not user_space:
|
||||
return views.space_overview(request)
|
||||
if not user_space and request.user.userspace_set.count() > 0:
|
||||
# if the users has a userspace but nothing is active, activate the first one
|
||||
user_space = request.user.userspace_set.first()
|
||||
if user_space:
|
||||
user_space.active = True
|
||||
user_space.save()
|
||||
|
||||
if not user_space:
|
||||
if 'signup_token' in request.session:
|
||||
# if user is authenticated, has no space but a signup token (InviteLink) is present, redirect to invite link logic
|
||||
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
|
||||
else:
|
||||
# if user does not yet have a space create one for him
|
||||
user_space = create_space_for_user(request.user)
|
||||
|
||||
# TODO remove the need for this view
|
||||
if user_space.groups.count() == 0 and not reverse('account_logout') in request.path:
|
||||
return views.no_groups(request)
|
||||
|
||||
|
||||
@@ -75,7 +75,8 @@ class RecipeShoppingEditor():
|
||||
|
||||
@staticmethod
|
||||
def get_shopping_list_recipe(id, user, space):
|
||||
return ShoppingListRecipe.objects.filter(id=id).filter(entries__space=space).filter(
|
||||
# TODO this sucks since it wont find SLR's that no longer have any entries
|
||||
return ShoppingListRecipe.objects.filter(id=id, space=space).filter(
|
||||
Q(entries__created_by=user)
|
||||
| Q(entries__created_by__in=list(user.get_shopping_share()))
|
||||
).prefetch_related('entries').first()
|
||||
@@ -136,7 +137,8 @@ class RecipeShoppingEditor():
|
||||
self.servings = servings
|
||||
|
||||
self._delete_ingredients(ingredients=ingredients)
|
||||
if self.servings != self._shopping_list_recipe.servings:
|
||||
# need to check if there is a SLR because its possible it cant be found if all entries are deleted
|
||||
if self._shopping_list_recipe and self.servings != self._shopping_list_recipe.servings:
|
||||
self.edit_servings()
|
||||
self._add_ingredients(ingredients=ingredients)
|
||||
return True
|
||||
|
||||
@@ -135,8 +135,9 @@ class UnitConversionHelper:
|
||||
:param food: base food
|
||||
:return: converted ingredient object from base amount/unit/food
|
||||
"""
|
||||
if uc.food is None or uc.food == food:
|
||||
if (uc.food is None or uc.food == food) and uc.converted_amount > 0 and uc.base_amount > 0:
|
||||
if unit == uc.base_unit:
|
||||
return Ingredient(amount=amount * (uc.converted_amount / uc.base_amount), unit=uc.converted_unit, food=food, space=self.space)
|
||||
else:
|
||||
return Ingredient(amount=amount * (uc.base_amount / uc.converted_amount), unit=uc.base_unit, food=food, space=self.space)
|
||||
return None
|
||||
|
||||
@@ -60,14 +60,15 @@ class CookBookApp(Integration):
|
||||
food=f, unit=u, amount=ingredient.get('amount', None), note=ingredient.get('note', None), original_text=ingredient.get('original_text', None), space=self.request.space,
|
||||
))
|
||||
|
||||
if len(images) > 0:
|
||||
try:
|
||||
url = images[0]
|
||||
if validate_import_url(url):
|
||||
try:
|
||||
for url in images:
|
||||
# import the first valid image which is not cookbookapp branding
|
||||
if validate_import_url(url) and not url.startswith("https://media.cookbookmanager.com/brand/"):
|
||||
response = requests.get(url)
|
||||
self.import_recipe_image(recipe, BytesIO(response.content))
|
||||
except Exception as e:
|
||||
print('failed to import image ', str(e))
|
||||
break
|
||||
except Exception as e:
|
||||
print('failed to import image ', str(e))
|
||||
|
||||
recipe.save()
|
||||
return recipe
|
||||
|
||||
@@ -26,6 +26,12 @@ class Integration:
|
||||
files = None
|
||||
export_type = None
|
||||
ignored_recipes = []
|
||||
import_log = None
|
||||
import_duplicates = False
|
||||
|
||||
import_meal_plans = True
|
||||
import_shopping_lists = True
|
||||
nutrition_per_serving = False
|
||||
|
||||
def __init__(self, request, export_type):
|
||||
"""
|
||||
@@ -102,7 +108,7 @@ class Integration:
|
||||
"""
|
||||
return True
|
||||
|
||||
def do_import(self, files, il, import_duplicates):
|
||||
def do_import(self, files, il, import_duplicates, meal_plans=True, shopping_lists=True, nutrition_per_serving=False):
|
||||
"""
|
||||
Imports given files
|
||||
:param import_duplicates: if true duplicates are imported as well
|
||||
@@ -111,6 +117,12 @@ class Integration:
|
||||
:return: HttpResponseRedirect to the recipe search showing all imported recipes
|
||||
"""
|
||||
with scope(space=self.request.space):
|
||||
self.import_log = il
|
||||
self.import_duplicates = import_duplicates
|
||||
|
||||
self.import_meal_plans = meal_plans
|
||||
self.import_shopping_lists = shopping_lists
|
||||
self.nutrition_per_serving = nutrition_per_serving
|
||||
|
||||
try:
|
||||
self.files = files
|
||||
@@ -166,20 +178,24 @@ class Integration:
|
||||
il.total_recipes = len(new_file_list)
|
||||
file_list = new_file_list
|
||||
|
||||
for z in file_list:
|
||||
try:
|
||||
if not hasattr(z, 'filename') or isinstance(z, Tag):
|
||||
recipe = self.get_recipe_from_file(z)
|
||||
else:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += self.get_recipe_processed_msg(recipe)
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
|
||||
if isinstance(self, cookbook.integration.mealie1.Mealie1):
|
||||
# since the mealie 1.0 export is a backup and not a classic recipe export we treat it a bit differently
|
||||
recipes = self.get_recipe_from_file(import_zip)
|
||||
else:
|
||||
for z in file_list:
|
||||
try:
|
||||
if not hasattr(z, 'filename') or isinstance(z, Tag):
|
||||
recipe = self.get_recipe_from_file(z)
|
||||
else:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += self.get_recipe_processed_msg(recipe)
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
except Exception as e:
|
||||
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 '.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'])
|
||||
|
||||
372
cookbook/integration/mealie1.py
Normal file
372
cookbook/integration/mealie1.py
Normal file
@@ -0,0 +1,372 @@
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
from gettext import gettext as _
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from cookbook.helper import ingredient_parser
|
||||
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, Keyword, Recipe, Step, Food, Unit, SupermarketCategory, PropertyType, Property, MealType, MealPlan, CookLog, ShoppingListEntry
|
||||
|
||||
|
||||
class Mealie1(Integration):
|
||||
"""
|
||||
integration for mealie past version 1.0
|
||||
"""
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
mealie_database = json.loads(BytesIO(file.read('database.json')).getvalue().decode("utf-8"))
|
||||
self.import_log.total_recipes = len(mealie_database['recipes'])
|
||||
self.import_log.msg += f"Importing {len(mealie_database["categories"]) + len(mealie_database["tags"])} tags and categories as keywords...\n"
|
||||
self.import_log.save()
|
||||
|
||||
keywords_categories_dict = {}
|
||||
for c in mealie_database['categories']:
|
||||
if keyword := Keyword.objects.filter(name=c['name'], space=self.request.space).first():
|
||||
keywords_categories_dict[c['id']] = keyword.pk
|
||||
else:
|
||||
keyword = Keyword.objects.create(name=c['name'], space=self.request.space)
|
||||
keywords_categories_dict[c['id']] = keyword.pk
|
||||
|
||||
keywords_tags_dict = {}
|
||||
for t in mealie_database['tags']:
|
||||
if keyword := Keyword.objects.filter(name=t['name'], space=self.request.space).first():
|
||||
keywords_tags_dict[t['id']] = keyword.pk
|
||||
else:
|
||||
keyword = Keyword.objects.create(name=t['name'], space=self.request.space)
|
||||
keywords_tags_dict[t['id']] = keyword.pk
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["multi_purpose_labels"])} multi purpose labels as supermarket categories...\n"
|
||||
self.import_log.save()
|
||||
|
||||
supermarket_categories_dict = {}
|
||||
for m in mealie_database['multi_purpose_labels']:
|
||||
if supermarket_category := SupermarketCategory.objects.filter(name=m['name'], space=self.request.space).first():
|
||||
supermarket_categories_dict[m['id']] = supermarket_category.pk
|
||||
else:
|
||||
supermarket_category = SupermarketCategory.objects.create(name=m['name'], space=self.request.space)
|
||||
supermarket_categories_dict[m['id']] = supermarket_category.pk
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["ingredient_foods"])} foods...\n"
|
||||
self.import_log.save()
|
||||
|
||||
foods_dict = {}
|
||||
for f in mealie_database['ingredient_foods']:
|
||||
if food := Food.objects.filter(name=f['name'], space=self.request.space).first():
|
||||
foods_dict[f['id']] = food.pk
|
||||
else:
|
||||
food = {'name': f['name'],
|
||||
'plural_name': f['plural_name'],
|
||||
'description': f['description'],
|
||||
'space': self.request.space}
|
||||
|
||||
if f['label_id'] and f['label_id'] in supermarket_categories_dict:
|
||||
food['supermarket_category_id'] = supermarket_categories_dict[f['label_id']]
|
||||
|
||||
food = Food.objects.create(**food)
|
||||
if f['on_hand']:
|
||||
food.onhand_users.add(self.request.user)
|
||||
foods_dict[f['id']] = food.pk
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["ingredient_units"])} units...\n"
|
||||
self.import_log.save()
|
||||
|
||||
units_dict = {}
|
||||
for u in mealie_database['ingredient_units']:
|
||||
if unit := Unit.objects.filter(name=u['name'], space=self.request.space).first():
|
||||
units_dict[u['id']] = unit.pk
|
||||
else:
|
||||
unit = Unit.objects.create(name=u['name'], plural_name=u['plural_name'], description=u['description'], space=self.request.space)
|
||||
units_dict[u['id']] = unit.pk
|
||||
|
||||
recipes_dict = {}
|
||||
recipe_property_factor_dict = {}
|
||||
recipes = []
|
||||
recipe_keyword_relation = []
|
||||
for r in mealie_database['recipes']:
|
||||
if Recipe.objects.filter(space=self.request.space, name=r['name']).exists() and not self.import_duplicates:
|
||||
self.import_log.msg += f"Ignoring {r['name']} because a recipe with this name already exists.\n"
|
||||
self.import_log.save()
|
||||
else:
|
||||
servings = 1
|
||||
try:
|
||||
servings = r['recipe_servings'] if r['recipe_servings'] and r['recipe_servings'] != 0 else 1
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
recipe = Recipe.objects.create(
|
||||
waiting_time=parse_time(r['perform_time']),
|
||||
working_time=parse_time(r['prep_time']),
|
||||
description=r['description'][:512],
|
||||
name=r['name'],
|
||||
source_url=r['org_url'],
|
||||
servings=servings,
|
||||
servings_text=r['recipe_yield'].strip()[:32] if r['recipe_yield'] else "",
|
||||
internal=True,
|
||||
created_at=r['created_at'],
|
||||
space=self.request.space,
|
||||
created_by=self.request.user,
|
||||
)
|
||||
|
||||
if not self.nutrition_per_serving:
|
||||
recipe_property_factor_dict[r['id']] = recipe.servings
|
||||
|
||||
self.import_log.msg += self.get_recipe_processed_msg(recipe)
|
||||
self.import_log.imported_recipes += 1
|
||||
self.import_log.save()
|
||||
|
||||
recipes.append(recipe)
|
||||
recipes_dict[r['id']] = recipe.pk
|
||||
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipe.pk, keyword_id=self.keyword.pk))
|
||||
|
||||
Recipe.keywords.through.objects.bulk_create(recipe_keyword_relation, ignore_conflicts=True)
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipe_instructions"])} instructions...\n"
|
||||
self.import_log.save()
|
||||
|
||||
steps_relation = []
|
||||
first_step_of_recipe_dict = {}
|
||||
step_id_dict = {}
|
||||
for s in mealie_database['recipe_instructions']:
|
||||
if s['recipe_id'] in recipes_dict:
|
||||
step = Step.objects.create(instruction=(s['text'] if s['text'] else "") + (f" \n {s['summary']}" if 'summary' in s and s['summary'] else ""),
|
||||
order=s['position'],
|
||||
name=s['title'],
|
||||
space=self.request.space)
|
||||
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[s['recipe_id']], step_id=step.pk))
|
||||
step_id_dict[s["id"]] = step.pk
|
||||
if s['recipe_id'] not in first_step_of_recipe_dict:
|
||||
first_step_of_recipe_dict[s['recipe_id']] = step.pk
|
||||
|
||||
# it is possible for a recipe to not have steps but have ingredients, in that case create an empty step to add them to later
|
||||
for r in recipes_dict.keys():
|
||||
if r not in first_step_of_recipe_dict:
|
||||
step = Step.objects.create(instruction='',
|
||||
order=0,
|
||||
name='',
|
||||
space=self.request.space)
|
||||
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[r], step_id=step.pk))
|
||||
first_step_of_recipe_dict[r] = step.pk
|
||||
|
||||
for n in mealie_database['notes']:
|
||||
if n['recipe_id'] in recipes_dict:
|
||||
step = Step.objects.create(instruction=n['text'],
|
||||
name=n['title'][:128] if n['title'] else "",
|
||||
order=100,
|
||||
space=self.request.space)
|
||||
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[n['recipe_id']], step_id=step.pk))
|
||||
|
||||
Recipe.steps.through.objects.bulk_create(steps_relation)
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipes_ingredients"])} ingredients...\n"
|
||||
self.import_log.save()
|
||||
|
||||
# mealie stores the reference to a step (instruction) from an ingredient (reference) in the recipe_ingredient_ref_link table
|
||||
recipe_ingredient_ref_link_dict = {}
|
||||
for ref in mealie_database['recipe_ingredient_ref_link']:
|
||||
recipe_ingredient_ref_link_dict[ref["reference_id"]] = ref["instruction_id"]
|
||||
|
||||
ingredients_relation = []
|
||||
for i in mealie_database['recipes_ingredients']:
|
||||
if i['recipe_id'] in recipes_dict:
|
||||
if i['title']:
|
||||
title_ingredient = Ingredient.objects.create(
|
||||
note=i['title'],
|
||||
is_header=True,
|
||||
space=self.request.space,
|
||||
)
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=get_step_id(i, first_step_of_recipe_dict, step_id_dict,recipe_ingredient_ref_link_dict), ingredient_id=title_ingredient.pk))
|
||||
if i['food_id']:
|
||||
ingredient = Ingredient.objects.create(
|
||||
food_id=foods_dict[i['food_id']] if i['food_id'] in foods_dict else None,
|
||||
unit_id=units_dict[i['unit_id']] if i['unit_id'] in units_dict else None,
|
||||
original_text=i['original_text'],
|
||||
order=i['position'],
|
||||
amount=i['quantity'] if i['quantity'] else 0,
|
||||
note=i['note'],
|
||||
space=self.request.space,
|
||||
)
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=get_step_id(i, first_step_of_recipe_dict, step_id_dict,recipe_ingredient_ref_link_dict), ingredient_id=ingredient.pk))
|
||||
elif i['note'].strip():
|
||||
amount, unit, food, note = ingredient_parser.parse(i['note'].strip())
|
||||
f = ingredient_parser.get_food(food)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
ingredient = Ingredient.objects.create(
|
||||
food=f,
|
||||
unit=u,
|
||||
amount=amount,
|
||||
note=note,
|
||||
original_text=i['original_text'],
|
||||
space=self.request.space,
|
||||
)
|
||||
ingredients_relation.append(Step.ingredients.through(step_id=get_step_id(i, first_step_of_recipe_dict, step_id_dict,recipe_ingredient_ref_link_dict), ingredient_id=ingredient.pk))
|
||||
Step.ingredients.through.objects.bulk_create(ingredients_relation)
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipes_to_categories"]) + len(mealie_database["recipes_to_tags"])} category and keyword relations...\n"
|
||||
self.import_log.save()
|
||||
|
||||
recipe_keyword_relation = []
|
||||
for rC in mealie_database['recipes_to_categories']:
|
||||
if rC['recipe_id'] in recipes_dict:
|
||||
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipes_dict[rC['recipe_id']], keyword_id=keywords_categories_dict[rC['category_id']]))
|
||||
|
||||
for rT in mealie_database['recipes_to_tags']:
|
||||
if rT['recipe_id'] in recipes_dict:
|
||||
recipe_keyword_relation.append(Recipe.keywords.through(recipe_id=recipes_dict[rT['recipe_id']], keyword_id=keywords_tags_dict[rT['tag_id']]))
|
||||
|
||||
Recipe.keywords.through.objects.bulk_create(recipe_keyword_relation, ignore_conflicts=True)
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipe_nutrition"])} properties...\n"
|
||||
self.import_log.save()
|
||||
|
||||
property_types_dict = {
|
||||
'calories': PropertyType.objects.get_or_create(name=_('Calories'), space=self.request.space, defaults={'unit': 'kcal', 'fdc_id': 1008})[0],
|
||||
'carbohydrate_content': PropertyType.objects.get_or_create(name=_('Carbohydrates'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1005})[0],
|
||||
'cholesterol_content': PropertyType.objects.get_or_create(name=_('Cholesterol'), space=self.request.space, defaults={'unit': 'mg', 'fdc_id': 1253})[0],
|
||||
'fat_content': PropertyType.objects.get_or_create(name=_('Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1004})[0],
|
||||
'fiber_content': PropertyType.objects.get_or_create(name=_('Fiber'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1079})[0],
|
||||
'protein_content': PropertyType.objects.get_or_create(name=_('Protein'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1003})[0],
|
||||
'saturated_fat_content': PropertyType.objects.get_or_create(name=_('Saturated Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1258})[0],
|
||||
'sodium_content': PropertyType.objects.get_or_create(name=_('Sodium'), space=self.request.space, defaults={'unit': 'mg', 'fdc_id': 1093})[0],
|
||||
'sugar_content': PropertyType.objects.get_or_create(name=_('Sugar'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1063})[0],
|
||||
'trans_fat_content': PropertyType.objects.get_or_create(name=_('Trans Fat'), space=self.request.space, defaults={'unit': 'g', 'fdc_id': 1257})[0],
|
||||
'unsaturated_fat_content': PropertyType.objects.get_or_create(name=_('Unsaturated Fat'), space=self.request.space, defaults={'unit': 'g'})[0],
|
||||
}
|
||||
|
||||
with transaction.atomic():
|
||||
recipe_properties_relation = []
|
||||
properties_relation = []
|
||||
for r in mealie_database['recipe_nutrition']:
|
||||
if r['recipe_id'] in recipes_dict:
|
||||
for key in property_types_dict:
|
||||
if key in r and r[key]:
|
||||
properties_relation.append(
|
||||
Property(property_type_id=property_types_dict[key].pk,
|
||||
property_amount=Decimal(str(r[key])) / (
|
||||
Decimal(str(recipe_property_factor_dict[r['recipe_id']])) if r['recipe_id'] in recipe_property_factor_dict else 1),
|
||||
open_data_food_slug=r['recipe_id'],
|
||||
space=self.request.space))
|
||||
properties = Property.objects.bulk_create(properties_relation)
|
||||
property_ids = []
|
||||
for p in properties:
|
||||
recipe_properties_relation.append(Recipe.properties.through(recipe_id=recipes_dict[p.open_data_food_slug], property_id=p.pk))
|
||||
property_ids.append(p.pk)
|
||||
Recipe.properties.through.objects.bulk_create(recipe_properties_relation, ignore_conflicts=True)
|
||||
Property.objects.filter(id__in=property_ids).update(open_data_food_slug=None)
|
||||
|
||||
# delete unused property types
|
||||
for pT in property_types_dict:
|
||||
try:
|
||||
property_types_dict[pT].delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
self.import_log.msg += f"Importing {len(mealie_database["recipe_comments"]) + len(mealie_database["recipe_timeline_events"])} comments and cook logs...\n"
|
||||
self.import_log.save()
|
||||
|
||||
cook_log_list = []
|
||||
for c in mealie_database['recipe_comments']:
|
||||
if c['recipe_id'] in recipes_dict:
|
||||
cook_log_list.append(CookLog(
|
||||
recipe_id=recipes_dict[c['recipe_id']],
|
||||
comment=c['text'],
|
||||
created_at=c['created_at'],
|
||||
created_by=self.request.user,
|
||||
space=self.request.space,
|
||||
))
|
||||
|
||||
for c in mealie_database['recipe_timeline_events']:
|
||||
if c['recipe_id'] in recipes_dict:
|
||||
if c['event_type'] == 'comment':
|
||||
cook_log_list.append(CookLog(
|
||||
recipe_id=recipes_dict[c['recipe_id']],
|
||||
comment=c['message'],
|
||||
created_at=c['created_at'],
|
||||
created_by=self.request.user,
|
||||
space=self.request.space,
|
||||
))
|
||||
|
||||
CookLog.objects.bulk_create(cook_log_list)
|
||||
|
||||
if self.import_meal_plans:
|
||||
self.import_log.msg += f"Importing {len(mealie_database["group_meal_plans"])} meal plans...\n"
|
||||
self.import_log.save()
|
||||
|
||||
meal_types_dict = {}
|
||||
meal_plans = []
|
||||
for m in mealie_database['group_meal_plans']:
|
||||
if m['recipe_id'] in recipes_dict:
|
||||
if not m['entry_type'] in meal_types_dict:
|
||||
meal_type = MealType.objects.get_or_create(name=m['entry_type'], created_by=self.request.user, space=self.request.space)[0]
|
||||
meal_types_dict[m['entry_type']] = meal_type.pk
|
||||
meal_plans.append(MealPlan(
|
||||
recipe_id=recipes_dict[m['recipe_id']] if m['recipe_id'] else None,
|
||||
title=m['title'] if m['title'] else "",
|
||||
note=m['text'] if m['text'] else "",
|
||||
from_date=m['date'],
|
||||
to_date=m['date'],
|
||||
meal_type_id=meal_types_dict[m['entry_type']],
|
||||
created_by=self.request.user,
|
||||
space=self.request.space,
|
||||
))
|
||||
|
||||
MealPlan.objects.bulk_create(meal_plans)
|
||||
|
||||
if self.import_shopping_lists:
|
||||
self.import_log.msg += f"Importing {len(mealie_database["shopping_list_items"])} shopping list items...\n"
|
||||
self.import_log.save()
|
||||
|
||||
shopping_list_items = []
|
||||
for sli in mealie_database['shopping_list_items']:
|
||||
if not sli['checked']:
|
||||
if sli['food_id']:
|
||||
shopping_list_items.append(ShoppingListEntry(
|
||||
amount=sli['quantity'],
|
||||
unit_id=units_dict[sli['unit_id']] if sli['unit_id'] else None,
|
||||
food_id=foods_dict[sli['food_id']] if sli['food_id'] else None,
|
||||
created_by=self.request.user,
|
||||
space=self.request.space,
|
||||
))
|
||||
elif not sli['food_id'] and sli['note'].strip():
|
||||
amount, unit, food, note = ingredient_parser.parse(sli['note'].strip())
|
||||
f = ingredient_parser.get_food(food)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
shopping_list_items.append(ShoppingListEntry(
|
||||
amount=amount,
|
||||
unit=u,
|
||||
food=f,
|
||||
created_by=self.request.user,
|
||||
space=self.request.space,
|
||||
))
|
||||
ShoppingListEntry.objects.bulk_create(shopping_list_items)
|
||||
|
||||
self.import_log.msg += f"Importing Images. This might take some time ...\n"
|
||||
self.import_log.save()
|
||||
for r in mealie_database['recipes']:
|
||||
try:
|
||||
if recipe := Recipe.objects.filter(pk=recipes_dict[r['id']]).first():
|
||||
self.import_recipe_image(recipe, BytesIO(file.read(f'data/recipes/{str(uuid.UUID(str(r['id'])))}/images/original.webp')), filetype='.webp')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return recipes
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
raise NotImplementedError('Method not implemented in storage integration')
|
||||
|
||||
|
||||
def get_step_id(i, first_step_of_recipe_dict, step_id_dict, recipe_ingredient_ref_link_dict):
|
||||
try:
|
||||
return step_id_dict[recipe_ingredient_ref_link_dict[i['reference_id']]]
|
||||
except KeyError:
|
||||
return first_step_of_recipe_dict[i['recipe_id']]
|
||||
@@ -63,7 +63,15 @@ class MealMaster(Integration):
|
||||
current_recipe = ''
|
||||
|
||||
for fl in file.readlines():
|
||||
line = fl.decode("windows-1250")
|
||||
line = ""
|
||||
try:
|
||||
line = fl.decode("UTF-8")
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
line = fl.decode("windows-1250")
|
||||
except Exception as e:
|
||||
line = "ERROR DECODING LINE"
|
||||
|
||||
if (line.startswith('MMMMM') or line.startswith('-----')) and 'meal-master' in line.lower():
|
||||
if current_recipe != '':
|
||||
recipe_list.append(current_recipe)
|
||||
|
||||
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.
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
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
BIN
cookbook/locale/hr/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/hr/LC_MESSAGES/django.mo
Normal file
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.
File diff suppressed because it is too large
Load Diff
2453
cookbook/locale/ko/LC_MESSAGES/django.po
Normal file
2453
cookbook/locale/ko/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.
@@ -13,8 +13,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-08-01 15:04+0200\n"
|
||||
"PO-Revision-Date: 2025-02-16 14:58+0000\n"
|
||||
"Last-Translator: Cots Partier <cots.pastier.34@icloud.com>\n"
|
||||
"PO-Revision-Date: 2025-09-23 19:45+0000\n"
|
||||
"Last-Translator: Justin Straver <justin.straver@gmail.com>\n"
|
||||
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/nl/>\n"
|
||||
"Language: nl\n"
|
||||
@@ -22,14 +22,14 @@ 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 5.8.4\n"
|
||||
"X-Generator: Weblate 5.13.1\n"
|
||||
|
||||
#: .\cookbook\forms.py:45
|
||||
msgid ""
|
||||
"Both fields are optional. If none are given the username will be displayed "
|
||||
"instead"
|
||||
msgstr ""
|
||||
"Beide velden zijn optioneel. Indien niks is opgegeven wordt de "
|
||||
"Beide velden zijn optioneel. Indien niets is opgegeven wordt de "
|
||||
"gebruikersnaam weergegeven"
|
||||
|
||||
#: .\cookbook\forms.py:62 .\cookbook\forms.py:246
|
||||
@@ -46,7 +46,7 @@ msgstr "Voorbereidingstijd in minuten"
|
||||
|
||||
#: .\cookbook\forms.py:62
|
||||
msgid "Waiting time (cooking/baking) in minutes"
|
||||
msgstr "Wacht tijd in minuten (koken en bakken)"
|
||||
msgstr "Wachttijd in minuten (koken en bakken)"
|
||||
|
||||
#: .\cookbook\forms.py:63 .\cookbook\forms.py:222 .\cookbook\forms.py:246
|
||||
msgid "Path"
|
||||
@@ -771,7 +771,7 @@ msgstr ""
|
||||
#: .\cookbook\templates\account\email_confirm.html:22
|
||||
#: .\cookbook\templates\generic\delete_template.html:72
|
||||
msgid "Confirm"
|
||||
msgstr "Bevestigen"
|
||||
msgstr "Bevestig"
|
||||
|
||||
#: .\cookbook\templates\account\email_confirm.html:29
|
||||
#, python-format
|
||||
@@ -1052,7 +1052,7 @@ msgstr "Beheer"
|
||||
#: .\cookbook\templates\base.html:351
|
||||
#: .\cookbook\templates\space_overview.html:25
|
||||
msgid "Your Spaces"
|
||||
msgstr "Jouw Spaces"
|
||||
msgstr "Jouw Ruimtes"
|
||||
|
||||
#: .\cookbook\templates\base.html:362
|
||||
#: .\cookbook\templates\space_overview.html:6
|
||||
@@ -1996,12 +1996,12 @@ msgstr "Eigenaar"
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:57
|
||||
msgid "Leave Space"
|
||||
msgstr "Verlaat Space"
|
||||
msgstr "Verlaat Ruimte"
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:78
|
||||
#: .\cookbook\templates\space_overview.html:88
|
||||
msgid "Join Space"
|
||||
msgstr "Sluit aan bij ruimte"
|
||||
msgstr "Sluit aan bij Ruimte"
|
||||
|
||||
#: .\cookbook\templates\space_overview.html:81
|
||||
msgid "Join an existing space."
|
||||
@@ -2237,7 +2237,7 @@ msgstr "Er bestaat geen {self.basename} met id {target}"
|
||||
|
||||
#: .\cookbook\views\api.py:250 .\cookbook\views\api.py:251
|
||||
msgid "Cannot merge with child object!"
|
||||
msgstr "Kan niet met kindobject samenvoegen!"
|
||||
msgstr "Kan niet met sub object samenvoegen!"
|
||||
|
||||
#: .\cookbook\views\api.py:288 .\cookbook\views\api.py:289
|
||||
#, python-brace-format
|
||||
@@ -2592,7 +2592,7 @@ msgstr "Uitnodigingslink"
|
||||
|
||||
#: .\cookbook\views\delete.py:168
|
||||
msgid "Space Membership"
|
||||
msgstr "Space Lidmaatschap"
|
||||
msgstr "Ruimte Lidmaatschap"
|
||||
|
||||
#: .\cookbook\views\edit.py:84
|
||||
msgid "You cannot edit this storage!"
|
||||
|
||||
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
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.
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
File diff suppressed because it is too large
Load Diff
34
cookbook/migrations/0223_auto_20250831_1111.py
Normal file
34
cookbook/migrations/0223_auto_20250831_1111.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.2.22 on 2025-08-31 09:11
|
||||
|
||||
from django.db import migrations
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
|
||||
def migrate_comments(apps, schema_editor):
|
||||
with scopes_disabled():
|
||||
Comment = apps.get_model('cookbook', 'Comment')
|
||||
CookLog = apps.get_model('cookbook', 'CookLog')
|
||||
|
||||
cook_logs = []
|
||||
|
||||
for c in Comment.objects.all():
|
||||
cook_logs.append(CookLog(
|
||||
recipe=c.recipe,
|
||||
created_by=c.created_by,
|
||||
created_at=c.created_at,
|
||||
comment=c.text,
|
||||
space=c.recipe.space,
|
||||
))
|
||||
|
||||
CookLog.objects.bulk_create(cook_logs, unique_fields=('recipe', 'comment', 'created_at', 'created_by'))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0222_alter_shoppinglistrecipe_created_by_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_comments),
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
# Generated by Django 4.2.22 on 2025-09-05 06:51
|
||||
|
||||
import cookbook.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('cookbook', '0223_auto_20250831_1111'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='ai_credits_balance',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='ai_credits_monthly',
|
||||
field=models.IntegerField(default=100),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AiProvider',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('api_key', models.CharField(max_length=2048)),
|
||||
('model_name', models.CharField(max_length=256)),
|
||||
('url', models.CharField(blank=True, max_length=2048, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('space', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AiLog',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('function', models.CharField(max_length=64)),
|
||||
('credit_cost', models.DecimalField(decimal_places=4, max_digits=16)),
|
||||
('credits_from_balance', models.BooleanField(default=False)),
|
||||
('input_tokens', models.IntegerField(default=0)),
|
||||
('output_tokens', models.IntegerField(default=0)),
|
||||
('start_time', models.DateTimeField(null=True)),
|
||||
('end_time', models.DateTimeField(null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('ai_provider', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.aiprovider')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
|
||||
],
|
||||
bases=(models.Model, cookbook.models.PermissionModelMixin),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0225_space_ai_enabled.py
Normal file
18
cookbook/migrations/0225_space_ai_enabled.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.22 on 2025-09-08 19:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0224_space_ai_credits_balance_space_ai_credits_monthly_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='space',
|
||||
name='ai_enabled',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.22 on 2025-09-08 20:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0225_space_ai_enabled'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='aiprovider',
|
||||
name='log_credit_cost',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='space',
|
||||
name='ai_credits_monthly',
|
||||
field=models.IntegerField(default=10000),
|
||||
),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user