Compare commits

...

522 Commits
1.0.2 ... 1.0.5

Author SHA1 Message Date
vabene1111
6cabeba3cb Merge branch 'develop' 2022-01-24 18:25:49 +01:00
vabene1111
90bb67ff89 compiled translations 2022-01-24 18:25:33 +01:00
vabene1111
69ed987db8 Merge pull request #1400 from geisterfurz007/chore/filename-consistency
k8s yaml file consistency
2022-01-23 21:58:16 +01:00
vabene1111
638904abc8 Merge pull request #1399 from geisterfurz007/patch-1
Correct path to kubernetes files
2022-01-23 21:57:52 +01:00
vabene1111
a07bd452a9 Merge pull request #1405 from TandoorRecipes/dependabot/npm_and_yarn/vue/node-fetch-2.6.7
Bump node-fetch from 2.6.6 to 2.6.7 in /vue
2022-01-23 21:55:04 +01:00
dependabot[bot]
2398c00dfe Bump node-fetch from 2.6.6 to 2.6.7 in /vue
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.6 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.6...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-23 20:53:52 +00:00
vabene1111
7314da1a5f Merge pull request #1395 from TandoorRecipes/dependabot/npm_and_yarn/vue/nanoid-3.2.0
Bump nanoid from 3.1.30 to 3.2.0 in /vue
2022-01-23 21:52:31 +01:00
geisterfurz007
9c80a10652 Move yml file to yaml file 2022-01-22 22:48:12 +00:00
geisterfurz007
30456c60e0 Correct path to kubernetes files 2022-01-22 23:24:27 +01:00
dependabot[bot]
202ef9509d Bump nanoid from 3.1.30 to 3.2.0 in /vue
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.30 to 3.2.0.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.30...3.2.0)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-22 10:32:18 +00:00
糖多
95b10bc01c Translated using Weblate (Chinese (Simplified))
Currently translated at 95.1% (275 of 289 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/zh_Hans/
2022-01-22 03:31:00 +00:00
糖多
289387f235 Translated using Weblate (Chinese (Simplified))
Currently translated at 88.7% (504 of 568 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/zh_Hans/
2022-01-22 03:31:00 +00:00
vabene1111
92c8afdf8f Merge pull request #1392 from smilerz/add-recipe
Add recipe from shopping list
2022-01-21 20:35:01 +01:00
Chris Scoggins
6e2374737e Squashed commit of the following:
commit a30a27c755
Author: vabene1111 <vabene1234@googlemail.com>
Date:   Fri Jan 21 17:49:27 2022 +0100

    added keyword clicking to recipe view and fixed deleted keyword showing in search when passed via parameter

commit f274f31e80
Author: vabene1111 <vabene1234@googlemail.com>
Date:   Fri Jan 21 16:56:47 2022 +0100

    fixed unit search on importer page

commit 20adcc0e83
Author: vabene1111 <vabene1234@googlemail.com>
Date:   Fri Jan 21 16:44:03 2022 +0100

    fixed v2 autosync flickering
Merge branch 'develop' into add-recipe
2022-01-21 13:11:33 -06:00
Chris Scoggins
f0b05808b8 moved Undefined to be first category 2022-01-21 12:36:54 -06:00
Chris Scoggins
250c3ce5b2 rebase with develop 2022-01-21 12:11:01 -06:00
Chris Scoggins
7916635716 add recipes to shopping list 2022-01-21 12:01:46 -06:00
vabene1111
a30a27c755 added keyword clicking to recipe view and fixed deleted keyword showing in search when passed via parameter 2022-01-21 17:49:27 +01:00
vabene1111
f274f31e80 fixed unit search on importer page 2022-01-21 16:56:47 +01:00
vabene1111
20adcc0e83 fixed v2 autosync flickering 2022-01-21 16:44:03 +01:00
Kaibu
c5b70b94c7 left handed only on mobile 2022-01-21 00:48:57 +01:00
Kaibu
c90e5d72af shopping list ux optimization, left handed mode 2022-01-21 00:13:36 +01:00
Sebastian Weber
0cf0fcea0a Translated using Weblate (German)
Currently translated at 89.6% (509 of 568 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2022-01-20 22:47:11 +00:00
vabene1111
ab5bff62e3 Merge pull request #1381 from sebweb3r/spelling_mistakes
Fix typos
2022-01-20 17:44:49 +01:00
vabene1111
001edecdd3 fixed quick entry for shopping v2 2022-01-20 15:54:45 +01:00
vabene1111
d27b39f7de changed default for auto on hand after shopping 2022-01-20 15:46:49 +01:00
Tomasz Klimczak
ddbbd53ace Translated using Weblate (Polish)
Currently translated at 100.0% (285 of 285 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/
2022-01-20 14:24:20 +00:00
Philipp Wensauer
0360d443ea Translated using Weblate (German)
Currently translated at 87.0% (248 of 285 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2022-01-20 14:24:19 +00:00
Sebastian Weber
c20e982fb1 Fix typos 2022-01-20 00:50:41 +01:00
Kaibu
0f7dc096cb shopping list ux 2022-01-20 00:29:10 +01:00
vabene1111
fc9eb249a8 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2022-01-19 17:37:01 +01:00
vabene1111
4a9e027849 changed asset caching strategy
there were to many issues with stale content breaking the application thus policy was changed to network first. might make another fix to split between more static assets (bootstrap/libraries/...) and more actively changed ones like the frontend
2022-01-19 17:36:57 +01:00
vabene1111
890817ef6d Merge pull request #1374 from smilerz/patch-updated-search
fix sort by new and show recent recipes
2022-01-19 17:01:46 +01:00
vabene1111
61a253675c fixed default import / export 2022-01-19 16:08:03 +01:00
smilerz
530b1a8986 fix sort by new 2022-01-19 08:39:40 -06:00
vabene1111
631d594f45 Merge pull request #1372 from MaxJa4/patch-1
Added GH template for documentation issues
2022-01-19 14:39:49 +01:00
MaxJa4
3fcea5af0a Added GH template for documentation issues
Added GH template for documentation issues.
Feel free to adjust.
2022-01-19 10:05:36 +01:00
vabene1111
07195b74a3 Merge pull request #1368 from smilerz/fix-mutliselect
fix multiselect
2022-01-18 22:52:08 +01:00
vabene1111
9e9a61e94e changed tests to support removed step type 2022-01-18 22:50:02 +01:00
smilerz
18c45771e7 fix multiselect 2022-01-18 15:44:11 -06:00
vabene1111
42aaed011c added saving of supermarket in shopping v2 2022-01-18 22:42:10 +01:00
vabene1111
66d29d10bf fixed shopping v2 export drop down alignment 2022-01-18 22:38:42 +01:00
vabene1111
dfa4f444ef Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2022-01-18 22:36:05 +01:00
vabene1111
12f2d3c7b3 Merge branch 'feature/recipe-edit-view-refactor' into develop 2022-01-18 22:36:00 +01:00
vabene1111
f9c68e9fcc layout tweaks 2022-01-18 22:35:53 +01:00
vabene1111
d65c881fde Merge pull request #1366 from smilerz/super-cat-fix
fix supermarket categories
2022-01-18 22:14:42 +01:00
vabene1111
7bf9f18402 allow file uploading in recipe editor 2022-01-18 21:36:20 +01:00
vabene1111
3ea96d4102 basics 2022-01-18 21:09:08 +01:00
smilerz
b3417be2ec fix supermarket categories 2022-01-18 13:26:07 -06:00
vabene1111
8d24ae9008 small tweaks to the recipe editor 2022-01-18 19:55:51 +01:00
vabene1111
a9d8080ec2 Merge pull request #1358 from smilerz/expert-settings
enable/disable treeselect
2022-01-18 16:17:36 +01:00
vabene1111
fe09278b0e silenced translation warnings 2022-01-18 16:16:11 +01:00
smilerz
2a13a341dd enable/disable treeselect 2022-01-18 08:17:54 -06:00
vabene1111
b382ab9024 step rendering improvements 2022-01-18 15:09:51 +01:00
vabene1111
7ff7d157dc updated translations 2022-01-18 14:53:16 +01:00
vabene1111
24c476830d Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2022-01-18 14:51:44 +01:00
vabene1111
2d0a638c0a fixed empty step headers 2022-01-18 14:51:34 +01:00
Tomasz Klimczak
70b8a50d1d Translated using Weblate (Polish)
Currently translated at 100.0% (284 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/
2022-01-18 13:43:20 +00:00
糖多
05df133960 Translated using Weblate (Chinese (Simplified))
Currently translated at 58.4% (166 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/zh_Hans/
2022-01-18 13:43:20 +00:00
Jesse
426f4d3e77 Translated using Weblate (Dutch)
Currently translated at 100.0% (284 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nl/
2022-01-18 13:43:20 +00:00
Philipp Wensauer
6b2ac3f873 Translated using Weblate (German)
Currently translated at 87.3% (248 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2022-01-18 13:43:20 +00:00
糖多
1986da7f6e Translated using Weblate (Chinese (Simplified))
Currently translated at 28.6% (146 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/zh_Hans/
2022-01-18 13:43:20 +00:00
Philipp Wensauer
cc7b9bba32 Translated using Weblate (German)
Currently translated at 87.3% (248 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2022-01-18 13:43:20 +00:00
Christoph Koch
8e0c709427 Translated using Weblate (German)
Currently translated at 87.3% (248 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2022-01-18 13:43:20 +00:00
Florian
1ed965adcd Translated using Weblate (German)
Currently translated at 87.3% (248 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2022-01-18 13:43:20 +00:00
vabene1111
8ced587562 fixed nginx config example for manual install 2022-01-18 14:02:11 +01:00
vabene1111
a0fd1f4104 fixed step header rendering 2022-01-18 14:00:25 +01:00
vabene1111
7fbc1cd8d1 Merge pull request #1354 from smilerz/facet-fix
better fix for counting facets
2022-01-18 07:59:36 +01:00
vabene1111
ba1f10cd3a Merge branch 'develop' into facet-fix 2022-01-18 07:59:32 +01:00
smilerz
4e0cc34d41 better fix for counting facets 2022-01-17 17:18:43 -06:00
Kaibu
ef4ce62f5b custom class selection for lookupinput comp 2022-01-17 23:48:57 +01:00
Kaibu
b990462bdb Merge branch 'develop' of https://github.com/vabene1111/recipes into develop
# Conflicts:
#	vue/src/components/Modals/LookupInput.vue
2022-01-17 23:48:12 +01:00
vabene1111
5e34c6ddf0 Merge pull request #1353 from smilerz/supermarket-category
fix missing label on first supermarket category
2022-01-17 23:29:49 +01:00
smilerz
d8d76ae9e0 fix missing label supermarket category 2022-01-17 16:12:54 -06:00
Kaibu
c60141940d shopping list ux improvements 2022-01-17 23:02:42 +01:00
vabene1111
532d32c194 fixed shopping user save setting would not work 2022-01-17 22:41:38 +01:00
vabene1111
54721a0a62 also added space to bot 2022-01-17 22:37:14 +01:00
vabene1111
c27933548d fixed order of delete 2022-01-17 22:28:02 +01:00
vabene1111
d04e9518cb fixed telegram shopping bot 2022-01-17 22:13:36 +01:00
vabene1111
b9065f7052 added space deletion feature 2022-01-17 22:03:57 +01:00
vabene1111
c8c29e1b5a fixed performance issue 2022-01-17 21:14:22 +01:00
smilerz
5724ef9511 fix boolean directive 2022-01-17 14:02:58 -06:00
vabene1111
2595a26fb4 Merge pull request #1351 from MaxJa4/patch-1
Add hint about trailing slashes for subpath setups
2022-01-17 20:26:02 +01:00
vabene1111
e1c7305c07 switcher basically working again 2022-01-17 20:22:37 +01:00
vabene1111
418c38423f Merge pull request #1352 from smilerz/fix-search
force list params to list
2022-01-17 20:15:01 +01:00
smilerz
cc5be844d5 force list params to list 2022-01-17 13:13:26 -06:00
vabene1111
90b6f9ad06 fixed sub recipe issue 2022-01-17 19:54:16 +01:00
MaxJa4
437296415e Update .env.template 2022-01-17 18:43:14 +01:00
MaxJa4
a8c885bd21 Remove newline at the end 2022-01-17 18:41:41 +01:00
vabene1111
a539d14aad wip switcher 2022-01-17 18:15:23 +01:00
MaxJa4
2b0541bd74 Add hint about trailing slashes for subpath setups
Add hint about trailing slashes for subpath setups due to recent issue on Discord.
2022-01-17 18:05:39 +01:00
vabene1111
3f53a924e1 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2022-01-17 18:01:47 +01:00
vabene1111
0ed9100fb1 Merge pull request #1350 from TandoorRecipes/patch-empty-recipe_list
Update recipe_search.py
2022-01-17 17:59:19 +01:00
vabene1111
d23158839b Revert "temporarily disable recipe switcher"
This reverts commit d2b796ddd2.
2022-01-17 17:58:09 +01:00
vabene1111
d2b796ddd2 temporarily disable recipe switcher 2022-01-17 17:58:04 +01:00
vabene1111
8b1e80efeb wip 2022-01-17 17:51:29 +01:00
smilerz
85ecac3a17 Update recipe_search.py 2022-01-17 10:10:38 -06:00
vabene1111
e0b8d6fcc3 added exception catch to nextcloud importer
to handle empty folders in sync
2022-01-17 17:00:08 +01:00
vabene1111
edd47873f7 fixed signup button and autofocus on user input fields 2022-01-17 16:51:04 +01:00
vabene1111
c14dd04261 Merge pull request #1348 from smilerz/fuzzy_search
Fuzzy search
2022-01-17 16:50:36 +01:00
smilerz
769365d624 Merge branch 'fuzzy_search' of github.com:smilerz/recipes into fuzzy_search 2022-01-17 09:46:33 -06:00
smilerz
ddb9e70d31 fix url_import 2022-01-17 09:46:26 -06:00
vabene1111
a376728120 fixed keyword creation in exporter #1213 2022-01-17 16:29:29 +01:00
vabene1111
306f90aa98 recipe editor decimal fixes 2022-01-17 16:27:10 +01:00
vabene1111
a19ad706ce Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2022-01-17 16:22:18 +01:00
vabene1111
4af6de7425 Revert "Merge pull request #1280 from MarcusWolschon/feature1275_readable_export_file_names"
This reverts commit c4f40b9639, reversing
changes made to 93b868bc69.
2022-01-17 16:22:11 +01:00
vabene1111
8f3044dbee Merge pull request #1316 from tomtjes/docs-swag-example
add swag config example
2022-01-17 16:04:40 +01:00
vabene1111
7c5ffdaef4 Merge pull request #1347 from smilerz/fuzzy_search
Fuzzy search
2022-01-17 15:51:54 +01:00
vabene1111
30421d067e Merge branch 'develop' into fuzzy_search 2022-01-17 15:51:23 +01:00
vabene1111
d3b71e40c7 cleand up context menu code 2022-01-17 15:43:35 +01:00
vabene1111
1a84a8fe80 Merge pull request #1289 from MarcusWolschon/features/1093_recipe_link_in_plan
#1093 Recipe link in plan
2022-01-17 15:39:11 +01:00
vabene1111
16cb99f915 Merge pull request #1317 from mheiland/patch-1
Example for third-party authentication
2022-01-17 15:37:48 +01:00
vabene1111
a451f722a1 Merge pull request #1327 from tomtjes/docs-faq-amendments
FAQ amendments
2022-01-17 15:35:43 +01:00
smilerz
dde350c8af prettier cleanup 2022-01-17 08:35:19 -06:00
smilerz
37971acb48 refactor recipe search 2022-01-17 08:26:34 -06:00
vabene1111
f12196d1c6 Merge pull request #1343 from MatthiasLohr/feature/db-url-path
Allow to specify an actual path using DATABASE_URL
2022-01-17 15:20:45 +01:00
vabene1111
d4242a244d Merge branch 'master' into develop
# Conflicts:
#	cookbook/forms.py
2022-01-17 15:16:37 +01:00
vabene1111
8a7c4e11c9 fixed invite link counting 2022-01-17 15:16:13 +01:00
vabene1111
745bb58c7e fixed valid filter on invite link counter 2022-01-17 15:02:41 +01:00
Matthias Lohr
b3e971fe09 allow to specify an actual path using DATABASE_URL 2022-01-17 11:21:36 +01:00
Oliver Cervera
0c603e3665 Translated using Weblate (Italian)
Currently translated at 84.1% (239 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/it/
2022-01-17 07:56:19 +00:00
Oliver Cervera
fed9cfeeb7 Translated using Weblate (Italian)
Currently translated at 96.6% (492 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/it/
2022-01-17 07:56:19 +00:00
vabene1111
5a65fd2231 Merge pull request #1331 from TandoorRecipes/dependabot/npm_and_yarn/vue/follow-redirects-1.14.7
Bump follow-redirects from 1.14.6 to 1.14.7 in /vue
2022-01-16 17:29:35 +01:00
SMunos
c2a763fa4c Translated using Weblate (French)
Currently translated at 100.0% (284 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2022-01-16 07:07:02 +00:00
Josselin du PLESSIS
528767a835 Translated using Weblate (French)
Currently translated at 100.0% (284 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2022-01-16 07:07:02 +00:00
糖多
9b182f6076 Translated using Weblate (Chinese (Simplified))
Currently translated at 32.0% (91 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/zh_Hans/
2022-01-16 07:07:02 +00:00
糖多
968b710b49 Translated using Weblate (Chinese (Simplified))
Currently translated at 28.6% (146 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/zh_Hans/
2022-01-16 07:07:02 +00:00
Josselin du PLESSIS
f11e07d347 Translated using Weblate (French)
Currently translated at 100.0% (509 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/fr/
2022-01-16 07:07:02 +00:00
dependabot[bot]
24e42496a7 Bump follow-redirects from 1.14.6 to 1.14.7 in /vue
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.6 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.6...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-16 01:02:54 +00:00
vabene1111
9da496cb6d Merge pull request #1323 from MaxJa4/patch-1
Added Apache2 in the bug report template
2022-01-15 21:46:04 +01:00
tomtjes
99b3ed8464 add FAQ for PWA 2022-01-15 13:58:40 -05:00
tomtjes
281535e756 phrase FAQ as questions 2022-01-15 13:57:20 -05:00
MaxJa4
9221533ae7 Added Apache2 in the bug report template
Added Apache2 as selectable option in the bug report template
2022-01-15 12:56:01 +01:00
mheiland
f07690d7e3 Example for third-party authentication
Providing an example to integrate Keycloak as IAM for Tandoor. Hinting that both SOCIAL* variables are required.
2022-01-15 00:24:56 +01:00
SMunos
8cebc98d3b Translated using Weblate (French)
Currently translated at 100.0% (284 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2022-01-14 23:18:42 +00:00
FrenchAnon
965d2c05e7 Translated using Weblate (French)
Currently translated at 100.0% (284 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2022-01-14 23:18:37 +00:00
Josselin du PLESSIS
17ad01ae8c Translated using Weblate (French)
Currently translated at 100.0% (284 of 284 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2022-01-14 23:18:36 +00:00
tomtjes
51620a34d9 add swag config example 2022-01-14 15:10:22 -05:00
Tomasz Klimczak
91fcb1b822 Translated using Weblate (Polish)
Currently translated at 80.9% (225 of 278 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/
2022-01-14 19:11:20 +00:00
Tiago Rascazzi
01d5ab92c5 Translated using Weblate (French)
Currently translated at 72.6% (202 of 278 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2022-01-14 19:11:20 +00:00
vabene1111
79c8d26e8c Merge pull request #1311 from smilerz/patch-2
fix bug creating food with create form
2022-01-14 17:18:30 +01:00
vabene1111
9486b08e20 Merge pull request #1309 from MaxJa4/develop
Disabled old issue templates and added new ones with new GitHub issues format
2022-01-14 16:57:10 +01:00
Marcus Wolschon
934eeee5c4 #1093 code cleanup 2022-01-14 13:56:46 +01:00
Marcus Wolschon
2927333bf1 #1093 code cleanup 2022-01-14 13:52:42 +01:00
smilerz
0e1153ce3a deleted extraneous emit 2022-01-13 17:40:26 -06:00
smilerz
b3f05b0bfd fix bug creating food with create form 2022-01-13 16:50:15 -06:00
smilerz
6d9a90c6ba fix onhand_users 2022-01-13 16:49:10 -06:00
smilerz
6555df824d Merge branch 'develop' into feature/custom_filters 2022-01-13 16:01:05 -06:00
smilerz
e313481fc8 WIP 2022-01-13 16:00:59 -06:00
vabene1111
d36033a8b5 Merge pull request #1310 from TandoorRecipes/smilerz-patch-1
Update serializer.py
2022-01-13 22:53:58 +01:00
smilerz
d2d2765765 Update serializer.py
image location should use MEDIA_URL alone and not combine with SCRIPT_NAME
2022-01-13 15:53:24 -06:00
smilerz
3aa7f6a367 Merge branch 'develop' into feature/custom_filters 2022-01-13 14:41:53 -06:00
Maximilian Jannack
ffa91863dd Added config.yml for FAQ link 2022-01-13 21:26:15 +01:00
Maximilian Jannack
cf2d33daad Disabled old issue templates and added new ones with new GitHub issues format 2022-01-13 21:17:08 +01:00
vabene1111
506d7a8bb2 Merge pull request #1306 from TandoorRecipes/performance_refactor
facets cache-only on initial load
2022-01-13 19:21:11 +01:00
smilerz
8b1233be62 facets cache-only on initial load 2022-01-13 12:02:28 -06:00
vabene1111
9a3a4b9450 Merge pull request #1262 from MaxJa4/patch-2
Extension and hopefully simplification of bug reporting process
2022-01-13 18:20:41 +01:00
vabene1111
2db300a8a4 Merge pull request #1253 from MaxJa4/patch-1
Some additional info for reverse proxy setups.
2022-01-13 17:49:55 +01:00
vabene1111
a2dc8d8988 Merge pull request #1300 from TandoorRecipes/performance_refactor
Performance refactor
2022-01-13 17:06:04 +01:00
smilerz
798aa7f179 detect empty queryset 2022-01-12 16:55:39 -06:00
smilerz
22953b0591 trees in recipe search loaded asynchronously 2022-01-12 16:21:36 -06:00
MaxJa4
0b8881c511 Merge branch 'TandoorRecipes:develop' into patch-1 2022-01-12 21:55:41 +01:00
MaxJa4
dc10bf2c49 Add general note and remove duplicate subchapter from docker installation docs
Add general note and remove duplicate subchapter from docker installation docs
2022-01-12 21:55:31 +01:00
smilerz
20d61160ba refactor get_facets as RecipeFacets class 2022-01-12 12:21:28 -06:00
vabene1111
c4f40b9639 Merge pull request #1280 from MarcusWolschon/feature1275_readable_export_file_names
#1275
2022-01-12 17:28:03 +01:00
Marcus Wolschon
8f08ba7114 #1093 conditional receipt link in plan 2022-01-12 16:15:55 +01:00
Marcus Wolschon
8a4f35e592 #1093 Recipe link in plan
#1093 add a recipe link into meal plan
2022-01-12 11:37:08 +01:00
Marcus Wolschon
80de87d459 #1275
#1275 readable receipt file names in default export
2022-01-11 21:31:02 +01:00
smilerz
f9b04a3f1e bug fix 2022-01-11 08:33:42 -06:00
smilerz
f7cb067b52 construct values in queryset instead of serializer methods 2022-01-11 07:24:59 -06:00
smilerz
25ccea90e0 WIP 2022-01-10 15:05:56 -06:00
vabene1111
93b868bc69 fixed valid filter on invite link counter 2022-01-09 18:25:38 +01:00
MaxJa4
acfb02cc0e Extension and hopefully simplification of bug template
To get more information about bugs and prohibit having to ask one by one for specific information, I extended and redesigned the bug template.
Fell free to change parts or suggest changes.
Please note, that all explanatory parts are hidden as comments in the markdown (which the user will see when creating a bug ticket) so they don't unnecessarily clutter the finished bug report.
2022-01-09 14:21:45 +01:00
MaxJa4
79c8edd354 Some additional info for reverse proxy setups.
Since there have been quite some people with basic docker setup issues when using a reverse proxy and very basic reverse proxies like a nginx running locally as a proxy or Caddy, I figured these added sentences might clear things up for some people.
Feel free to suggest additional topics which should be added or refined.
2022-01-07 20:14:49 +01:00
vabene1111
e1e53d12f8 playing around with the reciupe switcher 2022-01-07 16:19:25 +01:00
vabene1111
30683fe455 Merge pull request #1252 from smilerz/sw_at_ScriptName
change manifest paths to be relative
2022-01-07 15:39:49 +01:00
vabene1111
c20aae3efc fixed markdown issue 2022-01-07 11:13:13 +01:00
vabene1111
5e2ca250b0 fixed nav and export required recipe 2022-01-07 11:01:53 +01:00
vabene1111
d506952602 small PDF export tweaks 2022-01-07 10:55:27 +01:00
vabene1111
0a6abf9688 Merge pull request #1211 from TiagoRascazzi/develop
Added Saffron and PDF export format
2022-01-07 10:31:13 +01:00
vabene1111
6c4b1e76eb Merge pull request #1251 from smilerz/fail_connection_gracefully
ConnectError fail gracefully during URL import
2022-01-07 10:19:00 +01:00
vabene1111
1f391b794b Merge pull request #1250 from TandoorRecipes/dependabot/npm_and_yarn/vue/mermaid-8.13.8
Bump mermaid from 8.13.5 to 8.13.8 in /vue
2022-01-07 10:18:45 +01:00
smilerz
983d66c197 change manifest paths to be relative 2022-01-06 15:11:24 -06:00
dependabot[bot]
ab2098151b Bump mermaid from 8.13.5 to 8.13.8 in /vue
Bumps [mermaid](https://github.com/knsv/mermaid) from 8.13.5 to 8.13.8.
- [Release notes](https://github.com/knsv/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/docs/CHANGELOG.md)
- [Commits](https://github.com/knsv/mermaid/compare/8.13.5...8.13.8)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-06 19:48:16 +00:00
vabene1111
6053b1419c fixed undefined var 2022-01-06 16:57:17 +01:00
vabene1111
5c98f06208 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2022-01-06 16:34:53 +01:00
vabene1111
c141dc850f Merge pull request #1245 from TandoorRecipes/fix_scopes_subfolder
added prefix to request.path.startswith() conditionals
2022-01-06 16:30:27 +01:00
vabene1111
0283835a96 Merge pull request #1240 from TandoorRecipes/feature/shopping_list_v2
Feature/shopping list v2
2022-01-06 16:27:44 +01:00
vabene1111
724217f142 Merge branch 'develop' into feature/shopping_list_v2 2022-01-06 16:27:39 +01:00
vabene1111
0094fd28e2 Merge pull request #1247 from TandoorRecipes/feature/related_recipe_switcher
Feature/related recipe switcher
2022-01-06 16:22:54 +01:00
vabene1111
54b57a8bcb Merge branch 'develop' into feature/related_recipe_switcher 2022-01-06 16:22:44 +01:00
vabene1111
0778025a0c Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2022-01-06 16:08:17 +01:00
vabene1111
063a0dec24 Merge branch 'master' into develop
# Conflicts:
#	requirements.txt
2022-01-06 16:08:06 +01:00
smilerz
b09acefa6a fix 1244 2022-01-06 07:49:53 -06:00
smilerz
6a1fcabae0 added prefix to request.path.startswith() conditionals 2022-01-06 07:37:02 -06:00
smilerz
13115a1e53 fixes 1176 2022-01-05 17:49:38 -06:00
smilerz
f65b5d0733 clear unit after adding shopping list item 2022-01-05 16:41:26 -06:00
smilerz
922eb7402b fix tess 2022-01-05 15:20:10 -06:00
smilerz
2c76fb7b69 make food onhand when complete shopping entry 2022-01-05 15:20:10 -06:00
smilerz
7c89117e04 make on_hand multiuser 2022-01-05 15:20:10 -06:00
smilerz
b919fb4ae8 quick add shoppinglist 2022-01-05 15:20:10 -06:00
smilerz
29aa52aa3d fix saving old list 2022-01-05 15:20:10 -06:00
smilerz
214db80dac add category context menu 2022-01-05 15:20:10 -06:00
Hrachya Kocharyan
25c1689ca0 Translated using Weblate (Armenian)
Currently translated at 39.4% (82 of 208 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/hy/
2022-01-05 07:00:36 +00:00
Tiago Rascazzi
10001dde7b Fix default export 2022-01-04 13:55:13 -05:00
Tiago Rascazzi
578154510b Merge shopping_list develop 2022-01-04 13:19:34 -05:00
vabene1111
8a99907a51 reverted some updates 2022-01-04 17:32:17 +01:00
vabene1111
636fa8f318 Merge pull request #1200 from TandoorRecipes/dependabot/npm_and_yarn/vue/vue-simple-calendar-6.0.3
Bump vue-simple-calendar from 5.0.1 to 6.0.3 in /vue
2022-01-04 16:23:09 +01:00
vabene1111
7efbc9c42e Merge pull request #1197 from TandoorRecipes/dependabot/npm_and_yarn/vue/vue/eslint-config-typescript-10.0.0
Bump @vue/eslint-config-typescript from 9.1.0 to 10.0.0 in /vue
2022-01-04 16:23:03 +01:00
dependabot[bot]
b05639110a Bump vue-simple-calendar from 5.0.1 to 6.0.3 in /vue
Bumps [vue-simple-calendar](https://github.com/richardtallent/vue-simple-calendar) from 5.0.1 to 6.0.3.
- [Release notes](https://github.com/richardtallent/vue-simple-calendar/releases)
- [Changelog](https://github.com/richardtallent/vue-simple-calendar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/richardtallent/vue-simple-calendar/commits/v6.0.3)

---
updated-dependencies:
- dependency-name: vue-simple-calendar
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 15:22:08 +00:00
vabene1111
1fe673ba1e Merge pull request #1230 from TandoorRecipes/dependabot/pip/django-cors-headers-3.10.1
Bump django-cors-headers from 3.10.0 to 3.10.1
2022-01-04 16:21:54 +01:00
vabene1111
0a89bf4a10 Merge pull request #1229 from TandoorRecipes/dependabot/pip/django-4.0.1
Bump django from 3.2.10 to 4.0.1
2022-01-04 16:21:49 +01:00
vabene1111
049d218f7b Merge pull request #1228 from TandoorRecipes/dependabot/pip/requests-2.27.0
Bump requests from 2.26.0 to 2.27.0
2022-01-04 16:21:45 +01:00
vabene1111
0030775e55 Merge pull request #1227 from TandoorRecipes/dependabot/pip/recipe-scrapers-13.10.1
Bump recipe-scrapers from 13.7.0 to 13.10.1
2022-01-04 16:21:40 +01:00
dependabot[bot]
cd49311cba Bump django from 3.2.10 to 4.0.1
Bumps [django](https://github.com/django/django) from 3.2.10 to 4.0.1.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.10...4.0.1)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 15:21:11 +00:00
dependabot[bot]
f7af4b9cd2 Bump requests from 2.26.0 to 2.27.0
Bumps [requests](https://github.com/psf/requests) from 2.26.0 to 2.27.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.26.0...v2.27.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 15:21:03 +00:00
dependabot[bot]
6c205e2fc6 Bump django-cors-headers from 3.10.0 to 3.10.1
Bumps [django-cors-headers](https://github.com/adamchainz/django-cors-headers) from 3.10.0 to 3.10.1.
- [Release notes](https://github.com/adamchainz/django-cors-headers/releases)
- [Changelog](https://github.com/adamchainz/django-cors-headers/blob/main/HISTORY.rst)
- [Commits](https://github.com/adamchainz/django-cors-headers/compare/3.10.0...3.10.1)

---
updated-dependencies:
- dependency-name: django-cors-headers
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 15:21:01 +00:00
dependabot[bot]
938f5560fb Bump recipe-scrapers from 13.7.0 to 13.10.1
Bumps [recipe-scrapers](https://github.com/hhursev/recipe-scrapers) from 13.7.0 to 13.10.1.
- [Release notes](https://github.com/hhursev/recipe-scrapers/releases)
- [Commits](https://github.com/hhursev/recipe-scrapers/compare/13.7.0...13.10.1)

---
updated-dependencies:
- dependency-name: recipe-scrapers
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 15:20:53 +00:00
vabene1111
6791de94d7 Merge pull request #1220 from TandoorRecipes/dependabot/pip/pillow-9.0.0
Bump pillow from 8.4.0 to 9.0.0
2022-01-04 16:19:24 +01:00
dependabot[bot]
884dd6b8f8 Bump pillow from 8.4.0 to 9.0.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.4.0 to 9.0.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.4.0...9.0.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 15:18:05 +00:00
vabene1111
d2bf0359c0 Merge pull request #1214 from TandoorRecipes/dependabot/pip/boto3-1.20.27
Bump boto3 from 1.20.19 to 1.20.27
2022-01-04 16:17:25 +01:00
vabene1111
f418d74639 Merge pull request #1218 from TandoorRecipes/dependabot/npm_and_yarn/vue/core-js-3.20.2
Bump core-js from 3.19.3 to 3.20.2 in /vue
2022-01-04 16:17:19 +01:00
vabene1111
68260a2929 Merge pull request #1219 from TandoorRecipes/dependabot/pip/lxml-4.7.1
Bump lxml from 4.6.5 to 4.7.1
2022-01-04 16:17:15 +01:00
vabene1111
0f5feac067 Merge pull request #1221 from TandoorRecipes/dependabot/pip/psycopg2-binary-2.9.3
Bump psycopg2-binary from 2.9.2 to 2.9.3
2022-01-04 16:17:05 +01:00
dependabot[bot]
fde892dd78 Bump boto3 from 1.20.19 to 1.20.27
Bumps [boto3](https://github.com/boto/boto3) from 1.20.19 to 1.20.27.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.20.19...1.20.27)

---
updated-dependencies:
- dependency-name: boto3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 07:29:59 +00:00
dependabot[bot]
e54d477b12 Bump psycopg2-binary from 2.9.2 to 2.9.3
Bumps [psycopg2-binary](https://github.com/psycopg/psycopg2) from 2.9.2 to 2.9.3.
- [Release notes](https://github.com/psycopg/psycopg2/releases)
- [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS)
- [Commits](https://github.com/psycopg/psycopg2/commits)

---
updated-dependencies:
- dependency-name: psycopg2-binary
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 07:29:58 +00:00
dependabot[bot]
29411b5a74 Bump lxml from 4.6.5 to 4.7.1
Bumps [lxml](https://github.com/lxml/lxml) from 4.6.5 to 4.7.1.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.5...lxml-4.7.1)

---
updated-dependencies:
- dependency-name: lxml
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 07:29:50 +00:00
vabene1111
02fcf70ab2 Merge pull request #1215 from TandoorRecipes/dependabot/pip/django-prometheus-2.2.0
Bump django-prometheus from 2.1.0 to 2.2.0
2022-01-04 08:29:33 +01:00
vabene1111
b661ee2a23 Merge pull request #1216 from TandoorRecipes/dependabot/pip/django-auth-ldap-4.0.0
Bump django-auth-ldap from 3.0.0 to 4.0.0
2022-01-04 08:29:29 +01:00
vabene1111
b71c115194 Merge pull request #1217 from TandoorRecipes/dependabot/pip/django-allauth-0.47.0
Bump django-allauth from 0.46.0 to 0.47.0
2022-01-04 08:29:23 +01:00
dependabot[bot]
fc0f92eecc Bump core-js from 3.19.3 to 3.20.2 in /vue
Bumps [core-js](https://github.com/zloirock/core-js) from 3.19.3 to 3.20.2.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/compare/v3.19.3...v3.20.2)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 07:02:59 +00:00
dependabot[bot]
555451f64e Bump django-prometheus from 2.1.0 to 2.2.0
Bumps [django-prometheus](https://github.com/korfuri/django-prometheus) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/korfuri/django-prometheus/releases)
- [Changelog](https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/korfuri/django-prometheus/compare/2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: django-prometheus
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 07:00:08 +00:00
dependabot[bot]
557c8ce3b9 Bump django-auth-ldap from 3.0.0 to 4.0.0
Bumps [django-auth-ldap](https://github.com/django-auth-ldap/django-auth-ldap) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/django-auth-ldap/django-auth-ldap/releases)
- [Changelog](https://github.com/django-auth-ldap/django-auth-ldap/blob/master/CHANGES)
- [Commits](https://github.com/django-auth-ldap/django-auth-ldap/compare/3.0.0...4.0.0)

---
updated-dependencies:
- dependency-name: django-auth-ldap
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 07:00:08 +00:00
vabene1111
b19190e9e2 Merge pull request #1199 from TandoorRecipes/dependabot/npm_and_yarn/vue/vue-i18n-8.26.8
Bump vue-i18n from 8.26.7 to 8.26.8 in /vue
2022-01-04 07:59:39 +01:00
dependabot[bot]
c9a01a001e Bump django-allauth from 0.46.0 to 0.47.0
Bumps [django-allauth](https://github.com/pennersr/django-allauth) from 0.46.0 to 0.47.0.
- [Release notes](https://github.com/pennersr/django-allauth/releases)
- [Changelog](https://github.com/pennersr/django-allauth/blob/master/ChangeLog.rst)
- [Commits](https://github.com/pennersr/django-allauth/compare/0.46.0...0.47.0)

---
updated-dependencies:
- dependency-name: django-allauth
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-04 06:59:33 +00:00
vabene1111
0a085bfafa Merge pull request #1193 from TandoorRecipes/dependabot/pip/cryptography-36.0.1
Bump cryptography from 36.0.0 to 36.0.1
2022-01-04 07:59:30 +01:00
vabene1111
84cd4671a2 Merge pull request #1195 from TandoorRecipes/dependabot/pip/djangorestframework-3.13.1
Bump djangorestframework from 3.12.4 to 3.13.1
2022-01-04 07:59:25 +01:00
vabene1111
c05e44fdce Merge pull request #1196 from TandoorRecipes/dependabot/pip/pytest-django-4.5.2
Bump pytest-django from 4.5.1 to 4.5.2
2022-01-04 07:59:21 +01:00
vabene1111
6478bb3bb8 Merge pull request #1192 from TandoorRecipes/dependabot/pip/boto3-1.20.26
Bump boto3 from 1.20.19 to 1.20.26
2022-01-04 07:59:16 +01:00
vabene1111
e99c3af5d6 Merge pull request #1145 from TandoorRecipes/feature/shopping_list_v2
Feature/shopping list v2
2022-01-04 07:58:43 +01:00
vabene1111
4047febec9 Merge branch 'develop' into feature/shopping_list_v2 2022-01-04 07:58:36 +01:00
TiagoRascazzi
d1c8515b77 Delete example.pdf 2022-01-03 15:21:37 -05:00
Tiago Rascazzi
0aafd8d8b2 Added Saffron export format 2022-01-03 13:28:21 -05:00
Tiago Rascazzi
56ee5671ea restructured integration do_export 2022-01-03 12:46:34 -05:00
Tiago Rascazzi
ba032e9353 Added PDF export format 2022-01-03 00:58:02 -05:00
Tiago Rascazzi
1c30e643c3 Print format avoid breaking Ingredient and step 2022-01-02 22:28:16 -05:00
dependabot[bot]
a5638ea8a1 Bump vue-i18n from 8.26.7 to 8.26.8 in /vue
Bumps [vue-i18n](https://github.com/kazupon/vue-i18n) from 8.26.7 to 8.26.8.
- [Release notes](https://github.com/kazupon/vue-i18n/releases)
- [Changelog](https://github.com/kazupon/vue-i18n/blob/v8.x/CHANGELOG.md)
- [Commits](https://github.com/kazupon/vue-i18n/compare/v8.26.7...v8.26.8)

---
updated-dependencies:
- dependency-name: vue-i18n
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-01 00:06:18 +00:00
dependabot[bot]
5b462d81b4 Bump @vue/eslint-config-typescript from 9.1.0 to 10.0.0 in /vue
Bumps [@vue/eslint-config-typescript](https://github.com/vuejs/eslint-config-typescript) from 9.1.0 to 10.0.0.
- [Release notes](https://github.com/vuejs/eslint-config-typescript/releases)
- [Commits](https://github.com/vuejs/eslint-config-typescript/compare/v9.1.0...v10.0.0)

---
updated-dependencies:
- dependency-name: "@vue/eslint-config-typescript"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-01 00:05:56 +00:00
dependabot[bot]
e7acecb16b Bump pytest-django from 4.5.1 to 4.5.2
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.5.1...v4.5.2)

---
updated-dependencies:
- dependency-name: pytest-django
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-01 00:03:42 +00:00
dependabot[bot]
58a0d96fbd Bump djangorestframework from 3.12.4 to 3.13.1
Bumps [djangorestframework](https://github.com/encode/django-rest-framework) from 3.12.4 to 3.13.1.
- [Release notes](https://github.com/encode/django-rest-framework/releases)
- [Commits](https://github.com/encode/django-rest-framework/compare/3.12.4...3.13.1)

---
updated-dependencies:
- dependency-name: djangorestframework
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-01 00:03:40 +00:00
dependabot[bot]
30b9ea7e9f Bump cryptography from 36.0.0 to 36.0.1
Bumps [cryptography](https://github.com/pyca/cryptography) from 36.0.0 to 36.0.1.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/36.0.0...36.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-01 00:03:32 +00:00
dependabot[bot]
d26a1b5698 Bump boto3 from 1.20.19 to 1.20.26
Bumps [boto3](https://github.com/boto/boto3) from 1.20.19 to 1.20.26.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.20.19...1.20.26)

---
updated-dependencies:
- dependency-name: boto3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-01 00:03:24 +00:00
smilerz
795f3084d9 Merge branch 'feature/related_recipe_switcher' of https://github.com/TandoorRecipes/recipes into feature/related_recipe_switcher 2021-12-31 09:01:36 -06:00
smilerz
931eae4361 fix switcher to be TZ sensitive 2021-12-31 09:01:31 -06:00
smilerz
80fc50e09b remove console message 2021-12-30 16:52:08 -06:00
smilerz
045a0b7d4f persist servings changes 2021-12-30 15:33:47 -06:00
smilerz
957c659a62 Squashed commit of shoppinglist_v2 2021-12-30 15:33:34 -06:00
smilerz
b282c46c1a Fix after rebase 2021-12-30 13:58:34 -06:00
smilerz
582e145a9f Fix after rebase 2021-12-30 13:55:38 -06:00
smilerz
79b4bc387e change ingore_inherit to inherit_fields 2021-12-30 12:54:39 -06:00
vabene1111
af9a2a89ec Merge branch 'develop' 2021-12-30 18:18:23 +01:00
vabene1111
c50a89c651 disabled tree fix at startup for now 2021-12-30 18:08:29 +01:00
vabene1111
f21587605a fixed sub recipe rendering 2021-12-30 18:03:08 +01:00
vabene1111
2e69a00fce improved recipe edit input validation 2021-12-30 17:57:04 +01:00
vabene1111
bddaa77f71 fixed space not honourd in invite link 2021-12-30 17:37:10 +01:00
vabene1111
3743a08996 marked required env fields 2021-12-30 09:47:36 +01:00
smilerz
3fafd43e58 merge ignore shopping with onhand 2021-12-29 16:32:19 -06:00
smilerz
2787b64a96 Merge branch 'feature/shopping_list_v2' of https://github.com/TandoorRecipes/recipes into feature/shopping_list_v2 2021-12-29 09:37:38 -06:00
smilerz
52d1069353 fix reactivity of detailed items 2021-12-29 09:36:18 -06:00
vabene1111
c961909342 fixed import 2021-12-29 15:54:20 +01:00
vabene1111
ccd0966d92 Merge branch 'feature/shopping_list_v2' of https://github.com/vabene1111/recipes into feature/shopping_list_v2
# Conflicts:
#	cookbook/static/django_js_reverse/reverse.js
#	cookbook/tests/api/test_api_shopping_recipe.py
#	vue/src/apps/ShoppingListView/ShoppingListView.vue
2021-12-29 15:26:22 +01:00
smilerz
a4f2c994a0 fix responsive view of add-to-shopping form 2021-12-28 16:32:26 -06:00
smilerz
c43b8e91da Fix after rebase 2021-12-28 12:05:14 -06:00
smilerz
58d025f1a5 change default status of shopping in recipe view 2021-12-28 12:03:36 -06:00
smilerz
c20e036d90 add path to generic cards for food/keywords 2021-12-28 12:03:36 -06:00
smilerz
2d0a7330f3 reset new list item after create 2021-12-28 12:03:36 -06:00
smilerz
279faadf46 pytest create recipe with ingredient as header 2021-12-28 12:03:36 -06:00
smilerz
5b287ad484 fix responsive display of detail shopping list 2021-12-28 12:03:35 -06:00
smilerz
e257a8d29b fix responsive display of shopping list 2021-12-28 12:03:35 -06:00
smilerz
889fa7b8ea update console messages 2021-12-28 12:03:35 -06:00
smilerz
a3008a6091 WIP 2021-12-28 12:03:35 -06:00
smilerz
24bef756e8 fix filter_to_supermarket setting 2021-12-28 12:03:35 -06:00
smilerz
b4510a2cc1 fix categories in supermarket edit modal 2021-12-28 12:03:35 -06:00
smilerz
63fe174070 fix add to shopping from MealPlan Modal 2021-12-28 12:03:35 -06:00
smilerz
0f4bd9972e delete supermarkets and categories from shopping list view 2021-12-28 12:03:35 -06:00
smilerz
9794d544cc Squashed commit of the following:
commit 7837467c30
Merge: aaaae5b1b 84759383f
Author: vabene1111 <vabene1111@users.noreply.github.com>
Date:   Sat Dec 18 23:14:24 2021 +0100

    Merge pull request #1146 from auanasgheps/patch-1

    Add documentation about swag by linuxserver

commit 84759383fa
Author: Oliver Cervera <cervera93-19@yahoo.it>
Date:   Sat Dec 18 13:49:09 2021 +0100

    Add documentation about swag by linuxserver

    Documents behaviour in #959

commit aaaae5b1ba
Merge: 4a747f5cd ea62c10d9
Author: vabene1111 <vabene1111@users.noreply.github.com>
Date:   Thu Dec 16 18:10:48 2021 +0100

    Merge pull request #1143 from smilerz/fix_get_facet_api

    fix bug in get_facet_api

commit ea62c10d9a
Author: smilerz <smilerz@gmail.com>
Date:   Thu Dec 16 09:20:56 2021 -0600

    remove console message

commit 3516505dd1
Author: smilerz <smilerz@gmail.com>
Date:   Thu Dec 16 09:08:32 2021 -0600

    fix bug in get_facet_api
2021-12-28 12:03:35 -06:00
smilerz
e66897c1ea fix Shopping Modal to filter onhand from initial list 2021-12-28 12:03:35 -06:00
smilerz
2d94cb70ab fix shopping list api 2021-12-28 12:03:35 -06:00
smilerz
f5e4adba8b fix get_facets_API 2021-12-28 12:03:35 -06:00
smilerz
b0705da1fe filter fields available to inherit in space settings 2021-12-28 12:03:35 -06:00
smilerz
a20a877dc7 fix after rebase 2021-12-28 12:03:35 -06:00
smilerz
ed50a27669 fix rounding on new shopping list 2021-12-28 12:03:35 -06:00
smilerz
b3f4f2c895 Update settings.py 2021-12-28 12:03:35 -06:00
smilerz
682f4a4297 fix post_save signal for sqlite 2021-12-28 12:03:35 -06:00
smilerz
e33ca876a6 delete yarn lock 2021-12-28 12:03:34 -06:00
smilerz
453b1eb5b9 rebase and fixes 2021-12-28 12:03:34 -06:00
smilerz
ee4ab41c1c test shoppingFood API 2021-12-28 12:03:34 -06:00
smilerz
1364f75f21 test userpreference food_inherit defaults 2021-12-28 12:03:34 -06:00
smilerz
3047c09e55 test rest food inheritance 2021-12-28 12:03:34 -06:00
smilerz
5bdcbb1d17 pytest shopping user preferences 2021-12-28 12:03:34 -06:00
smilerz
35e81f6247 update 2021-12-28 12:03:34 -06:00
smilerz
a51eb7a2cb pytest edit shopping list recipes 2021-12-28 12:03:34 -06:00
smilerz
262387da3e pytest shopping list from recipe methods 2021-12-28 12:03:34 -06:00
smilerz
ab968f225b test recent shopping list 2021-12-28 12:03:34 -06:00
smilerz
0e6685882c test shopping list sharing 2021-12-28 12:03:34 -06:00
smilerz
8f0c5e21ad basic tests with new factories 2021-12-28 12:03:34 -06:00
smilerz
b5bf0a4584 fixed userpref serializer 2021-12-28 12:03:34 -06:00
smilerz
c7ad9c8d15 WIP 2021-12-28 12:03:34 -06:00
smilerz
729aa51901 fix package.json 2021-12-28 12:03:34 -06:00
smilerz
2763eed5b2 minor cleanup 2021-12-28 12:03:34 -06:00
smilerz
2af7b64d4f visual indicator meal plan in shopping 2021-12-28 12:03:34 -06:00
smilerz
24b0643765 copy shopping as markdown 2021-12-28 12:03:34 -06:00
smilerz
df54b10610 download as CSV 2021-12-28 12:03:33 -06:00
smilerz
7ad088d953 fix after rebase 2021-12-28 12:03:33 -06:00
smilerz
fdd86b0c2d download shopping list PDF 2021-12-28 12:03:33 -06:00
smilerz
8dcdf00dc7 refresh shopping list when item is delayed 2021-12-28 12:03:33 -06:00
smilerz
0693d31550 WIP 2021-12-28 12:03:33 -06:00
smilerz
cae3773d5a Fix after rebase 2021-12-28 12:03:33 -06:00
smilerz
f2222fd7d5 pre-merge 2021-12-28 12:03:33 -06:00
smilerz
b8dfc00106 undo move 2021-12-28 12:03:33 -06:00
smilerz
1d224d8658 yarn build 2021-12-28 12:03:33 -06:00
smilerz
2b41fbc9f8 Fix after rebase 2021-12-28 12:03:33 -06:00
smilerz
a24f09c419 WIP 2021-12-28 12:03:33 -06:00
smilerz
450de740b6 RecipeFactory and all related models 2021-12-28 12:03:33 -06:00
smilerz
b92c027919 food inherit tests 2021-12-28 12:03:33 -06:00
smilerz
6c0e979909 finish refactoring test_api_food to use factoryboy 2021-12-28 12:03:33 -06:00
smilerz
a035e02288 refactor Food tests to use factory_boy fixture factories 2021-12-28 12:03:33 -06:00
smilerz
6eec3d18fe more fixes after rebase 2021-12-28 12:03:33 -06:00
smilerz
94b2e9b01c minor updates 2021-12-28 12:03:33 -06:00
smilerz
de7d2e27d9 update migrations 2021-12-28 12:03:32 -06:00
smilerz
dcfe4de61f Fix after rebase 2021-12-28 12:03:32 -06:00
smilerz
f245aa8b4f add to shopping from card context menu 2021-12-28 12:03:32 -06:00
smilerz
a217db5822 add new unit/food from shopping list 2021-12-28 12:03:32 -06:00
smilerz
6e9d609fe0 edit supermarket categories 2021-12-28 12:03:32 -06:00
smilerz
ecac3f3c2d related recipes included when adding mealplan to shopping list 2021-12-28 12:03:32 -06:00
smilerz
6135a6f26d fix apis 2021-12-28 12:03:32 -06:00
smilerz
7a0b395107 alpha shopping list 2021-12-28 12:03:32 -06:00
smilerz
1f41fa04a3 autosync 2021-12-28 12:03:32 -06:00
smilerz
7c598720d0 WIP 2021-12-28 12:03:32 -06:00
smilerz
5c9f5e0e1a fade-enter-active 2021-12-28 12:03:32 -06:00
smilerz
f400c7cd7c shopping line item 2021-12-28 12:03:32 -06:00
smilerz
2a138a852f inheritance works with object moves 2021-12-28 12:03:32 -06:00
smilerz
fbe748db62 food inherit attributes 2021-12-28 12:03:32 -06:00
smilerz
4377505b14 Fix after rebase 2021-12-28 12:03:31 -06:00
smilerz
c5c76cadea getUserPreference available for all UserPreferences 2021-12-28 12:03:31 -06:00
smilerz
fbd17b48fe Fix after rebase 2021-12-28 12:03:31 -06:00
smilerz
6eea7ac99b model changes and GenericAutoSchema 2021-12-28 12:03:31 -06:00
smilerz
f5f9380344 model migrations 2021-12-28 12:03:31 -06:00
smilerz
e243e089cc WIP 2021-12-28 12:03:31 -06:00
smilerz
0b1d8bbd5f WIP 2021-12-28 12:03:31 -06:00
smilerz
10a33add75 Fix after rebase 2021-12-28 12:03:31 -06:00
vabene1111
f16e457d14 Merge pull request #1160 from TandoorRecipes/logout_redirect_patch
fix logout redirect
2021-12-26 13:50:06 +01:00
smilerz
64f2787943 fix logout redirect 2021-12-23 14:48:30 -06:00
vabene1111
3ff15b6766 added copy me that importer 2021-12-23 15:54:48 +01:00
smilerz
d67c5fcf1b change default status of shopping in recipe view 2021-12-23 08:38:43 -06:00
vabene1111
b8e0a7cf69 Merge pull request #1157 from TandoorRecipes/brand_button_patch
fixes 1123
2021-12-23 15:00:33 +01:00
vabene1111
e2915dde55 Merge pull request #1156 from TandoorRecipes/1129_patch
fixes 1129
2021-12-23 14:59:45 +01:00
smilerz
05f2fdecb3 fixes 1123 2021-12-23 07:44:11 -06:00
smilerz
5d33d82d70 fixes 1129 2021-12-23 07:38:30 -06:00
smilerz
17efc388ca fix 1129 2021-12-23 07:37:26 -06:00
vabene1111
e3a3220f00 Merge pull request #1150 from auanasgheps/patch-1
punctuation fixes
2021-12-23 12:50:17 +01:00
vabene1111
f15f34887a Merge pull request #1149 from smilerz/search_pagination
Search pagination
2021-12-23 12:49:15 +01:00
smilerz
20984d3dd6 add path to generic cards for food/keywords 2021-12-22 16:18:45 -06:00
smilerz
67e4c88be7 implement related recipes on home page 2021-12-22 15:23:16 -06:00
smilerz
2d01a2af47 implemented quick switch 2021-12-22 14:43:00 -06:00
smilerz
5272cf0a5c reset new list item after create 2021-12-22 10:19:15 -06:00
smilerz
6b848e27a8 pytest create recipe with ingredient as header 2021-12-22 08:31:32 -06:00
smilerz
efec416604 fix responsive display of detail shopping list 2021-12-21 20:41:49 -06:00
smilerz
e5a4f6b5bf fix responsive display of shopping list 2021-12-21 18:38:16 -06:00
smilerz
a55f975068 update console messages 2021-12-21 14:05:46 -06:00
smilerz
421ade7ad0 WIP 2021-12-21 13:53:40 -06:00
smilerz
c785b590a1 fix filter_to_supermarket setting 2021-12-21 13:53:16 -06:00
smilerz
42132568c4 fix categories in supermarket edit modal 2021-12-21 13:40:50 -06:00
smilerz
dfe414985b fix add to shopping from MealPlan Modal 2021-12-21 11:35:35 -06:00
smilerz
ee52092e24 delete supermarkets and categories from shopping list view 2021-12-21 11:05:24 -06:00
smilerz
75b45ba8eb Squashed commit of the following:
commit 7837467c30
Merge: aaaae5b1b 84759383f
Author: vabene1111 <vabene1111@users.noreply.github.com>
Date:   Sat Dec 18 23:14:24 2021 +0100

    Merge pull request #1146 from auanasgheps/patch-1

    Add documentation about swag by linuxserver

commit 84759383fa
Author: Oliver Cervera <cervera93-19@yahoo.it>
Date:   Sat Dec 18 13:49:09 2021 +0100

    Add documentation about swag by linuxserver

    Documents behaviour in #959

commit aaaae5b1ba
Merge: 4a747f5cd ea62c10d9
Author: vabene1111 <vabene1111@users.noreply.github.com>
Date:   Thu Dec 16 18:10:48 2021 +0100

    Merge pull request #1143 from smilerz/fix_get_facet_api

    fix bug in get_facet_api

commit ea62c10d9a
Author: smilerz <smilerz@gmail.com>
Date:   Thu Dec 16 09:20:56 2021 -0600

    remove console message

commit 3516505dd1
Author: smilerz <smilerz@gmail.com>
Date:   Thu Dec 16 09:08:32 2021 -0600

    fix bug in get_facet_api
2021-12-20 15:26:31 -06:00
smilerz
bf9e59d64c fix Shopping Modal to filter onhand from initial list 2021-12-20 14:59:56 -06:00
smilerz
132c48a490 fix shopping list api 2021-12-20 12:27:25 -06:00
Oliver Cervera
e470a70321 punctuation fixes
First commit was done in rush. Fixed a couple of punctuations.
2021-12-20 11:09:05 +01:00
smilerz
1a99a2d6f1 remove console.log 2021-12-19 11:31:16 -06:00
smilerz
cf3ddfc610 fix inconsistent pagination 2021-12-19 11:08:30 -06:00
smilerz
ecbd3edb97 WIP 2021-12-19 10:21:46 -06:00
vabene1111
7837467c30 Merge pull request #1146 from auanasgheps/patch-1
Add documentation about swag by linuxserver
2021-12-18 23:14:24 +01:00
Oliver Cervera
84759383fa Add documentation about swag by linuxserver
Documents behaviour in #959
2021-12-18 13:49:09 +01:00
vabene1111
aaaae5b1ba Merge pull request #1143 from smilerz/fix_get_facet_api
fix bug in get_facet_api
2021-12-16 18:10:48 +01:00
smilerz
ea62c10d9a remove console message 2021-12-16 09:20:56 -06:00
smilerz
3516505dd1 fix bug in get_facet_api 2021-12-16 09:08:32 -06:00
smilerz
d4553c05c2 fix get_facets_API 2021-12-16 08:13:10 -06:00
smilerz
edc670e87d filter fields available to inherit in space settings 2021-12-15 16:59:33 -06:00
smilerz
a313039b65 fix after rebase 2021-12-15 15:48:55 -06:00
smilerz
963dad39e8 fix rounding on new shopping list 2021-12-15 14:42:12 -06:00
smilerz
8f19ab6e5e Update settings.py 2021-12-15 13:29:08 -06:00
smilerz
0e20f679b3 fix post_save signal for sqlite 2021-12-15 13:23:22 -06:00
smilerz
46b83c8205 delete yarn lock 2021-12-15 12:37:40 -06:00
smilerz
8b28a47297 rebase and fixes 2021-12-15 12:37:40 -06:00
smilerz
e7e3a3083d test shoppingFood API 2021-12-15 12:37:40 -06:00
smilerz
ea7d34c8d2 test userpreference food_inherit defaults 2021-12-15 12:37:40 -06:00
smilerz
7e081d4389 test rest food inheritance 2021-12-15 12:37:40 -06:00
smilerz
2edb455bd6 pytest shopping user preferences 2021-12-15 12:37:40 -06:00
smilerz
c32a96fd6f update 2021-12-15 12:37:40 -06:00
smilerz
6d1476b2d8 pytest edit shopping list recipes 2021-12-15 12:37:40 -06:00
smilerz
5d79e4d3be pytest shopping list from recipe methods 2021-12-15 12:37:40 -06:00
smilerz
0866d21fa5 test recent shopping list 2021-12-15 12:37:40 -06:00
smilerz
6448c062f9 test shopping list sharing 2021-12-15 12:37:40 -06:00
smilerz
b146e75daa basic tests with new factories 2021-12-15 12:37:40 -06:00
smilerz
68927d141e fixed userpref serializer 2021-12-15 12:37:40 -06:00
smilerz
1e36e6cd5b WIP 2021-12-15 12:37:40 -06:00
smilerz
4877d69947 fix package.json 2021-12-15 12:37:23 -06:00
smilerz
f2f187a844 minor cleanup 2021-12-15 12:37:04 -06:00
smilerz
c2e84c1fa4 visual indicator meal plan in shopping 2021-12-15 12:37:04 -06:00
smilerz
ca93920f04 copy shopping as markdown 2021-12-15 12:37:04 -06:00
smilerz
903a721a1d download as CSV 2021-12-15 12:37:04 -06:00
smilerz
44e513ff2d fix after rebase 2021-12-15 12:37:03 -06:00
smilerz
2d7d160d1b download shopping list PDF 2021-12-15 12:37:03 -06:00
smilerz
54ca8b2bd0 refresh shopping list when item is delayed 2021-12-15 12:37:03 -06:00
smilerz
a972a757b2 WIP 2021-12-15 12:37:03 -06:00
smilerz
7c0d1236c2 Fix after rebase 2021-12-15 12:37:03 -06:00
smilerz
09b0dcb136 pre-merge 2021-12-15 12:37:03 -06:00
smilerz
5b4867d172 undo move 2021-12-15 12:37:03 -06:00
smilerz
d3d4c210c1 yarn build 2021-12-15 12:37:03 -06:00
smilerz
6cffee57fe Fix after rebase 2021-12-15 12:36:33 -06:00
smilerz
286595e03d WIP 2021-12-15 12:36:33 -06:00
smilerz
0d1c55d2e4 RecipeFactory and all related models 2021-12-15 12:36:33 -06:00
smilerz
fd8ca2e9ac food inherit tests 2021-12-15 12:36:33 -06:00
smilerz
9ef4c88d02 finish refactoring test_api_food to use factoryboy 2021-12-15 12:36:33 -06:00
smilerz
08d3c40200 refactor Food tests to use factory_boy fixture factories 2021-12-15 12:36:33 -06:00
smilerz
e229a70360 more fixes after rebase 2021-12-15 12:36:33 -06:00
smilerz
06b7ba809b minor updates 2021-12-15 12:36:06 -06:00
smilerz
099a5420d6 update migrations 2021-12-15 12:36:06 -06:00
smilerz
5a9543b4d8 Fix after rebase 2021-12-15 12:36:06 -06:00
smilerz
60d7e63da8 add to shopping from card context menu 2021-12-15 12:36:06 -06:00
smilerz
867e2d4fbf add new unit/food from shopping list 2021-12-15 12:36:06 -06:00
smilerz
757fa5e49c edit supermarket categories 2021-12-15 12:36:06 -06:00
smilerz
8b682c33f3 related recipes included when adding mealplan to shopping list 2021-12-15 12:35:48 -06:00
smilerz
27f358dd03 fix apis 2021-12-15 12:35:48 -06:00
smilerz
7c6a7ef6a4 alpha shopping list 2021-12-15 12:35:48 -06:00
smilerz
4c506750de autosync 2021-12-15 12:35:48 -06:00
smilerz
b84d77be15 WIP 2021-12-15 12:35:48 -06:00
smilerz
247dd30b20 fade-enter-active 2021-12-15 12:35:48 -06:00
smilerz
5e4e203dfb shopping line item 2021-12-15 12:35:48 -06:00
smilerz
79b6d4817e inheritance works with object moves 2021-12-15 12:35:48 -06:00
smilerz
6075ce50e7 food inherit attributes 2021-12-15 12:35:48 -06:00
smilerz
2ca7722afb Fix after rebase 2021-12-15 12:35:48 -06:00
smilerz
7a9e5b1e3f getUserPreference available for all UserPreferences 2021-12-15 12:35:48 -06:00
smilerz
7f87a9efed Fix after rebase 2021-12-15 12:35:48 -06:00
smilerz
3d674cfca6 model changes and GenericAutoSchema 2021-12-15 12:35:48 -06:00
smilerz
1642224205 model migrations 2021-12-15 12:35:48 -06:00
smilerz
3d359f844f WIP 2021-12-15 12:35:48 -06:00
smilerz
94c69271d3 WIP 2021-12-15 12:35:48 -06:00
smilerz
9827c3ffd5 Fix after rebase 2021-12-15 12:35:48 -06:00
vabene1111
4a747f5cd4 Revert "Revert "fixed vue build""
This reverts commit edde015b71.
2021-12-15 18:02:37 +01:00
vabene1111
0623a8ebc7 clear package cache in build 2021-12-15 17:59:22 +01:00
vabene1111
5941022b5e fixed markdown table extension 2021-12-15 17:46:01 +01:00
vabene1111
2559905a78 fixed empty fields breaking recipe update 2021-12-15 17:45:53 +01:00
vabene1111
edde015b71 Revert "fixed vue build"
This reverts commit 7e07508a31.
2021-12-15 17:26:06 +01:00
vabene1111
9b7b8beea4 addeed yarn lock 2021-12-15 17:22:24 +01:00
vabene1111
2eae8e5eeb Merge pull request #1141 from TandoorRecipes/dependabot/pip/django-3.2.10
Bump django from 3.2.9 to 3.2.10
2021-12-14 16:35:09 +01:00
dependabot[bot]
6d8bc396f8 Bump django from 3.2.9 to 3.2.10
Bumps [django](https://github.com/django/django) from 3.2.9 to 3.2.10.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.9...3.2.10)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-14 15:33:48 +00:00
vabene1111
4118c8d9e3 Merge pull request #1138 from TandoorRecipes/dependabot/pip/lxml-4.6.5
Bump lxml from 4.6.4 to 4.6.5
2021-12-14 16:27:23 +01:00
dependabot[bot]
78c2eacbd8 Bump lxml from 4.6.4 to 4.6.5
Bumps [lxml](https://github.com/lxml/lxml) from 4.6.4 to 4.6.5.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.4...lxml-4.6.5)

---
updated-dependencies:
- dependency-name: lxml
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-13 20:26:37 +00:00
vabene1111
01510f39e5 Merge pull request #1119 from TandoorRecipes/user_prefs_fix
fix package.json
2021-12-08 14:59:32 +01:00
smilerz
09cc5aafe9 fix package.json 2021-12-07 15:38:03 -06:00
vabene1111
e8b2f57812 Merge pull request #1111 from TandoorRecipes/dependabot/pip/simplejson-3.17.6
Bump simplejson from 3.17.5 to 3.17.6
2021-12-03 17:40:07 +01:00
vabene1111
664e83143f Merge pull request #1112 from TandoorRecipes/dependabot/pip/pytest-django-4.5.1
Bump pytest-django from 4.4.0 to 4.5.1
2021-12-03 17:39:56 +01:00
dependabot[bot]
f1309cc624 Bump simplejson from 3.17.5 to 3.17.6
Bumps [simplejson](https://github.com/simplejson/simplejson) from 3.17.5 to 3.17.6.
- [Release notes](https://github.com/simplejson/simplejson/releases)
- [Changelog](https://github.com/simplejson/simplejson/blob/master/CHANGES.txt)
- [Commits](https://github.com/simplejson/simplejson/compare/v3.17.5...v3.17.6)

---
updated-dependencies:
- dependency-name: simplejson
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-03 16:39:51 +00:00
vabene1111
6fb7f6bd1f Merge pull request #1109 from TandoorRecipes/dependabot/pip/boto3-1.20.19
Bump boto3 from 1.19.7 to 1.20.19
2021-12-03 17:39:44 +01:00
dependabot[bot]
158bb1bf03 Bump pytest-django from 4.4.0 to 4.5.1
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.4.0 to 4.5.1.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.4.0...v4.5.1)

---
updated-dependencies:
- dependency-name: pytest-django
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-03 16:39:32 +00:00
vabene1111
086e802873 Merge pull request #1106 from TandoorRecipes/dependabot/pip/python-dotenv-0.19.2
Bump python-dotenv from 0.19.1 to 0.19.2
2021-12-03 17:39:25 +01:00
dependabot[bot]
c94c8d3559 Bump python-dotenv from 0.19.1 to 0.19.2
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 0.19.1 to 0.19.2.
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v0.19.1...v0.19.2)

---
updated-dependencies:
- dependency-name: python-dotenv
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-03 16:39:15 +00:00
vabene1111
f99010aa1d Merge pull request #1107 from TandoorRecipes/dependabot/pip/django-allauth-0.46.0
Bump django-allauth from 0.45.0 to 0.46.0
2021-12-03 17:39:13 +01:00
vabene1111
32e00999f3 Merge pull request #1100 from TandoorRecipes/dependabot/pip/lxml-4.6.4
Bump lxml from 4.6.3 to 4.6.4
2021-12-03 17:39:03 +01:00
vabene1111
e3196a79a8 Merge pull request #1102 from TandoorRecipes/dependabot/npm_and_yarn/vue/typescript-4.5.2
Bump typescript from 4.4.4 to 4.5.2 in /vue
2021-12-03 17:38:53 +01:00
dependabot[bot]
e926b34bec Bump boto3 from 1.19.7 to 1.20.19
Bumps [boto3](https://github.com/boto/boto3) from 1.19.7 to 1.20.19.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.19.7...1.20.19)

---
updated-dependencies:
- dependency-name: boto3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-03 16:38:47 +00:00
vabene1111
a460123184 Merge pull request #1110 from TandoorRecipes/dependabot/pip/psycopg2-binary-2.9.2
Bump psycopg2-binary from 2.9.1 to 2.9.2
2021-12-03 17:38:37 +01:00
vabene1111
c89c88b981 Merge pull request #1101 from TandoorRecipes/dependabot/npm_and_yarn/vue/eslint-8.3.0
Bump eslint from 7.32.0 to 8.3.0 in /vue
2021-12-03 17:38:27 +01:00
dependabot[bot]
cf6ea04f30 Bump typescript from 4.4.4 to 4.5.2 in /vue
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.4.4 to 4.5.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.4.4...v4.5.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-03 16:38:10 +00:00
dependabot[bot]
15c4609db3 Bump psycopg2-binary from 2.9.1 to 2.9.2
Bumps [psycopg2-binary](https://github.com/psycopg/psycopg2) from 2.9.1 to 2.9.2.
- [Release notes](https://github.com/psycopg/psycopg2/releases)
- [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS)
- [Commits](https://github.com/psycopg/psycopg2/commits)

---
updated-dependencies:
- dependency-name: psycopg2-binary
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-03 16:38:03 +00:00
vabene1111
053804f8cb Merge pull request #1108 from TandoorRecipes/dependabot/pip/cryptography-36.0.0
Bump cryptography from 35.0.0 to 36.0.0
2021-12-03 17:37:58 +01:00
dependabot[bot]
da748995e7 Bump eslint from 7.32.0 to 8.3.0 in /vue
Bumps [eslint](https://github.com/eslint/eslint) from 7.32.0 to 8.3.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.32.0...v8.3.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-03 16:37:54 +00:00
vabene1111
1d80ba3a3b Merge pull request #1103 from TandoorRecipes/dependabot/npm_and_yarn/vue/axios-0.24.0
Bump axios from 0.21.4 to 0.24.0 in /vue
2021-12-03 17:37:48 +01:00
vabene1111
29fe6c7363 Merge pull request #1099 from TandoorRecipes/dependabot/npm_and_yarn/vue/vue-simple-calendar-6.0.3
Bump vue-simple-calendar from 5.0.1 to 6.0.3 in /vue
2021-12-03 17:37:38 +01:00
dependabot[bot]
42d4a32ffc Bump lxml from 4.6.3 to 4.6.4
Bumps [lxml](https://github.com/lxml/lxml) from 4.6.3 to 4.6.4.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.3...lxml-4.6.4)

---
updated-dependencies:
- dependency-name: lxml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-03 16:37:13 +00:00
dependabot[bot]
e8ae844fb0 Bump django-allauth from 0.45.0 to 0.46.0
Bumps [django-allauth](https://github.com/pennersr/django-allauth) from 0.45.0 to 0.46.0.
- [Release notes](https://github.com/pennersr/django-allauth/releases)
- [Changelog](https://github.com/pennersr/django-allauth/blob/master/ChangeLog.rst)
- [Commits](https://github.com/pennersr/django-allauth/compare/0.45.0...0.46.0)

---
updated-dependencies:
- dependency-name: django-allauth
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-03 16:37:12 +00:00
vabene1111
c93f68804a Merge pull request #1098 from TandoorRecipes/dependabot/pip/markdown-3.3.6
Bump markdown from 3.3.4 to 3.3.6
2021-12-03 17:36:45 +01:00
dependabot[bot]
b4ea236241 Bump cryptography from 35.0.0 to 36.0.0
Bumps [cryptography](https://github.com/pyca/cryptography) from 35.0.0 to 36.0.0.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/35.0.0...36.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-03 16:36:38 +00:00
vabene1111
2bef5c3b51 Merge pull request #1096 from TandoorRecipes/dependabot/npm_and_yarn/vue/vue/eslint-config-typescript-9.1.0
Bump @vue/eslint-config-typescript from 7.0.0 to 9.1.0 in /vue
2021-12-03 17:36:36 +01:00
vabene1111
52f2086616 Merge pull request #1097 from TandoorRecipes/dependabot/pip/boto3-1.20.16
Bump boto3 from 1.19.7 to 1.20.16
2021-12-03 17:36:27 +01:00
vabene1111
03e1474113 Merge pull request #1095 from TandoorRecipes/dependabot/pip/jinja2-3.0.3
Bump jinja2 from 3.0.2 to 3.0.3
2021-12-03 17:36:19 +01:00
vabene1111
9829ab68a6 Merge pull request #1094 from TandoorRecipes/dependabot/pip/recipe-scrapers-13.7.0
Bump recipe-scrapers from 13.5.0 to 13.7.0
2021-12-03 17:36:07 +01:00
vabene1111
7e07508a31 fixed vue build 2021-12-03 11:47:04 +01:00
vabene1111
94b0438516 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2021-12-03 11:41:22 +01:00
vabene1111
b97c90e22f fixed user pref serializer not having access to context 2021-12-03 11:41:00 +01:00
Kaibu
f78264620f Merge pull request #1104 from Nailik/patch-1
Update synology.me
2021-12-02 23:23:23 +01:00
Nailik
571a618818 Update synology.me
Added information to fix problems where container could not reach each other because firewall blocked it.
Added information how to setup ssl via reverse proxy.
2021-12-01 12:39:13 +01:00
dependabot[bot]
6c97594591 Bump axios from 0.21.4 to 0.24.0 in /vue
Bumps [axios](https://github.com/axios/axios) from 0.21.4 to 0.24.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.4...v0.24.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 00:08:38 +00:00
dependabot[bot]
5d353a0839 Bump vue-simple-calendar from 5.0.1 to 6.0.3 in /vue
Bumps [vue-simple-calendar](https://github.com/richardtallent/vue-simple-calendar) from 5.0.1 to 6.0.3.
- [Release notes](https://github.com/richardtallent/vue-simple-calendar/releases)
- [Changelog](https://github.com/richardtallent/vue-simple-calendar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/richardtallent/vue-simple-calendar/commits/v6.0.3)

---
updated-dependencies:
- dependency-name: vue-simple-calendar
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 00:07:33 +00:00
dependabot[bot]
0be1f6a170 Bump markdown from 3.3.4 to 3.3.6
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.3.4 to 3.3.6.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.3.4...3.3.6)

---
updated-dependencies:
- dependency-name: markdown
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 00:07:29 +00:00
dependabot[bot]
5cd042fa7c Bump boto3 from 1.19.7 to 1.20.16
Bumps [boto3](https://github.com/boto/boto3) from 1.19.7 to 1.20.16.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.19.7...1.20.16)

---
updated-dependencies:
- dependency-name: boto3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 00:07:25 +00:00
dependabot[bot]
e02d2530aa Bump @vue/eslint-config-typescript from 7.0.0 to 9.1.0 in /vue
Bumps [@vue/eslint-config-typescript](https://github.com/vuejs/eslint-config-typescript) from 7.0.0 to 9.1.0.
- [Release notes](https://github.com/vuejs/eslint-config-typescript/releases)
- [Commits](https://github.com/vuejs/eslint-config-typescript/compare/v7.0.0...v9.1.0)

---
updated-dependencies:
- dependency-name: "@vue/eslint-config-typescript"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 00:07:24 +00:00
dependabot[bot]
b35f5047ab Bump jinja2 from 3.0.2 to 3.0.3
Bumps [jinja2](https://github.com/pallets/jinja) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 00:07:19 +00:00
dependabot[bot]
f10bec8ab4 Bump recipe-scrapers from 13.5.0 to 13.7.0
Bumps [recipe-scrapers](https://github.com/hhursev/recipe-scrapers) from 13.5.0 to 13.7.0.
- [Release notes](https://github.com/hhursev/recipe-scrapers/releases)
- [Commits](https://github.com/hhursev/recipe-scrapers/compare/13.5.0...13.7.0)

---
updated-dependencies:
- dependency-name: recipe-scrapers
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-01 00:07:11 +00:00
vabene1111
3bc1daa72e fixed import 2021-11-30 18:48:07 +01:00
vabene1111
5d6574b8cc Merge pull request #1068 from rose-a/fix-recipe-step-template
Render separate ingredients flex row for each step
2021-11-30 17:23:47 +01:00
vabene1111
adc65baf9c Merge pull request #1086 from smilerz/generic_modal_v2
generic modal refactored
2021-11-30 17:23:33 +01:00
vabene1111
4d2e7eadb6 Merge branch 'develop' into generic_modal_v2 2021-11-30 17:23:27 +01:00
vabene1111
7c985cec23 Merge pull request #1088 from smilerz/search_troubleshooting
add search debug
2021-11-30 17:21:40 +01:00
vabene1111
2cd33ee40a fixed several tests 2021-11-30 17:20:36 +01:00
vabene1111
f61146123e fixed ci python install 2021-11-30 16:26:11 +01:00
vabene1111
4806bd63b6 updated ci to run under python 3.10 2021-11-30 16:23:04 +01:00
vabene1111
41242c8d09 updated microdata dependency 2021-11-30 16:16:40 +01:00
vabene1111
57a967b91d fixed view log user filter 2021-11-30 16:07:31 +01:00
vabene1111
fb931f4715 Merge pull request #1092 from TandoorRecipes/dependabot/pip/python-ldap-3.4.0
Bump python-ldap from 3.3.1 to 3.4.0
2021-11-30 09:05:37 +01:00
dependabot[bot]
e86b476b3a Bump python-ldap from 3.3.1 to 3.4.0
Bumps [python-ldap](https://github.com/python-ldap/python-ldap) from 3.3.1 to 3.4.0.
- [Release notes](https://github.com/python-ldap/python-ldap/releases)
- [Commits](https://github.com/python-ldap/python-ldap/compare/python-ldap-3.3.1...python-ldap-3.4.0)

---
updated-dependencies:
- dependency-name: python-ldap
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-29 18:00:46 +00:00
vabene1111
7f22e0a275 fixed duplicate results on cook book view api 2021-11-27 16:03:08 +01:00
Kaibu
1907223a8a reduced api calls 2021-11-25 12:51:55 +01:00
Kaibu
9b5fe8f4e7 prevent page jump due to href=# 2021-11-25 01:35:12 +01:00
Kaibu
d76fdd090a meal plan hotkeys and sharing 2021-11-25 01:28:07 +01:00
smilerz
55a0304700 add search debug 2021-11-24 12:10:15 -06:00
smilerz
5b6dd62f8e generic modal refactored 2021-11-23 19:18:10 -06:00
vabene1111
19f5684d26 fixed linebreak in release workflow 2021-11-23 21:38:52 +01:00
vabene1111
d6ad1354db Merge branch 'master' into develop 2021-11-23 18:18:10 +01:00
Alexander Rose
4626af3505 render separate flex row for step ingredients 2021-11-13 14:12:10 +01:00
214 changed files with 38313 additions and 14569 deletions

View File

@@ -7,7 +7,9 @@ SQL_DEBUG=0
ALLOWED_HOSTS=*
# random secret key, use for example `base64 /dev/urandom | head -c50` to generate one
# ---------------------------- REQUIRED -------------------------
SECRET_KEY=
# ---------------------------------------------------------------
# your default timezone See https://timezonedb.com/time-zones for a list of timezones
TIMEZONE=Europe/Berlin
@@ -18,7 +20,9 @@ DB_ENGINE=django.db.backends.postgresql
POSTGRES_HOST=db_recipes
POSTGRES_PORT=5432
POSTGRES_USER=djangouser
# ---------------------------- REQUIRED -------------------------
POSTGRES_PASSWORD=
# ---------------------------------------------------------------
POSTGRES_DB=djangodb
# database connection string, when used overrides other database settings.
@@ -41,7 +45,8 @@ SHOPPING_MIN_AUTOSYNC_INTERVAL=5
# Default for user setting sticky navbar
# STICKY_NAV_PREF_DEFAULT=1
# If base URL is something other than just / (you are serving a subfolder in your proxy for instance http://recipe_app/recipes/)
# If base URL is something other than just / (you are serving a subfolder in your proxy for instance http://recipe_app/recipes/)
# Be sure to not have a trailing slash: e.g. '/recipes' instead of '/recipes/'
# SCRIPT_NAME=/recipes
# If staticfiles are stored at a different location uncomment and change accordingly, MUST END IN /
@@ -141,3 +146,7 @@ REVERSE_PROXY_AUTH=0
#AUTH_LDAP_BIND_DN=
#AUTH_LDAP_BIND_PASSWORD=
#AUTH_LDAP_USER_SEARCH_BASE_DN=
# Enables exporting PDF (see export docs)
# Disabled by default, uncomment to enable
# ENABLE_PDF_EXPORT=1

View File

@@ -1,15 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
### Version
Please provide your current version (can be found on the system page since v0.8.4)
Version:
### Bug description
A clear and concise description of what the bug is.

View File

@@ -0,0 +1,81 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## Version
<!-- Please provide your current version (can be found on the system page since v0.8.4). -->
**Tandoor-Version:**
## Setup configuration
<!--Please tick all boxes which apply to your configuration. Feel free to provide additional information below.
To tick boxes here, simply put an X inside the brackets below -->
### Setup
- [ ] Docker / Docker-Compose
- [ ] Unraid
- [ ] Synology
- [ ] Kubernetes
- [ ] Manual setup
- [ ] Others (please state below)
### Reverse Proxy
- [ ] No reverse proxy
- [ ] jwilder's nginx proxy
- [ ] Nginx proxy manager (NPM)
- [ ] SWAG
- [ ] Caddy
- [ ] Traefik
- [ ] Others (please state below)
<!-- Please provide additional information if possible -->
**Additional information:**
## Bug description
A clear and concise description of what the bug is.
## Logs
<!-- *(Remove this section entirely if no logs are available or necessary for your issue)*
To get the most information about your issue, set DEBUG=1 (e.g. in your `.env` file if using docker-compose) and try to reproduce the issue afterwards.
Please put your logs into the expandable section below and use code quotation for all logs! Usage: Put three backticks in front and after the log, like this:
` ``` <Many lines of log messages ``` `
Feel free to remove parts if you don't fill them out.
-->
<details>
<summary>Web-Container-Logs</summary>
<!-- *Put your logs inside here (leave the code quotations in takt):* -->
```
Replace me with logs
```
</details>
<details>
<summary>DB-Container-Logs</summary>
<!-- *Put your logs inside here (leave the code quotations in takt):* -->
```
Replace me with logs
```
</details>
<details>
<summary>Nginx-Container-Logs <!-- if you use one --></summary>
<!-- *Put your logs inside here (leave the code quotations in takt):* -->
```
Replace me with logs
```
</details>

64
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Bug Report
description: "Create a report to help us improve"
#title: ""
#labels: ["Bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: version
attributes:
label: Tandoor Version
description: "What version of Tandoor are you using? (can be found on the system page since v0.8.4)"
validations:
required: true
- type: dropdown
id: setup
attributes:
label: Setup
description: "How is your Tandoor instance set up?"
options:
- Docker / Docker-Compose
- Unraid
- Synology
- Kubernetes
- Manual Setup
- Others (please state below)
validations:
required: true
- type: dropdown
id: reverse-proxy
attributes:
label: "Reverse Proxy"
description: "What reverse proxy do you use with Tandoor?"
options:
- No reverse proxy
- jwilder's nginx proxy
- Nginx Proxy Manager (NPM)
- SWAG
- Caddy
- Traefik
- Apache2
- Others (please state below)
validations:
required: true
- type: input
id: other
attributes:
label: Other
description: "In case you chose 'Others' above, please provide more info here."
- type: textarea
id: bug-descr
attributes:
label: Bug description
description: "Please accurately describe the bug you encountered."
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant logs
description: Please copy and paste any relevant logs. This will be automatically formatted into code, so no need for backticks.
render: shell

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: FAQs
url: https://docs.tandoor.dev/faq/
about: Please take a look at the FAQs before creating a bug ticket.

40
.github/ISSUE_TEMPLATE/doc_issue.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Documentation Issue
description: "Create a report to help us improve"
#title: ""
labels: ["documentation"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this documentation issue report!
- type: input
id: docs-link
attributes:
label: Documentation link
description: "Please provide a link to the corresponding documentation site on docs.tandoor.dev"
- type: dropdown
id: section
attributes:
label: Affected section
description: "What part of the documentation is the issue about?"
options:
- Installation
- Features
- System
- FAQ
- Does not exist yet
- Other (please state below)
validations:
required: true
- type: input
id: other
attributes:
label: Other
description: "In case you chose 'Other' above, please provide more info here."
- type: textarea
id: descr
attributes:
label: Issue description
description: "Please accurately describe the documentation issue you are seeing."
validations:
required: true

View File

@@ -0,0 +1,39 @@
name: Feature Request
description: "Suggest an idea for this project"
#title: ""
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request!
- type: textarea
id: problem
attributes:
label: "Is your feature request related to a problem? Please describe."
description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when..."
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like"
description: "A clear and concise description of what you want to happen."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: "Describe alternatives you've considered"
description: "A clear and concise description of any alternative solutions or features you've considered."
- type: textarea
id: additional
attributes:
label: "Additional context"
description: "Add any other context or screenshots about the feature request here."
- type: checkboxes
attributes:
label: "Contribute"
description: "Are you willing and able to help develop this feature?"
options:
- label: "Yes"
- label: "Partly"
- label: "No"

82
.github/ISSUE_TEMPLATE/help_request.yml vendored Normal file
View File

@@ -0,0 +1,82 @@
name: Help request
description: "If there is anything wrong with your setup"
#title: ""
labels: ["setup issue"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this help request!
- type: textarea
id: issue
attributes:
label: Issue
description: "Please describe your problem here."
validations:
required: true
- type: input
id: version
attributes:
label: Tandoor Version
description: "What version of Tandoor are you using? (can be found on the system page since v0.8.4)"
validations:
required: true
- type: input
id: os
attributes:
label: OS Version
description: "E.g. Ubuntu 20.02"
validations:
required: true
- type: dropdown
id: setup
attributes:
label: Setup
description: "How is your Tandoor instance set up?"
options:
- Docker / Docker-Compose
- Unraid
- Synology
- Kubernetes
- Manual Setup
- Others (please state below)
validations:
required: true
- type: dropdown
id: reverse-proxy
attributes:
label: "Reverse Proxy"
description: "What reverse proxy do you use with Tandoor?"
options:
- No reverse proxy
- jwilder's nginx proxy
- Nginx Proxy Manager (NPM)
- SWAG
- Caddy
- Traefik
- Others (please state below)
validations:
required: true
- type: input
id: other
attributes:
label: Other
description: "In case you chose 'Others' above or have more info, please provide additional details here."
- type: textarea
id: env
attributes:
label: Environment file
description: "Please include your `.env` config file (**make sure to remove/replace all secrets**)"
render: shell
- type: textarea
id: docker-compose
attributes:
label: Docker-Compose file
description: "When running with docker compose please provide your `docker-compose.yml`"
render: shell
- type: textarea
id: logs
attributes:
label: Relevant logs
description: "If you feel like there is anything interesting please post the output of `docker-compose logs` at container startup and when the issue happens."
render: shell

View File

@@ -0,0 +1,36 @@
name: Website Import
description: "Anything related to website imports"
#title: ""
#labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this website import form!
- type: input
id: version
attributes:
label: Tandoor Version
description: "What version of Tandoor are you using? (can be found on the system page since v0.8.4)"
validations:
required: true
- type: input
id: url
attributes:
label: Import URL
description: "Exact URL you are trying to import from."
validations:
required: true
- type: textarea
id: bug-descr
attributes:
label: "When did the issue happen?"
description: "When pressing the search button with the url / when importing after the page has loaded / ..."
validations:
required: true
- type: textarea
id: logs
attributes:
label: Response / message shown
description: Please copy and paste any relevant logs or responses / messages which are shown in Tandoor. This will be automatically formatted into code, so no need for backticks.
render: shell

View File

@@ -1,4 +1,4 @@
name: Continous Integration
name: Continuous Integration
on: [push]
@@ -9,14 +9,14 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: [3.9]
python-version: ['3.10']
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.9
- name: Set up Python 3.10
uses: actions/setup-python@v1
with:
python-version: 3.9
python-version: '3.10'
# Build Vue frontend
- uses: actions/setup-node@v2
with:

View File

@@ -24,6 +24,9 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: Clear Cache
working-directory: ./vue
run: yarn cache clean --all
- name: Install dependencies
working-directory: ./vue
run: yarn install

View File

@@ -49,4 +49,4 @@ jobs:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2
with:
args: '🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of tandoor has been released 🥳 \nCheck it out https://github.com/vabene1111/recipes/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}'
args: '🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}'

2
.gitignore vendored
View File

@@ -79,8 +79,8 @@ postgresql/
/docker-compose.override.yml
vue/node_modules
.vscode/
vue/yarn.lock
vetur.config.js
cookbook/static/vue
vue/webpack-stats.json
cookbook/templates/sw.js
.prettierignore

View File

@@ -9,7 +9,7 @@
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
<p align="center">
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=master" ></a>
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continuous%20Integration/badge.svg?branch=master" ></a>
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>

View File

@@ -7,4 +7,4 @@ Since this software is still considered beta/WIP support is always only given fo
## Reporting a Vulnerability
Please open a normal public issue if you have any security related concerns. If you feel like the issue should not be discussed in
public just open a generic issue and we will discuss further communitcation there (since GitHub does not allow everyone to create a security advisory :/).
public just open a generic issue and we will discuss further communication there (since GitHub does not allow everyone to create a security advisory :/).

View File

@@ -1,23 +1,22 @@
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group, User
from django.contrib.postgres.search import SearchVector
from django.utils import translation
from django_scopes import scopes_disabled
from treebeard.admin import TreeAdmin
from treebeard.forms import movenodeform_factory
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User, Group
from django_scopes import scopes_disabled
from django.utils import translation
from .models import (Comment, CookLog, Food, Ingredient, InviteLink, Keyword,
MealPlan, MealType, NutritionInformation, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShareLink,
ShoppingList, ShoppingListEntry, ShoppingListRecipe,
Space, Step, Storage, Sync, SyncLog, Unit, UserPreference,
ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation,
ImportLog, TelegramBot, BookmarkletImport, UserFile, SearchPreference)
from cookbook.managers import DICTIONARY
from .models import (BookmarkletImport, Comment, CookLog, Food, FoodInheritField, ImportLog,
Ingredient, InviteLink, Keyword, MealPlan, MealType, NutritionInformation,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation)
class CustomUserAdmin(UserAdmin):
def has_add_permission(self, request, obj=None):
@@ -30,11 +29,52 @@ admin.site.register(User, CustomUserAdmin)
admin.site.unregister(Group)
@admin.action(description='Delete all data from a space')
def delete_space_action(modeladmin, request, queryset):
for space in queryset:
CookLog.objects.filter(space=space).delete()
ViewLog.objects.filter(space=space).delete()
ImportLog.objects.filter(space=space).delete()
BookmarkletImport.objects.filter(space=space).delete()
Comment.objects.filter(recipe__space=space).delete()
Keyword.objects.filter(space=space).delete()
Ingredient.objects.filter(space=space).delete()
Food.objects.filter(space=space).delete()
Unit.objects.filter(space=space).delete()
Step.objects.filter(space=space).delete()
NutritionInformation.objects.filter(space=space).delete()
RecipeBookEntry.objects.filter(book__space=space).delete()
RecipeBook.objects.filter(space=space).delete()
MealType.objects.filter(space=space).delete()
MealPlan.objects.filter(space=space).delete()
ShareLink.objects.filter(space=space).delete()
Recipe.objects.filter(space=space).delete()
RecipeImport.objects.filter(space=space).delete()
SyncLog.objects.filter(sync__space=space).delete()
Sync.objects.filter(space=space).delete()
Storage.objects.filter(space=space).delete()
ShoppingListEntry.objects.filter(shoppinglist__space=space).delete()
ShoppingListRecipe.objects.filter(shoppinglist__space=space).delete()
ShoppingList.objects.filter(space=space).delete()
SupermarketCategoryRelation.objects.filter(supermarket__space=space).delete()
SupermarketCategory.objects.filter(space=space).delete()
Supermarket.objects.filter(space=space).delete()
InviteLink.objects.filter(space=space).delete()
UserFile.objects.filter(space=space).delete()
Automation.objects.filter(space=space).delete()
class SpaceAdmin(admin.ModelAdmin):
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
search_fields = ('name', 'created_by__username')
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
date_hierarchy = 'created_at'
actions = [delete_space_action]
admin.site.register(Space, SpaceAdmin)
@@ -129,6 +169,7 @@ def sort_tree(modeladmin, request, queryset):
class KeywordAdmin(TreeAdmin):
form = movenodeform_factory(Keyword)
ordering = ('space', 'path',)
search_fields = ('name',)
actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]
@@ -136,8 +177,8 @@ admin.site.register(Keyword, KeywordAdmin)
class StepAdmin(admin.ModelAdmin):
list_display = ('name', 'type', 'order')
search_fields = ('name', 'type')
list_display = ('name', 'order',)
search_fields = ('name',)
admin.site.register(Step, StepAdmin)
@@ -173,9 +214,13 @@ admin.site.register(Recipe, RecipeAdmin)
admin.site.register(Unit)
# admin.site.register(FoodInheritField)
class FoodAdmin(TreeAdmin):
form = movenodeform_factory(Keyword)
ordering = ('space', 'path',)
search_fields = ('name',)
actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]
@@ -257,7 +302,7 @@ admin.site.register(ViewLog, ViewLogAdmin)
class InviteLinkAdmin(admin.ModelAdmin):
list_display = (
'group', 'valid_until',
'group', 'valid_until', 'space',
'created_by', 'created_at', 'used_by'
)
@@ -280,7 +325,7 @@ admin.site.register(ShoppingListRecipe, ShoppingListRecipeAdmin)
class ShoppingListEntryAdmin(admin.ModelAdmin):
list_display = ('id', 'food', 'unit', 'list_recipe', 'checked')
list_display = ('id', 'food', 'unit', 'list_recipe', 'created_by', 'created_at', 'checked')
admin.site.register(ShoppingListEntry, ShoppingListEntryAdmin)

View File

@@ -12,29 +12,26 @@ class CookbookConfig(AppConfig):
name = 'cookbook'
def ready(self):
# post_save signal is only necessary if using full-text search on postgres
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
import cookbook.signals # noqa
import cookbook.signals # noqa
if not settings.DISABLE_TREE_FIX_STARTUP:
# when starting up run fix_tree to:
# a) make sure that nodes are sorted when switching between sort modes
# b) fix problems, if any, with tree consistency
with scopes_disabled():
try:
from cookbook.models import Keyword, Food
Keyword.fix_tree(fix_paths=True)
Food.fix_tree(fix_paths=True)
except OperationalError:
if DEBUG:
traceback.print_exc()
pass # if model does not exist there is no need to fix it
except ProgrammingError:
if DEBUG:
traceback.print_exc()
pass # if migration has not been run database cannot be fixed yet
except Exception:
if DEBUG:
traceback.print_exc()
pass # dont break startup just because fix could not run, need to investigate cases when this happens
# if not settings.DISABLE_TREE_FIX_STARTUP:
# # when starting up run fix_tree to:
# # a) make sure that nodes are sorted when switching between sort modes
# # b) fix problems, if any, with tree consistency
# with scopes_disabled():
# try:
# from cookbook.models import Food, Keyword
# Keyword.fix_tree(fix_paths=True)
# Food.fix_tree(fix_paths=True)
# except OperationalError:
# if DEBUG:
# traceback.print_exc()
# pass # if model does not exist there is no need to fix it
# except ProgrammingError:
# if DEBUG:
# traceback.print_exc()
# pass # if migration has not been run database cannot be fixed yet
# except Exception:
# if DEBUG:
# traceback.print_exc()
# pass # dont break startup just because fix could not run, need to investigate cases when this happens

View File

@@ -1,16 +1,16 @@
from datetime import datetime
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms import widgets, NumberInput
from django.forms import NumberInput, widgets
from django.utils.translation import gettext_lazy as _
from django_scopes import scopes_disabled
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
from hcaptcha.fields import hCaptchaField
from .models import (Comment, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, Storage, Sync, User,
UserPreference, MealType, Space,
SearchPreference)
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook,
RecipeBookEntry, SearchPreference, Space, Storage, Sync, User, UserPreference)
class SelectWidget(widgets.Select):
@@ -37,7 +37,10 @@ class UserPreferenceForm(forms.ModelForm):
prefix = 'preference'
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
if x := kwargs.get('instance', None):
space = x.space
else:
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['plan_share'].queryset = User.objects.filter(userpreference__space=space).all()
@@ -46,8 +49,7 @@ class UserPreferenceForm(forms.ModelForm):
fields = (
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
'sticky_navbar', 'default_page', 'show_recent', 'search_style',
'plan_share', 'ingredient_decimals', 'shopping_auto_sync',
'comments'
'plan_share', 'ingredient_decimals', 'comments',
)
labels = {
@@ -74,8 +76,8 @@ class UserPreferenceForm(forms.ModelForm):
'Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'),
# noqa: E501
'use_kj': _('Display nutritional energy amounts in joules instead of calories'), # noqa: E501
'plan_share': _(
'Users with whom newly created meal plan/shopping list entries should be shared by default.'),
'plan_share': _('Users with whom newly created meal plans should be shared by default.'),
'shopping_share': _('Users with whom to share shopping lists.'),
# noqa: E501
'show_recent': _('Show recently viewed recipes on search page.'), # noqa: E501
'ingredient_decimals': _('Number of decimals to round ingredients.'), # noqa: E501
@@ -84,11 +86,14 @@ class UserPreferenceForm(forms.ModelForm):
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit ' # noqa: E501
'of mobile data. If lower than instance limit it is reset when saving.' # noqa: E501
),
'sticky_navbar': _('Makes the navbar stick to the top of the page.') # noqa: E501
'sticky_navbar': _('Makes the navbar stick to the top of the page.'), # noqa: E501
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
'mealplan_autoexclude_onhand': _('Exclude ingredients that are on hand.'),
}
widgets = {
'plan_share': MultiSelectWidget
'plan_share': MultiSelectWidget,
'shopping_share': MultiSelectWidget,
}
@@ -140,7 +145,7 @@ class ImportExportBase(forms.Form):
NEXTCLOUD = 'NEXTCLOUD'
MEALIE = 'MEALIE'
CHOWDOWN = 'CHOWDOWN'
SAFRON = 'SAFRON'
SAFFRON = 'SAFFRON'
CHEFTAP = 'CHEFTAP'
PEPPERPLATE = 'PEPPERPLATE'
RECIPEKEEPER = 'RECIPEKEEPER'
@@ -152,13 +157,15 @@ class ImportExportBase(forms.Form):
OPENEATS = 'OPENEATS'
PLANTOEAT = 'PLANTOEAT'
COOKBOOKAPP = 'COOKBOOKAPP'
COPYMETHAT = 'COPYMETHAT'
PDF = 'PDF'
type = forms.ChoiceField(choices=(
(DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'),
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'),
(MEALIE, 'Mealie'), (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'),
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'),
))
@@ -170,7 +177,7 @@ class ImportForm(ImportExportBase):
class ExportForm(ImportExportBase):
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none())
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False)
all = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
@@ -222,6 +229,7 @@ class StorageForm(forms.ModelForm):
}
# TODO: Deprecate
class RecipeBookEntryForm(forms.ModelForm):
prefix = 'bookmark'
@@ -261,6 +269,7 @@ class SyncForm(forms.ModelForm):
}
# TODO deprecate
class BatchEditForm(forms.Form):
search = forms.CharField(label=_('Search String'))
keywords = forms.ModelMultipleChoiceField(
@@ -297,6 +306,7 @@ class ImportRecipeForm(forms.ModelForm):
}
# TODO deprecate
class MealPlanForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
@@ -348,8 +358,8 @@ class InviteLinkForm(forms.ModelForm):
def clean(self):
space = self.cleaned_data['space']
if space.max_users != 0 and (UserPreference.objects.filter(space=space).count() + InviteLink.objects.filter(
space=space).count()) >= space.max_users:
if space.max_users != 0 and (UserPreference.objects.filter(space=space).count() +
InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=space).count()) >= space.max_users:
raise ValidationError(_('Maximum number of users for this space reached.'))
def clean_email(self):
@@ -432,7 +442,7 @@ class SearchPreferenceForm(forms.ModelForm):
help_texts = {
'search': _(
'Select type method of search. Click <a href="/docs/search/">here</a> for full desciption of choices.'),
'Select type method of search. Click <a href="/docs/search/">here</a> for full description of choices.'),
'lookup': _('Use fuzzy matching on units, keywords and ingredients when editing and importing recipes.'),
'unaccent': _(
'Fields to search ignoring accents. Selecting this option can improve or degrade search quality depending on language'),
@@ -451,7 +461,7 @@ class SearchPreferenceForm(forms.ModelForm):
'lookup': _('Fuzzy Lookups'),
'unaccent': _('Ignore Accent'),
'icontains': _("Partial Match"),
'istartswith': _("Starts Wtih"),
'istartswith': _("Starts With"),
'trigram': _("Fuzzy Search"),
'fulltext': _("Full Text")
}
@@ -464,3 +474,73 @@ class SearchPreferenceForm(forms.ModelForm):
'trigram': MultiSelectWidget,
'fulltext': MultiSelectWidget,
}
class ShoppingPreferenceForm(forms.ModelForm):
prefix = 'shopping'
class Meta:
model = UserPreference
fields = (
'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand',
'mealplan_autoinclude_related', 'shopping_add_onhand', 'default_delay', 'filter_to_supermarket', 'shopping_recent_days', 'csv_delim', 'csv_prefix'
)
help_texts = {
'shopping_share': _('Users will see all items you add to your shopping list. They must add you to see items on their list.'),
'shopping_auto_sync': _(
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit ' # noqa: E501
'of mobile data. If lower than instance limit it is reset when saving.' # noqa: E501
),
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
'mealplan_autoinclude_related': _('When adding a meal plan to the shopping list (manually or automatically), include all related recipes.'),
'mealplan_autoexclude_onhand': _('When adding a meal plan to the shopping list (manually or automatically), exclude ingredients that are on hand.'),
'default_delay': _('Default number of hours to delay a shopping list entry.'),
'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'),
'shopping_recent_days': _('Days of recent shopping list entries to display.'),
'shopping_add_onhand': _("Mark food 'On Hand' when checked off shopping list."),
'csv_delim': _('Delimiter to use for CSV exports.'),
'csv_prefix': _('Prefix to add when copying list to the clipboard.'),
}
labels = {
'shopping_share': _('Share Shopping List'),
'shopping_auto_sync': _('Autosync'),
'mealplan_autoadd_shopping': _('Auto Add Meal Plan'),
'mealplan_autoexclude_onhand': _('Exclude On Hand'),
'mealplan_autoinclude_related': _('Include Related'),
'default_delay': _('Default Delay Hours'),
'filter_to_supermarket': _('Filter to Supermarket'),
'shopping_recent_days': _('Recent Days'),
'csv_delim': _('CSV Delimiter'),
"csv_prefix_label": _("List Prefix"),
'shopping_add_onhand': _("Auto On Hand"),
}
widgets = {
'shopping_share': MultiSelectWidget
}
class SpacePreferenceForm(forms.ModelForm):
prefix = 'space'
reset_food_inherit = forms.BooleanField(label=_("Reset Food Inheritance"), initial=False, required=False,
help_text=_("Reset all food to inherit the fields configured."))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # populates the post
self.fields['food_inherit'].queryset = Food.inheritable_fields
class Meta:
model = Space
fields = ('food_inherit', 'reset_food_inherit', 'show_facet_count')
help_texts = {
'food_inherit': _('Fields on food that should be inherited by default.'),
'show_facet_count': _('Show recipe counts on search filters'), }
widgets = {
'food_inherit': MultiSelectWidget
}

View File

@@ -0,0 +1,13 @@
from django.db.models import Func
class Round(Func):
function = 'ROUND'
template = '%(function)s(%(expressions)s, 0)'
def str2bool(v):
if type(v) == bool or v is None:
return v
else:
return v.lower() in ("yes", "true", "1")

View File

@@ -2,11 +2,9 @@
Source: https://djangosnippets.org/snippets/1703/
"""
from django.conf import settings
from django.core.cache import caches
from cookbook.models import ShareLink
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.core.cache import caches
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.urls import reverse, reverse_lazy
@@ -14,6 +12,8 @@ from django.utils.translation import gettext as _
from rest_framework import permissions
from rest_framework.permissions import SAFE_METHODS
from cookbook.models import ShareLink
def get_allowed_groups(groups_required):
"""
@@ -34,7 +34,7 @@ def has_group_permission(user, groups):
"""
Tests if a given user is member of a certain group (or any higher group)
Superusers always bypass permission checks.
Unauthenticated users cant be member of any group thus always return false.
Unauthenticated users can't be member of any group thus always return false.
:param user: django auth user object
:param groups: list or tuple of groups the user should be checked for
:return: True if user is in allowed groups, false otherwise
@@ -205,6 +205,9 @@ class CustomIsShared(permissions.BasePermission):
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
# temporary hack to make old shopping list work with new shopping list
if obj.__class__.__name__ == 'ShoppingList':
return is_object_shared(request.user, obj) or obj.created_by in list(request.user.get_shopping_share())
return is_object_shared(request.user, obj)

View File

@@ -1,13 +1,14 @@
import json
import re
from json import JSONDecodeError
from urllib.parse import unquote
from bs4 import BeautifulSoup
from bs4.element import Tag
from recipe_scrapers._utils import get_host_name, normalize_string
from cookbook.helper import recipe_url_import as helper
from cookbook.helper.scrapers.scrapers import text_scraper
from json import JSONDecodeError
from recipe_scrapers._utils import get_host_name, normalize_string
from urllib.parse import unquote
def get_recipe_from_source(text, url, request):
@@ -58,7 +59,7 @@ def get_recipe_from_source(text, url, request):
return kid_list
recipe_json = {
'name': '',
'name': '',
'url': '',
'description': '',
'image': '',
@@ -188,6 +189,6 @@ def remove_graph(el):
for x in el['@graph']:
if '@type' in x and x['@type'] == 'Recipe':
el = x
except TypeError:
except (TypeError, JSONDecodeError):
pass
return el

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,15 @@
import random
import re
from html import unescape
from django.utils.dateparse import parse_duration
from isodate import parse_duration as iso_parse_duration
from isodate.isoerror import ISO8601Error
from recipe_scrapers._utils import get_minutes
from cookbook.helper import recipe_url_import as helper
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.models import Keyword
from django.utils.dateparse import parse_duration
from html import unescape
from recipe_scrapers._utils import get_minutes
def get_from_scraper(scrape, request):
@@ -96,8 +98,9 @@ def get_from_scraper(scrape, request):
recipe_json['keywords'] = keywords
ingredient_parser = IngredientParser(request, True)
ingredients = []
try:
ingredients = []
for x in scrape.ingredients():
try:
amount, unit, ingredient, note = ingredient_parser.parse(x)

View File

@@ -5,6 +5,7 @@ from rest_framework.authtoken.models import Token
from rest_framework.exceptions import AuthenticationFailed
from cookbook.views import views
from recipes import settings
class ScopeMiddleware:
@@ -12,16 +13,17 @@ class ScopeMiddleware:
self.get_response = get_response
def __call__(self, request):
prefix = settings.JS_REVERSE_SCRIPT_PREFIX or ''
if request.user.is_authenticated:
if request.path.startswith('/admin/'):
if request.path.startswith(prefix + '/admin/'):
with scopes_disabled():
return self.get_response(request)
if request.path.startswith('/signup/') or request.path.startswith('/invite/'):
if request.path.startswith(prefix + '/signup/') or request.path.startswith(prefix + '/invite/'):
return self.get_response(request)
if request.path.startswith('/accounts/'):
if request.path.startswith(prefix + '/accounts/'):
return self.get_response(request)
with scopes_disabled():
@@ -36,7 +38,7 @@ class ScopeMiddleware:
with scope(space=request.space):
return self.get_response(request)
else:
if request.path.startswith('/api/'):
if request.path.startswith(prefix + '/api/'):
try:
if auth := TokenAuthentication().authenticate(request):
request.space = auth[0].userpreference.space

View File

@@ -0,0 +1,155 @@
from datetime import timedelta
from decimal import Decimal
from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import F, OuterRef, Q, Subquery, Value
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.utils.translation import gettext as _
from cookbook.helper.HelperFunctions import Round, str2bool
from cookbook.models import (Ingredient, ShoppingListEntry, ShoppingListRecipe,
SupermarketCategoryRelation)
from recipes import settings
def shopping_helper(qs, request):
supermarket = request.query_params.get('supermarket', None)
checked = request.query_params.get('checked', 'recent')
user = request.user
supermarket_order = [F('food__supermarket_category__name').asc(nulls_first=True), 'food__name']
# TODO created either scheduled task or startup task to delete very old shopping list entries
# TODO create user preference to define 'very old'
if supermarket:
supermarket_categories = SupermarketCategoryRelation.objects.filter(supermarket=supermarket, category=OuterRef('food__supermarket_category'))
qs = qs.annotate(supermarket_order=Coalesce(Subquery(supermarket_categories.values('order')), Value(9999)))
supermarket_order = ['supermarket_order'] + supermarket_order
if checked in ['false', 0, '0']:
qs = qs.filter(checked=False)
elif checked in ['true', 1, '1']:
qs = qs.filter(checked=True)
elif checked in ['recent']:
today_start = timezone.now().replace(hour=0, minute=0, second=0)
week_ago = today_start - timedelta(days=user.userpreference.shopping_recent_days)
qs = qs.filter(Q(checked=False) | Q(completed_at__gte=week_ago))
supermarket_order = ['checked'] + supermarket_order
return qs.order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe')
# TODO refactor as class
def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None, append=False):
"""
Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
:param list_recipe: Modify an existing ShoppingListRecipe
:param recipe: Recipe to use as list of ingredients. One of [recipe, mealplan] are required
:param mealplan: alternatively use a mealplan recipe as source of ingredients
:param servings: Optional: Number of servings to use to scale shoppinglist. If servings = 0 an existing recipe list will be deleted
:param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
:param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list
"""
r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
if not r:
raise ValueError(_("You must supply a recipe or mealplan"))
created_by = created_by or getattr(ShoppingListEntry.objects.filter(list_recipe=list_recipe).first(), 'created_by', None)
if not created_by:
raise ValueError(_("You must supply a created_by"))
try:
servings = float(servings)
except (ValueError, TypeError):
servings = getattr(mealplan, 'servings', 1.0)
servings_factor = servings / r.servings
shared_users = list(created_by.get_shopping_share())
shared_users.append(created_by)
if list_recipe:
created = False
else:
list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings)
created = True
related_step_ing = []
if servings == 0 and not created:
list_recipe.delete()
return []
elif ingredients:
ingredients = Ingredient.objects.filter(pk__in=ingredients, space=space)
else:
ingredients = Ingredient.objects.filter(step__recipe=r, space=space)
if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
ingredients = ingredients.exclude(food__onhand_users__id__in=[x.id for x in shared_users])
if related := created_by.userpreference.mealplan_autoinclude_related:
# TODO: add levels of related recipes (related recipes of related recipes) to use when auto-adding mealplans
related_recipes = r.get_related_recipes()
for x in related_recipes:
# related recipe is a Step serving size is driven by recipe serving size
# TODO once/if Steps can have a serving size this needs to be refactored
if exclude_onhand:
# if steps are used more than once in a recipe or subrecipe - I don' think this results in the desired behavior
related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users]).values_list('id', flat=True)
else:
related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True)
x_ing = []
if ingredients.filter(food__recipe=x).exists():
for ing in ingredients.filter(food__recipe=x):
if exclude_onhand:
x_ing = Ingredient.objects.filter(step__recipe=x, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
else:
x_ing = Ingredient.objects.filter(step__recipe=x, space=space)
for i in [x for x in x_ing]:
ShoppingListEntry.objects.create(
list_recipe=list_recipe,
food=i.food,
unit=i.unit,
ingredient=i,
amount=i.amount * Decimal(servings_factor),
created_by=created_by,
space=space,
)
# dont' add food to the shopping list that are actually recipes that will be added as ingredients
ingredients = ingredients.exclude(food__recipe=x)
add_ingredients = list(ingredients.values_list('id', flat=True)) + related_step_ing
if not append:
existing_list = ShoppingListEntry.objects.filter(list_recipe=list_recipe)
# delete shopping list entries not included in ingredients
existing_list.exclude(ingredient__in=ingredients).delete()
# add shopping list entries that did not previously exist
add_ingredients = set(add_ingredients) - set(existing_list.values_list('ingredient__id', flat=True))
add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
# if servings have changed, update the ShoppingListRecipe and existing Entries
if servings <= 0:
servings = 1
if not created and list_recipe.servings != servings:
update_ingredients = set(ingredients.values_list('id', flat=True)) - set(add_ingredients.values_list('id', flat=True))
list_recipe.servings = servings
list_recipe.save()
for sle in ShoppingListEntry.objects.filter(list_recipe=list_recipe, ingredient__id__in=update_ingredients):
sle.amount = sle.ingredient.amount * Decimal(servings_factor)
sle.save()
# add any missing Entries
for i in [x for x in add_ingredients if x.food]:
ShoppingListEntry.objects.create(
list_recipe=list_recipe,
food=i.food,
unit=i.unit,
ingredient=i,
amount=i.amount * Decimal(servings_factor),
created_by=created_by,
space=space,
)
# return all shopping list items
return list_recipe

View File

@@ -5,7 +5,7 @@ from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.helper.mdx_urlize import UrlizeExtension
from jinja2 import Template, TemplateSyntaxError, UndefinedError
from gettext import gettext as _
from markdown.extensions.tables import TableExtension
class IngredientObject(object):
amount = ""
@@ -41,7 +41,7 @@ def render_instructions(step): # TODO deduplicate markdown cleanup code
parsed_md = md.markdown(
instructions,
extensions=[
'markdown.extensions.fenced_code', 'tables',
'markdown.extensions.fenced_code', TableExtension(),
UrlizeExtension(), MarkdownFormatExtension()
]
)

View File

@@ -0,0 +1,84 @@
import re
from io import BytesIO
from zipfile import ZipFile
from bs4 import BeautifulSoup
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_url_import import iso_duration_to_minutes, parse_servings
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from recipes.settings import DEBUG
class CopyMeThat(Integration):
def import_file_name_filter(self, zip_info_object):
if DEBUG:
print("testing", zip_info_object.filename, zip_info_object.filename == 'recipes.html')
return zip_info_object.filename == 'recipes.html'
def get_recipe_from_file(self, file):
# 'file' comes is as a beautifulsoup object
recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, )
for category in file.find_all("span", {"class": "recipeCategory"}):
keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space)
recipe.keywords.add(keyword)
try:
recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip())
recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
recipe.waiting_time = iso_duration_to_minutes(file.find("span", {"meta": "cookTime"}).text.strip())
recipe.save()
except AttributeError:
pass
step = Step.objects.create(instruction='', space=self.request.space, )
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file.find_all("li", {"class": "recipeIngredient"}):
if ingredient.text == "":
continue
amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
for s in file.find_all("li", {"class": "instruction"}):
if s.text == "":
continue
step.instruction += s.text.strip() + ' \n\n'
for s in file.find_all("li", {"class": "recipeNote"}):
if s.text == "":
continue
step.instruction += s.text.strip() + ' \n\n'
try:
if file.find("a", {"id": "original_link"}).text != '':
step.instruction += "\n\nImported from: " + file.find("a", {"id": "original_link"}).text
step.save()
except AttributeError:
pass
recipe.steps.add(step)
# import the Primary recipe image that is stored in the Zip
try:
for f in self.files:
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipeImage").get("src"))), filetype='.jpeg')
except Exception as e:
print(recipe.name, ': failed to import image ', str(e))
recipe.save()
return recipe
def split_recipe_file(self, file):
soup = BeautifulSoup(file, "html.parser")
return soup.find_all("div", {"class": "recipe"})

View File

@@ -1,5 +1,5 @@
import json
from io import BytesIO
from io import BytesIO, StringIO
from re import match
from zipfile import ZipFile
@@ -35,3 +35,28 @@ class Default(Integration):
export = RecipeExportSerializer(recipe).data
return 'recipe.json', JSONRenderer().render(export).decode("utf-8")
def get_files_from_recipes(self, recipes, cookie):
export_zip_stream = BytesIO()
export_zip_obj = ZipFile(export_zip_stream, 'w')
for r in recipes:
if r.internal and r.space == self.request.space:
recipe_zip_stream = BytesIO()
recipe_zip_obj = ZipFile(recipe_zip_stream, 'w')
recipe_stream = StringIO()
filename, data = self.get_file_from_recipe(r)
recipe_stream.write(data)
recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
recipe_stream.close()
try:
recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read())
except ValueError:
pass
recipe_zip_obj.close()
export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue())
export_zip_obj.close()
return [[ 'export.zip', export_zip_stream.getvalue() ]]

View File

@@ -3,8 +3,9 @@ import json
import traceback
import uuid
from io import BytesIO, StringIO
from zipfile import ZipFile, BadZipFile
from zipfile import BadZipFile, ZipFile
from bs4 import Tag
from django.core.exceptions import ObjectDoesNotExist
from django.core.files import File
from django.db import IntegrityError
@@ -16,7 +17,7 @@ from django_scopes import scope
from cookbook.forms import ImportExportBase
from cookbook.helper.image_processing import get_filetype, handle_image
from cookbook.models import Keyword, Recipe
from recipes.settings import DATABASES, DEBUG
from recipes.settings import DEBUG
class Integration:
@@ -41,7 +42,7 @@ class Integration:
try:
last_kw = Keyword.objects.filter(name__regex=r'^(Import [0-9]+)', space=request.space).latest('created_at')
name = f'Import {int(last_kw.name.replace("Import ", "")) + 1}'
except ObjectDoesNotExist:
except (ObjectDoesNotExist, ValueError):
name = 'Import 1'
parent, created = Keyword.objects.get_or_create(name='Import', space=request.space)
@@ -52,7 +53,7 @@ class Integration:
icon=icon,
space=request.space
)
except IntegrityError: # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
except (IntegrityError, ValueError): # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
self.keyword = parent.add_child(
name=f'{name} {str(uuid.uuid4())[0:8]}',
description=description,
@@ -64,45 +65,30 @@ class Integration:
"""
Perform the export based on a list of recipes
:param recipes: list of recipe objects
:return: HttpResponse with a ZIP file that is directly downloaded
:return: HttpResponse with the file of the requested export format that is directly downloaded (When that format involve multiple files they are zipped together)
"""
# TODO this is temporary, find a better solution for different export formats when doing other exporters
if self.export_type != ImportExportBase.RECIPESAGE:
export_zip_stream = BytesIO()
export_zip_obj = ZipFile(export_zip_stream, 'w')
files = self.get_files_from_recipes(recipes, self.request.COOKIES)
for r in recipes:
if r.internal and r.space == self.request.space:
recipe_zip_stream = BytesIO()
recipe_zip_obj = ZipFile(recipe_zip_stream, 'w')
if len(files) == 1:
filename, file = files[0]
export_filename = filename
export_file = file
recipe_stream = StringIO()
filename, data = self.get_file_from_recipe(r)
recipe_stream.write(data)
recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
recipe_stream.close()
try:
recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read())
except ValueError:
pass
recipe_zip_obj.close()
export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue())
export_zip_obj.close()
response = HttpResponse(export_zip_stream.getvalue(), content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="export.zip"'
return response
else:
json_list = []
for r in recipes:
json_list.append(self.get_file_from_recipe(r))
export_filename = "export.zip"
export_stream = BytesIO()
export_obj = ZipFile(export_stream, 'w')
response = HttpResponse(json.dumps(json_list), content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="recipes.json"'
return response
for filename, file in files:
export_obj.writestr(filename, file)
export_obj.close()
export_file = export_stream.getvalue()
response = HttpResponse(export_file, content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="' + export_filename + '"'
return response
def import_file_name_filter(self, zip_info_object):
"""
@@ -153,9 +139,17 @@ class Integration:
file_list.append(z)
il.total_recipes += len(file_list)
import cookbook
if isinstance(self, cookbook.integration.copymethat.CopyMeThat):
file_list = self.split_recipe_file(BytesIO(import_zip.read('recipes.html')))
il.total_recipes += len(file_list)
for z in file_list:
try:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
if 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 += f'{recipe.pk} - {recipe.name} \n'
self.handle_duplicates(recipe, import_duplicates)
@@ -266,6 +260,16 @@ class Integration:
"""
raise NotImplementedError('Method not implemented in integration')
def get_files_from_recipes(self, recipes, cookie):
"""
Takes a list of recipe object and converts it to a array containing each file.
Each file is represented as an array [filename, data] where data is a string of the content of the file.
:param recipe: Recipe object that should be converted
:returns:
[[filename, data], ...]
"""
raise NotImplementedError('Method not implemented in integration')
@staticmethod
def handle_exception(exception, log=None, message=''):
if log:

View File

@@ -0,0 +1,55 @@
import json
from io import BytesIO
from re import match
from zipfile import ZipFile
import asyncio
from pyppeteer import launch
from rest_framework.renderers import JSONRenderer
from cookbook.helper.image_processing import get_filetype
from cookbook.integration.integration import Integration
from cookbook.serializer import RecipeExportSerializer
import django.core.management.commands.runserver as runserver
class PDFexport(Integration):
def get_recipe_from_file(self, file):
raise NotImplementedError('Method not implemented in storage integration')
async def get_files_from_recipes_async(self, recipes, cookie):
cmd = runserver.Command()
browser = await launch(
handleSIGINT=False,
handleSIGTERM=False,
handleSIGHUP=False,
ignoreHTTPSErrors=True
)
cookies = {'domain': cmd.default_addr, 'name': 'sessionid', 'value': cookie['sessionid'], }
options = {'format': 'letter',
'margin': {
'top': '0.75in',
'bottom': '0.75in',
'left': '0.75in',
'right': '0.75in',
}
}
page = await browser.newPage()
await page.emulateMedia('print')
await page.setCookie(cookies)
files = []
for recipe in recipes:
await page.goto('http://' + cmd.default_addr + ':' + cmd.default_port + '/view/recipe/' + str(recipe.id), {'waitUntil': 'networkidle0', })
files.append([recipe.name + '.pdf', await page.pdf(options)])
await browser.close()
return files
def get_files_from_recipes(self, recipes, cookie):
return asyncio.run(self.get_files_from_recipes_async(recipes, cookie))

View File

@@ -27,10 +27,10 @@ class RecetteTek(Integration):
def get_recipe_from_file(self, file):
# Create initial recipe with just a title and a decription
# Create initial recipe with just a title and a description
recipe = Recipe.objects.create(name=file['title'], created_by=self.request.user, internal=True, space=self.request.space, )
# set the description as an empty string for later use for the source URL, incase there is no description text.
# set the description as an empty string for later use for the source URL, in case there is no description text.
recipe.description = ''
try:

View File

@@ -88,5 +88,12 @@ class RecipeSage(Integration):
return data
def get_files_from_recipes(self, recipes, cookie):
json_list = []
for r in recipes:
json_list.append(self.get_file_from_recipe(r))
return [['export.json', json.dumps(json_list)]]
def split_recipe_file(self, file):
return json.loads(file.read().decode("utf-8"))

View File

@@ -5,7 +5,7 @@ from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
class Safron(Integration):
class Saffron(Integration):
def get_recipe_from_file(self, file):
ingredient_mode = False
@@ -58,4 +58,39 @@ class Safron(Integration):
return recipe
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')
data = "Title: "+recipe.name if recipe.name else ""+"\n"
data += "Description: "+recipe.description if recipe.description else ""+"\n"
data += "Source: \n"
data += "Original URL: \n"
data += "Yield: "+str(recipe.servings)+"\n"
data += "Cookbook: \n"
data += "Section: \n"
data += "Image: \n"
recipeInstructions = []
recipeIngredient = []
for s in recipe.steps.all():
if s.type != Step.TIME:
recipeInstructions.append(s.instruction)
for i in s.ingredients.all():
recipeIngredient.append(f'{float(i.amount)} {i.unit} {i.food}')
data += "Ingredients: \n"
for ingredient in recipeIngredient:
data += ingredient+"\n"
data += "Instructions: \n"
for instruction in recipeInstructions:
data += instruction+"\n"
return recipe.name+'.txt', data
def get_files_from_recipes(self, recipes, cookie):
files = []
for r in recipes:
filename, data = self.get_file_from_recipe(r)
files.append([ filename, data ])
return files

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
if settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
self.stdout.write(self.style.WARNING(_('Only Postgress databases use full text search, no index to rebuild')))
self.stdout.write(self.style.WARNING(_('Only Postgresql databases use full text search, no index to rebuild')))
try:
language = DICTIONARY.get(translation.get_language(), 'simple')

View File

@@ -2,13 +2,15 @@
import annoying.fields
from django.conf import settings
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField, SearchVector
from django.contrib.postgres.search import SearchVector, SearchVectorField
from django.db import migrations, models
from django.db.models import deletion
from django_scopes import scopes_disabled
from django.utils import translation
from django_scopes import scopes_disabled
from cookbook.managers import DICTIONARY
from cookbook.models import Recipe, Step, Index, PermissionModelMixin, nameSearchField, allSearchFields
from cookbook.models import (Index, PermissionModelMixin, Recipe, Step, allSearchFields,
nameSearchField)
def set_default_search_vector(apps, schema_editor):
@@ -16,8 +18,6 @@ def set_default_search_vector(apps, schema_editor):
return
language = DICTIONARY.get(translation.get_language(), 'simple')
with scopes_disabled():
# TODO this approach doesn't work terribly well if multiple languages are in use
# I'm also uncertain about forcing unaccent here
Recipe.objects.all().update(
name_search_vector=SearchVector('name__unaccent', weight='A', config=language),
desc_search_vector=SearchVector('description__unaccent', weight='B', config=language)

View File

@@ -0,0 +1,144 @@
# Generated by Django 3.2.7 on 2021-10-01 20:52
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
from django_scopes import scopes_disabled
from cookbook.models import PermissionModelMixin, ShoppingListEntry
def copy_values_to_sle(apps, schema_editor):
with scopes_disabled():
entries = ShoppingListEntry.objects.all()
for entry in entries:
if entry.shoppinglist_set.first():
entry.created_by = entry.shoppinglist_set.first().created_by
entry.space = entry.shoppinglist_set.first().space
if entries:
ShoppingListEntry.objects.bulk_update(entries, ["created_by", "space", ])
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0158_userpreference_use_kj'),
]
operations = [
migrations.AddField(
model_name='shoppinglistentry',
name='completed_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='shoppinglistentry',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='shoppinglistentry',
name='created_by',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='auth.user'),
preserve_default=False,
),
migrations.AddField(
model_name='userpreference',
name='shopping_share',
field=models.ManyToManyField(blank=True, related_name='shopping_share', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='shoppinglistentry',
name='space',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
preserve_default=False,
),
migrations.AddField(
model_name='shoppinglistrecipe',
name='mealplan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.mealplan'),
),
migrations.AddField(
model_name='shoppinglistrecipe',
name='name',
field=models.CharField(blank=True, default='', max_length=32),
),
migrations.AddField(
model_name='shoppinglistentry',
name='ingredient',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.ingredient'),
),
migrations.AlterField(
model_name='shoppinglistentry',
name='unit',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.unit'),
),
migrations.AddField(
model_name='userpreference',
name='mealplan_autoadd_shopping',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='userpreference',
name='mealplan_autoexclude_onhand',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='shoppinglistentry',
name='list_recipe',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='cookbook.shoppinglistrecipe'),
),
migrations.CreateModel(
name='FoodInheritField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('field', models.CharField(max_length=32, unique=True)),
('name', models.CharField(max_length=64, unique=True)),
],
bases=(models.Model, PermissionModelMixin),
),
migrations.AddField(
model_name='userpreference',
name='mealplan_autoinclude_related',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='food',
name='inherit_fields',
field=models.ManyToManyField(blank=True, to='cookbook.FoodInheritField'),
),
migrations.AddField(
model_name='space',
name='food_inherit',
field=models.ManyToManyField(blank=True, to='cookbook.FoodInheritField'),
),
migrations.AddField(
model_name='shoppinglistentry',
name='delay_until',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='userpreference',
name='default_delay',
field=models.DecimalField(decimal_places=4, default=4, max_digits=8),
),
migrations.AddField(
model_name='userpreference',
name='filter_to_supermarket',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='userpreference',
name='shopping_recent_days',
field=models.PositiveIntegerField(default=7),
),
migrations.RenameField(
model_name='food',
old_name='ignore_shopping',
new_name='food_onhand',
),
migrations.RunPython(copy_values_to_sle),
]

View File

@@ -0,0 +1,50 @@
# Generated by Django 3.2.7 on 2021-10-01 22:34
import datetime
from datetime import timedelta
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
from django.utils import timezone
from django.utils.timezone import utc
from django_scopes import scopes_disabled
from cookbook.models import FoodInheritField, ShoppingListEntry
def delete_orphaned_sle(apps, schema_editor):
with scopes_disabled():
# shopping list entry is orphaned - delete it
ShoppingListEntry.objects.filter(shoppinglist=None).delete()
def create_inheritfields(apps, schema_editor):
FoodInheritField.objects.create(name='Supermarket Category', field='supermarket_category')
FoodInheritField.objects.create(name='On Hand', field='food_onhand')
FoodInheritField.objects.create(name='Diet', field='diet')
FoodInheritField.objects.create(name='Substitute', field='substitute')
FoodInheritField.objects.create(name='Substitute Children', field='substitute_children')
FoodInheritField.objects.create(name='Substitute Siblings', field='substitute_siblings')
def set_completed_at(apps, schema_editor):
today_start = timezone.now().replace(hour=0, minute=0, second=0)
# arbitrary - keeping all of the closed shopping list items out of the 'recent' view
month_ago = today_start - timedelta(days=30)
with scopes_disabled():
ShoppingListEntry.objects.filter(checked=True).update(completed_at=month_ago)
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0159_add_shoppinglistentry_fields'),
]
operations = [
migrations.RunPython(delete_orphaned_sle),
migrations.RunPython(create_inheritfields),
migrations.RunPython(set_completed_at),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.8 on 2021-11-03 23:19
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0160_delete_shoppinglist_orphans'),
]
operations = [
migrations.AlterField(
model_name='shoppinglistentry',
name='food',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shopping_entries', to='cookbook.food'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.9 on 2021-11-30 22:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0161_alter_shoppinglistentry_food'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='csv_delim',
field=models.CharField(default=',', max_length=2),
),
migrations.AddField(
model_name='userpreference',
name='csv_prefix',
field=models.CharField(blank=True, max_length=10),
),
]

View File

@@ -0,0 +1,41 @@
# Generated by Django 3.2.10 on 2022-01-05 13:58
from django.conf import settings
from django.db import migrations, models
from cookbook.models import FoodInheritField
def rename_inherit_field(apps, schema_editor):
x = FoodInheritField.objects.filter(name='On Hand', field='food_onhand').first()
if x:
x.name = "Ignore Shopping"
x.field = "ignore_shopping"
x.save()
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0162_userpreference_csv_delim'),
]
operations = [
migrations.AddField(
model_name='food',
name='onhand_users',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='userpreference',
name='shopping_add_onhand',
field=models.BooleanField(default=True),
),
migrations.RenameField(
model_name='food',
old_name='food_onhand',
new_name='ignore_shopping',
),
migrations.RunPython(rename_inherit_field),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.11 on 2022-01-17 22:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0163_auto_20220105_0758'),
]
operations = [
migrations.AddField(
model_name='space',
name='show_facet_count',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.11 on 2022-01-18 19:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0164_space_show_facet_count'),
]
operations = [
migrations.RemoveField(
model_name='step',
name='type',
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.2.11 on 2022-01-20 14:39
from django.db import migrations, models
from django_scopes import scopes_disabled
def add_default_trigram(apps, schema_editor):
with scopes_disabled():
UserPreference = apps.get_model('cookbook', 'UserPreference')
UserPreference.objects.all().update(shopping_add_onhand=False)
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0165_remove_step_type'),
]
operations = [
migrations.AlterField(
model_name='userpreference',
name='shopping_add_onhand',
field=models.BooleanField(default=False),
),
migrations.RunPython(add_default_trigram),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.11 on 2022-01-20 22:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0166_alter_userpreference_shopping_add_onhand'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='left_handed',
field=models.BooleanField(default=False),
),
]

View File

@@ -2,25 +2,30 @@ import operator
import pathlib
import re
import uuid
from collections import OrderedDict
from datetime import date, timedelta
from decimal import Decimal
from annoying.fields import AutoOneToOneField
from django.contrib import auth
from django.contrib.auth.models import Group, User
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField
from django.core.files.uploadedfile import UploadedFile, InMemoryUploadedFile
from django.core.files.uploadedfile import InMemoryUploadedFile, UploadedFile
from django.core.validators import MinLengthValidator
from django.db import models, IntegrityError
from django.db.models import Index, ProtectedError
from django.db import IntegrityError, models
from django.db.models import Index, ProtectedError, Q, Subquery
from django.db.models.fields.related import ManyToManyField
from django.db.models.functions import Substr
from django.db.transaction import atomic
from django.utils import timezone
from django.utils.translation import gettext as _
from treebeard.mp_tree import MP_Node, MP_NodeManager
from django_scopes import ScopedManager, scopes_disabled
from django_prometheus.models import ExportModelOperationsMixin
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT,
KJ_PREF_DEFAULT, STICKY_NAV_PREF_DEFAULT,
SORT_TREE_BY_NAME)
from django_scopes import ScopedManager, scopes_disabled
from treebeard.mp_tree import MP_Node, MP_NodeManager
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, KJ_PREF_DEFAULT,
SORT_TREE_BY_NAME, STICKY_NAV_PREF_DEFAULT)
def get_user_name(self):
@@ -30,7 +35,20 @@ def get_user_name(self):
return self.username
def get_shopping_share(self):
# get list of users that shared shopping list with user. Django ORM forbids this type of query, so raw is required
return User.objects.raw(' '.join([
'SELECT auth_user.id FROM auth_user',
'INNER JOIN cookbook_userpreference',
'ON (auth_user.id = cookbook_userpreference.user_id)',
'INNER JOIN cookbook_userpreference_shopping_share',
'ON (cookbook_userpreference.user_id = cookbook_userpreference_shopping_share.userpreference_id)',
'WHERE cookbook_userpreference_shopping_share.user_id ={}'.format(self.id)
]))
auth.models.User.add_to_class('get_user_name', get_user_name)
auth.models.User.add_to_class('get_shopping_share', get_shopping_share)
def get_model_name(model):
@@ -38,15 +56,32 @@ def get_model_name(model):
class TreeManager(MP_NodeManager):
def create(self, *args, **kwargs):
return self.get_or_create(*args, **kwargs)[0]
# model.Manager get_or_create() is not compatible with MP_Tree
def get_or_create(self, **kwargs):
def get_or_create(self, *args, **kwargs):
kwargs['name'] = kwargs['name'].strip()
try:
return self.get(name__exact=kwargs['name'], space=kwargs['space']), False
except self.model.DoesNotExist:
with scopes_disabled():
try:
return self.model.add_root(**kwargs), True
defaults = kwargs.pop('defaults', None)
if defaults:
kwargs = {**kwargs, **defaults}
# ManyToMany fields can't be set this way, so pop them out to save for later
fields = [field.name for field in self.model._meta.get_fields() if issubclass(type(field), ManyToManyField)]
many_to_many = {field: kwargs.pop(field) for field in list(kwargs) if field in fields}
obj = self.model.add_root(**kwargs)
for field in many_to_many:
field_model = getattr(obj, field).model
for related_obj in many_to_many[field]:
if isinstance(related_obj, User):
getattr(obj, field).add(field_model.objects.get(id=related_obj.id))
else:
getattr(obj, field).add(field_model.objects.get(**dict(related_obj)))
return obj, True
except IntegrityError as e:
if 'Key (path)' in e.args[0]:
self.model.fix_tree(fix_paths=True)
@@ -62,6 +97,13 @@ class TreeModel(MP_Node):
else:
return f"{self.name}"
# MP_Tree move uses raw SQL to execute move, override behavior to force a save triggering post_save signal
def move(self, *args, **kwargs):
super().move(*args, **kwargs)
# treebeard bypasses ORM, need to retrieve the object again to avoid writing previous state back to disk
obj = self.__class__.objects.get(id=self.id)
obj.save()
@property
def parent(self):
parent = self.get_parent()
@@ -108,6 +150,48 @@ class TreeModel(MP_Node):
with scopes_disabled():
return super().add_root(**kwargs)
# i'm 99% sure there is a more idiomatic way to do this subclassing MP_NodeQuerySet
@staticmethod
def include_descendants(queryset=None, filter=None):
"""
:param queryset: Model Queryset to add descendants
:param filter: Filter (exclude) the descendants nodes with the provided Q filter
"""
descendants = Q()
# TODO filter the queryset nodes to exclude descendants of objects in the queryset
nodes = queryset.values('path', 'depth')
for node in nodes:
descendants |= Q(path__startswith=node['path'], depth__gt=node['depth'])
return queryset.model.objects.filter(Q(id__in=queryset.values_list('id')) | descendants)
def exclude_descendants(queryset=None, filter=None):
"""
:param queryset: Model Queryset to add descendants
:param filter: Filter (include) the descendants nodes with the provided Q filter
"""
descendants = Q()
# TODO filter the queryset nodes to exclude descendants of objects in the queryset
nodes = queryset.values('path', 'depth')
for node in nodes:
descendants |= Q(path__startswith=node['path'], depth__gt=node['depth'])
return queryset.model.objects.filter(id__in=queryset.values_list('id')).exclude(descendants)
def include_ancestors(queryset=None):
"""
:param queryset: Model Queryset to add ancestors
:param filter: Filter (include) the ancestors nodes with the provided Q filter
"""
queryset = queryset.annotate(root=Substr('path', 1, queryset.model.steplen))
nodes = list(set(queryset.values_list('root', 'depth')))
ancestors = Q()
for node in nodes:
ancestors |= Q(path__startswith=node[0], depth__lt=node[1])
return queryset.model.objects.filter(Q(id__in=queryset.values_list('id')) | ancestors)
class Meta:
abstract = True
@@ -141,6 +225,18 @@ class PermissionModelMixin:
raise NotImplementedError('get space for method not implemented and standard fields not available')
class FoodInheritField(models.Model, PermissionModelMixin):
field = models.CharField(max_length=32, unique=True)
name = models.CharField(max_length=64, unique=True)
def __str__(self):
return _(self.name)
@staticmethod
def get_name(self):
return _(self.name)
class Space(ExportModelOperationsMixin('space'), models.Model):
name = models.CharField(max_length=128, default='Default')
created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
@@ -151,6 +247,8 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
max_users = models.IntegerField(default=0)
allow_sharing = models.BooleanField(default=True)
demo = models.BooleanField(default=False)
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
show_facet_count = models.BooleanField(default=False)
def __str__(self):
return self.name
@@ -229,10 +327,23 @@ class UserPreference(models.Model, PermissionModelMixin):
plan_share = models.ManyToManyField(
User, blank=True, related_name='plan_share_default'
)
shopping_share = models.ManyToManyField(
User, blank=True, related_name='shopping_share'
)
ingredient_decimals = models.IntegerField(default=2)
comments = models.BooleanField(default=COMMENT_PREF_DEFAULT)
shopping_auto_sync = models.IntegerField(default=5)
sticky_navbar = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT)
mealplan_autoadd_shopping = models.BooleanField(default=False)
mealplan_autoexclude_onhand = models.BooleanField(default=True)
mealplan_autoinclude_related = models.BooleanField(default=True)
shopping_add_onhand = models.BooleanField(default=False)
filter_to_supermarket = models.BooleanField(default=False)
left_handed = models.BooleanField(default=False)
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
shopping_recent_days = models.PositiveIntegerField(default=7)
csv_delim = models.CharField(max_length=2, default=",")
csv_prefix = models.CharField(max_length=10, blank=True,)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
@@ -347,8 +458,8 @@ class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelM
name = models.CharField(max_length=64)
icon = models.CharField(max_length=16, blank=True, null=True)
description = models.TextField(default="", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True) # TODO deprecate
updated_at = models.DateTimeField(auto_now=True) # TODO deprecate
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space', _manager_class=TreeManager)
@@ -377,13 +488,19 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
# exclude fields not implemented yet
inheritable_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings'])
# WARNING: Food inheritance relies on post_save signals, avoid using UPDATE to update Food objects unless you intend to bypass those signals
if SORT_TREE_BY_NAME:
node_order_by = ['name']
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL)
ignore_shopping = models.BooleanField(default=False)
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) # inherited field
ignore_shopping = models.BooleanField(default=False) # inherited field
onhand_users = models.ManyToManyField(User, blank=True)
description = models.TextField(default='', blank=True)
inherit_fields = models.ManyToManyField(FoodInheritField, blank=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space', _manager_class=TreeManager)
@@ -397,6 +514,35 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
else:
return super().delete()
@staticmethod
def reset_inheritance(space=None):
# resets inherited fields to the space defaults and updates all inherited fields to root object values
inherit = space.food_inherit.all()
# remove all inherited fields from food
Through = Food.objects.filter(space=space).first().inherit_fields.through
Through.objects.all().delete()
# food is going to inherit attributes
if space.food_inherit.all().count() > 0:
# ManyToMany cannot be updated through an UPDATE operation
for i in inherit:
Through.objects.bulk_create([
Through(food_id=x, foodinheritfield_id=i.id)
for x in Food.objects.filter(space=space).values_list('id', flat=True)
])
inherit = inherit.values_list('field', flat=True)
if 'ignore_shopping' in inherit:
# get food at root that have children that need updated
Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, ignore_shopping=True)).update(ignore_shopping=True)
Food.include_descendants(queryset=Food.objects.filter(depth=1, numchild__gt=0, space=space, ignore_shopping=False)).update(ignore_shopping=False)
if 'supermarket_category' in inherit:
# when supermarket_category is null or blank assuming it is not set and not intended to be blank for all descedants
# find top node that has category set
category_roots = Food.exclude_descendants(queryset=Food.objects.filter(supermarket_category__isnull=False, numchild__gt=0, space=space))
for root in category_roots:
root.get_descendants().update(supermarket_category=root.supermarket_category)
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='f_unique_name_per_space')
@@ -431,17 +577,7 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixin):
TEXT = 'TEXT'
TIME = 'TIME'
FILE = 'FILE'
RECIPE = 'RECIPE'
name = models.CharField(max_length=128, default='', blank=True)
type = models.CharField(
choices=((TEXT, _('Text')), (TIME, _('Time')), (FILE, _('File')), (RECIPE, _('Recipe')),),
default=TEXT,
max_length=16
)
instruction = models.TextField(blank=True)
ingredients = models.ManyToManyField(Ingredient, blank=True)
time = models.IntegerField(default=0, blank=True)
@@ -473,9 +609,7 @@ class NutritionInformation(models.Model, PermissionModelMixin):
)
proteins = models.DecimalField(default=0, decimal_places=16, max_digits=32)
calories = models.DecimalField(default=0, decimal_places=16, max_digits=32)
source = models.CharField(
max_length=512, default="", null=True, blank=True
)
source = models.CharField( max_length=512, default="", null=True, blank=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@@ -484,6 +618,15 @@ class NutritionInformation(models.Model, PermissionModelMixin):
return f'Nutrition {self.pk}'
# class NutritionType(models.Model, PermissionModelMixin):
# name = models.CharField(max_length=128)
# icon = models.CharField(max_length=16, blank=True, null=True)
# description = models.CharField(max_length=512, blank=True, null=True)
#
# space = models.ForeignKey(Space, on_delete=models.CASCADE)
# objects = ScopedManager(space='space')
class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
description = models.CharField(max_length=512, blank=True, null=True)
@@ -518,6 +661,21 @@ class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModel
def __str__(self):
return self.name
def get_related_recipes(self, levels=1):
# recipes for step recipe
step_recipes = Q(id__in=self.steps.exclude(step_recipe=None).values_list('step_recipe'))
# recipes for foods
food_recipes = Q(id__in=Food.objects.filter(ingredient__step__recipe=self).exclude(recipe=None).values_list('recipe'))
related_recipes = Recipe.objects.filter(step_recipes | food_recipes)
if levels == 1:
return related_recipes
# this can loop over multiple levels if you update the value of related_recipes at each step (maybe an array?)
# for now keeping it at 2 levels max, should be sufficient in 99.9% of scenarios
sub_step_recipes = Q(id__in=Step.objects.filter(recipe__in=related_recipes.values_list('steps')).exclude(step_recipe=None).values_list('step_recipe'))
sub_food_recipes = Q(id__in=Food.objects.filter(ingredient__step__recipe__in=related_recipes).exclude(recipe=None).values_list('recipe'))
return Recipe.objects.filter(Q(id__in=related_recipes.values_list('id')) | sub_step_recipes | sub_food_recipes)
class Meta():
indexes = (
GinIndex(fields=["name_search_vector"]),
@@ -644,8 +802,10 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission
class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True)
name = models.CharField(max_length=32, blank=True, default='')
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True) # TODO make required after old shoppinglist deprecated
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
mealplan = models.ForeignKey(MealPlan, on_delete=models.CASCADE, null=True, blank=True)
objects = ScopedManager(space='recipe__space')
@@ -661,20 +821,26 @@ class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), mod
def get_owner(self):
try:
return self.shoppinglist_set.first().created_by
return getattr(self.entries.first(), 'created_by', None) or getattr(self.shoppinglist_set.first(), 'created_by', None)
except AttributeError:
return None
class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin):
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True)
food = models.ForeignKey(Food, on_delete=models.CASCADE)
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True, blank=True)
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True, related_name='entries')
food = models.ForeignKey(Food, on_delete=models.CASCADE, related_name='shopping_entries')
unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True)
ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE, null=True, blank=True)
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
order = models.IntegerField(default=0)
checked = models.BooleanField(default=False)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
delay_until = models.DateTimeField(null=True, blank=True)
objects = ScopedManager(space='shoppinglist__space')
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@staticmethod
def get_space_key():
@@ -686,12 +852,14 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
def __str__(self):
return f'Shopping list entry {self.id}'
# TODO deprecate
def get_shared(self):
return self.shoppinglist_set.first().shared.all()
# TODO deprecate
def get_owner(self):
try:
return self.shoppinglist_set.first().created_by
return self.created_by or self.shoppinglist_set.first().created_by
except AttributeError:
return None

View File

@@ -29,7 +29,11 @@ class Nextcloud(Provider):
client = Nextcloud.get_client(monitor.storage)
files = client.list(monitor.path)
files.pop(0) # remove first element because its the folder itself
try:
files.pop(0) # remove first element because its the folder itself
except IndexError:
pass # folder is empty, no recipes will be imported
import_count = 0
for file in files:

View File

@@ -2,78 +2,29 @@ from rest_framework.schemas.openapi import AutoSchema
from rest_framework.schemas.utils import is_list_view
# TODO move to separate class to cleanup
class RecipeSchema(AutoSchema):
class QueryParam(object):
def __init__(self, name, description=None, qtype='string', required=False):
self.name = name
self.description = description
self.qtype = qtype
self.required = required
def __str__(self):
return f'{self.name}, {self.qtype}, {self.description}'
class QueryParamAutoSchema(AutoSchema):
def get_path_parameters(self, path, method):
if not is_list_view(path, method, self.view):
return super(RecipeSchema, self).get_path_parameters(path, method)
return super().get_path_parameters(path, method)
parameters = super().get_path_parameters(path, method)
parameters.append({
"name": 'query', "in": "query", "required": False,
"description": 'Query string matched (fuzzy) against recipe name. In the future also fulltext search.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'keywords', "in": "query", "required": False,
"description": 'Id of keyword a recipe should have. For multiple repeat parameter.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'foods', "in": "query", "required": False,
"description": 'Id of food a recipe should have. For multiple repeat parameter.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'units', "in": "query", "required": False,
"description": 'Id of unit a recipe should have.',
'schema': {'type': 'int', },
})
parameters.append({
"name": 'rating', "in": "query", "required": False,
"description": 'Id of unit a recipe should have.',
'schema': {'type': 'int', },
})
parameters.append({
"name": 'books', "in": "query", "required": False,
"description": 'Id of book a recipe should have. For multiple repeat parameter.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'steps', "in": "query", "required": False,
"description": 'Id of a step a recipe should have. For multiple repeat parameter.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'keywords_or', "in": "query", "required": False,
"description": 'If recipe should have all (AND) or any (OR) of the provided keywords.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'foods_or', "in": "query", "required": False,
"description": 'If recipe should have all (AND) or any (OR) any of the provided foods.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'books_or', "in": "query", "required": False,
"description": 'If recipe should be in all (AND) or any (OR) any of the provided books.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'internal', "in": "query", "required": False,
"description": 'true or false. If only internal recipes should be returned or not.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'random', "in": "query", "required": False,
"description": 'true or false. returns the results in randomized order.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'new', "in": "query", "required": False,
"description": 'true or false. returns new results first in search results',
'schema': {'type': 'string', },
})
for q in self.view.query_params:
parameters.append({
"name": q.name, "in": "query", "required": q.required,
"description": q.description,
'schema': {'type': q.qtype, },
})
return parameters
@@ -118,15 +69,15 @@ class FilterSchema(AutoSchema):
return parameters
class QueryOnlySchema(AutoSchema):
def get_path_parameters(self, path, method):
if not is_list_view(path, method, self.view):
return super(QueryOnlySchema, self).get_path_parameters(path, method)
# class QueryOnlySchema(AutoSchema):
# def get_path_parameters(self, path, method):
# if not is_list_view(path, method, self.view):
# return super(QueryOnlySchema, self).get_path_parameters(path, method)
parameters = super().get_path_parameters(path, method)
parameters.append({
"name": 'query', "in": "query", "required": False,
"description": 'Query string matched (fuzzy) against object name.',
'schema': {'type': 'string', },
})
return parameters
# parameters = super().get_path_parameters(path, method)
# parameters.append({
# "name": 'query', "in": "query", "required": False,
# "description": 'Query string matched (fuzzy) against object name.',
# 'schema': {'type': 'string', },
# })
# return parameters

View File

@@ -10,21 +10,30 @@ from django.utils import timezone
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
from rest_framework import serializers
from rest_framework.exceptions import NotFound, ValidationError
from rest_framework.fields import empty
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food, ImportLog,
Ingredient, Keyword, MealPlan, MealType, NutritionInformation, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
UserFile, UserPreference, ViewLog)
from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.shopping_helper import list_from_recipe
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food,
FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType,
NutritionInformation, Recipe, RecipeBook, RecipeBookEntry,
RecipeImport, ShareLink, ShoppingList, ShoppingListEntry,
ShoppingListRecipe, Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UserFile,
UserPreference, ViewLog)
from cookbook.templatetags.custom_tags import markdown
from recipes.settings import MEDIA_URL
class ExtendedRecipeMixin(serializers.ModelSerializer):
# adds image and recipe count to serializer when query param extended=1
image = serializers.SerializerMethodField('get_image')
numrecipe = serializers.SerializerMethodField('count_recipes')
# ORM path to this object from Recipe
recipe_filter = None
# list of ORM paths to any image
images = None
image = serializers.SerializerMethodField('get_image')
numrecipe = serializers.ReadOnlyField(source='count_recipes_test')
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
@@ -34,12 +43,9 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
api_serializer = None
# extended values are computationally expensive and not needed in normal circumstances
try:
if bool(int(
self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
return fields
except AttributeError:
pass
except KeyError:
except (AttributeError, KeyError) as e:
pass
try:
del fields['image']
@@ -49,21 +55,8 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
return fields
def get_image(self, obj):
# TODO add caching
recipes = Recipe.objects.filter(**{self.recipe_filter: obj}, space=obj.space).exclude(
image__isnull=True).exclude(image__exact='')
try:
if recipes.count() == 0 and obj.has_children():
obj__in = self.recipe_filter + '__in'
recipes = Recipe.objects.filter(**{obj__in: obj.get_descendants()}, space=obj.space).exclude(
image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
except AttributeError:
# probably not a tree
pass
if recipes.count() != 0:
return random.choice(recipes).image.url
else:
return None
if obj.recipe_image:
return MEDIA_URL + obj.recipe_image
def count_recipes(self, obj):
return Recipe.objects.filter(**{self.recipe_filter: obj}, space=obj.space).count()
@@ -92,9 +85,27 @@ class CustomDecimalField(serializers.Field):
raise ValidationError('A valid number is required')
class CustomOnHandField(serializers.Field):
def get_attribute(self, instance):
return instance
def to_representation(self, obj):
shared_users = None
if request := self.context.get('request', None):
shared_users = getattr(request, '_shared_users', None)
if shared_users is None:
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
return obj.onhand_users.filter(id__in=shared_users).exists()
def to_internal_value(self, data):
return data
class SpaceFilterSerializer(serializers.ListSerializer):
def to_representation(self, data):
if self.context.get('request', None) is None:
return
if (type(data) == QuerySet and data.query.is_sliced):
# if query is sliced it came from api request not nested serializer
return super().to_representation(data)
@@ -136,19 +147,43 @@ class UserNameSerializer(WritableNestedModelSerializer):
fields = ('id', 'username')
class UserPreferenceSerializer(serializers.ModelSerializer):
class FoodInheritFieldSerializer(WritableNestedModelSerializer):
name = serializers.CharField(allow_null=True, allow_blank=True, required=False)
field = serializers.CharField(allow_null=True, allow_blank=True, required=False)
def create(self, validated_data):
if validated_data['user'] != self.context['request'].user:
# don't allow writing to FoodInheritField via API
return FoodInheritField.objects.get(**validated_data)
def update(self, instance, validated_data):
# don't allow writing to FoodInheritField via API
return FoodInheritField.objects.get(**validated_data)
class Meta:
model = FoodInheritField
fields = ('id', 'name', 'field', )
read_only_fields = ['id']
class UserPreferenceSerializer(WritableNestedModelSerializer):
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True)
plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True)
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
def create(self, validated_data):
if not validated_data.get('user', None):
raise ValidationError(_('A user is required'))
if (validated_data['user'] != self.context['request'].user):
raise NotFound()
return super().create(validated_data)
class Meta:
model = UserPreference
fields = (
'user', 'theme', 'nav_color', 'default_unit', 'default_page',
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
'comments'
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj', 'search_style', 'show_recent', 'plan_share',
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_inherit_default', 'default_delay',
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix',
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed'
)
@@ -254,25 +289,11 @@ class KeywordLabelSerializer(serializers.ModelSerializer):
class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
label = serializers.SerializerMethodField('get_label')
# image = serializers.SerializerMethodField('get_image')
# numrecipe = serializers.SerializerMethodField('count_recipes')
recipe_filter = 'keywords'
def get_label(self, obj):
return str(obj)
# def get_image(self, obj):
# recipes = obj.recipe_set.all().filter(space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
# if recipes.count() == 0 and obj.has_children():
# recipes = Recipe.objects.filter(keywords__in=obj.get_descendants(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
# if recipes.count() != 0:
# return random.choice(recipes).image.url
# else:
# return None
# def count_recipes(self, obj):
# return obj.recipe_set.filter(space=self.context['request'].space).all().count()
def create(self, validated_data):
# since multi select tags dont have id's
# duplicate names might be routed to create
@@ -285,26 +306,13 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
model = Keyword
fields = (
'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
'updated_at')
read_only_fields = ('id', 'numchild', 'parent', 'image')
'updated_at', 'full_name')
read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
# image = serializers.SerializerMethodField('get_image')
# numrecipe = serializers.SerializerMethodField('count_recipes')
recipe_filter = 'steps__ingredients__unit'
# def get_image(self, obj):
# recipes = Recipe.objects.filter(steps__ingredients__unit=obj, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
# if recipes.count() != 0:
# return random.choice(recipes).image.url
# else:
# return None
# def count_recipes(self, obj):
# return Recipe.objects.filter(steps__ingredients__unit=obj, space=obj.space).count()
def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
validated_data['space'] = self.context['request'].space
@@ -368,27 +376,16 @@ class RecipeSimpleSerializer(serializers.ModelSerializer):
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
# image = serializers.SerializerMethodField('get_image')
# numrecipe = serializers.SerializerMethodField('count_recipes')
# shopping = serializers.SerializerMethodField('get_shopping_status')
shopping = serializers.ReadOnlyField(source='shopping_status')
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
food_onhand = CustomOnHandField(required=False, allow_null=True)
recipe_filter = 'steps__ingredients__food'
images = ['recipe__image']
# def get_image(self, obj):
# if obj.recipe and obj.space == obj.recipe.space:
# if obj.recipe.image and obj.recipe.image != '':
# return obj.recipe.image.url
# # if food is not also a recipe, look for recipe images that use the food
# recipes = Recipe.objects.filter(steps__ingredients__food=obj, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
# # if no recipes found - check whole tree
# if recipes.count() == 0 and obj.has_children():
# recipes = Recipe.objects.filter(steps__ingredients__food__in=obj.get_descendants(), space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
# if recipes.count() != 0:
# return random.choice(recipes).image.url
# else:
# return None
# def count_recipes(self, obj):
# return Recipe.objects.filter(steps__ingredients__food=obj, space=obj.space).count()
# def get_shopping_status(self, obj):
# return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0
def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
@@ -398,20 +395,43 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
validated_data['supermarket_category'], sc_created = SupermarketCategory.objects.get_or_create(
name=validated_data.pop('supermarket_category')['name'],
space=self.context['request'].space)
onhand = validated_data.pop('food_onhand', None)
# assuming if on hand for user also onhand for shopping_share users
if not onhand is None:
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
if self.instance:
onhand_users = self.instance.onhand_users.all()
else:
onhand_users = []
if onhand:
validated_data['onhand_users'] = list(onhand_users) + shared_users
else:
validated_data['onhand_users'] = list(set(onhand_users) - set(shared_users))
obj, created = Food.objects.get_or_create(**validated_data)
return obj
def update(self, instance, validated_data):
validated_data['name'] = validated_data['name'].strip()
if name := validated_data.get('name', None):
validated_data['name'] = name.strip()
# assuming if on hand for user also onhand for shopping_share users
onhand = validated_data.get('food_onhand', None)
if not onhand is None:
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
if onhand:
validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users
else:
validated_data['onhand_users'] = list(set(self.instance.onhand_users.all()) - set(shared_users))
return super(FoodSerializer, self).update(instance, validated_data)
class Meta:
model = Food
fields = (
'id', 'name', 'description', 'recipe', 'ignore_shopping', 'supermarket_category', 'image', 'parent',
'numchild',
'numrecipe')
read_only_fields = ('id', 'numchild', 'parent', 'image')
'id', 'name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name'
)
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
class IngredientSerializer(WritableNestedModelSerializer):
@@ -456,12 +476,12 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
# check if root type is recipe to prevent infinite recursion
# can be improved later to allow multi level embedding
if obj.step_recipe and type(self.parent.root) == RecipeSerializer:
return StepRecipeSerializer(obj.step_recipe).data
return StepRecipeSerializer(obj.step_recipe, context={'request': self.context['request']}).data
class Meta:
model = Step
fields = (
'id', 'name', 'type', 'instruction', 'ingredients', 'ingredients_markdown',
'id', 'name', 'instruction', 'ingredients', 'ingredients_markdown',
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe', 'step_recipe_data', 'numrecipe'
)
@@ -477,6 +497,10 @@ class StepRecipeSerializer(WritableNestedModelSerializer):
class NutritionInformationSerializer(serializers.ModelSerializer):
carbohydrates = CustomDecimalField()
fats = CustomDecimalField()
proteins = CustomDecimalField()
calories = CustomDecimalField()
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
@@ -500,7 +524,7 @@ class RecipeBaseSerializer(WritableNestedModelSerializer):
def get_recipe_last_cooked(self, obj):
try:
last = obj.cooklog_set.filter(created_by=self.context['request'].user).last()
last = obj.cooklog_set.filter(created_by=self.context['request'].user).order_by('created_at').last()
if last:
return last.created_at
except TypeError:
@@ -509,7 +533,7 @@ class RecipeBaseSerializer(WritableNestedModelSerializer):
# TODO make days of new recipe a setting
def is_recipe_new(self, obj):
if obj.created_at > (timezone.now() - timedelta(days=7)):
if getattr(obj, 'new_recipe', None) or obj.created_at > (timezone.now() - timedelta(days=7)):
return True
else:
return False
@@ -520,6 +544,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
new = serializers.SerializerMethodField('is_recipe_new')
recent = serializers.ReadOnlyField()
def create(self, validated_data):
pass
@@ -532,7 +557,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
fields = (
'id', 'name', 'description', 'image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new'
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
)
read_only_fields = ['image', 'created_by', 'created_at']
@@ -620,52 +645,136 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
meal_type_name = serializers.ReadOnlyField(source='meal_type.name') # TODO deprecate once old meal plan was removed
note_markdown = serializers.SerializerMethodField('get_note_markdown')
servings = CustomDecimalField()
shared = UserNameSerializer(many=True, required=False, allow_null=True)
shopping = serializers.SerializerMethodField('in_shopping')
def get_note_markdown(self, obj):
return markdown(obj.note)
def in_shopping(self, obj):
return ShoppingListRecipe.objects.filter(mealplan=obj.id).exists()
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
mealplan = super().create(validated_data)
if self.context['request'].data.get('addshopping', False):
list_from_recipe(mealplan=mealplan, servings=validated_data['servings'], created_by=validated_data['created_by'], space=validated_data['space'])
return mealplan
class Meta:
model = MealPlan
fields = (
'id', 'title', 'recipe', 'servings', 'note', 'note_markdown',
'date', 'meal_type', 'created_by', 'shared', 'recipe_name',
'meal_type_name'
'meal_type_name', 'shopping'
)
read_only_fields = ('created_by',)
# TODO deprecate
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField('get_name') # should this be done at the front end?
recipe_name = serializers.ReadOnlyField(source='recipe.name')
mealplan_note = serializers.ReadOnlyField(source='mealplan.note')
servings = CustomDecimalField()
def get_name(self, obj):
if not isinstance(value := obj.servings, Decimal):
value = Decimal(value)
value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
return (
obj.name
or getattr(obj.mealplan, 'title', None)
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
or obj.recipe.name
) + f' ({value:.2g})'
def update(self, instance, validated_data):
if 'servings' in validated_data:
list_from_recipe(
list_recipe=instance,
servings=validated_data['servings'],
created_by=self.context['request'].user,
space=self.context['request'].space
)
return super().update(instance, validated_data)
class Meta:
model = ShoppingListRecipe
fields = ('id', 'recipe', 'recipe_name', 'servings')
fields = ('id', 'recipe_name', 'name', 'recipe', 'mealplan', 'servings', 'mealplan_note')
read_only_fields = ('id',)
class ShoppingListEntrySerializer(WritableNestedModelSerializer):
food = FoodSerializer(allow_null=True)
unit = UnitSerializer(allow_null=True, required=False)
ingredient_note = serializers.ReadOnlyField(source='ingredient.note')
recipe_mealplan = ShoppingListRecipeSerializer(source='list_recipe', read_only=True)
amount = CustomDecimalField()
created_by = UserNameSerializer(read_only=True)
completed_at = serializers.DateTimeField(allow_null=True, required=False)
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
# autosync values are only needed for frequent 'checked' value updating
if self.context['request'] and bool(int(self.context['request'].query_params.get('autosync', False))):
for f in list(set(fields) - set(['id', 'checked'])):
del fields[f]
return fields
def run_validation(self, data):
if self.root.instance.__class__.__name__ == 'ShoppingListEntry':
if (
data.get('checked', False)
and self.root.instance
and not self.root.instance.checked
):
# if checked flips from false to true set completed datetime
data['completed_at'] = timezone.now()
elif not data.get('checked', False):
# if not checked set completed to None
data['completed_at'] = None
else:
# otherwise don't write anything
if 'completed_at' in data:
del data['completed_at']
return super().run_validation(data)
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
def update(self, instance, validated_data):
user = self.context['request'].user
# update the onhand for food if shopping_add_onhand is True
if user.userpreference.shopping_add_onhand:
if checked := validated_data.get('checked', None):
instance.food.onhand_users.add(*user.userpreference.shopping_share.all(), user)
elif checked == False:
instance.food.onhand_users.remove(*user.userpreference.shopping_share.all(), user)
return super().update(instance, validated_data)
class Meta:
model = ShoppingListEntry
fields = (
'id', 'list_recipe', 'food', 'unit', 'amount', 'order', 'checked'
'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked', 'recipe_mealplan',
'created_by', 'created_at', 'completed_at', 'delay_until'
)
read_only_fields = ('id', 'created_by', 'created_at',)
# TODO deprecate
class ShoppingListEntryCheckedSerializer(serializers.ModelSerializer):
class Meta:
model = ShoppingListEntry
fields = ('id', 'checked')
# TODO deprecate
class ShoppingListSerializer(WritableNestedModelSerializer):
recipes = ShoppingListRecipeSerializer(many=True, allow_null=True)
entries = ShoppingListEntrySerializer(many=True, allow_null=True)
@@ -686,6 +795,7 @@ class ShoppingListSerializer(WritableNestedModelSerializer):
read_only_fields = ('id', 'created_by',)
# TODO deprecate
class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
entries = ShoppingListEntryCheckedSerializer(many=True, allow_null=True)
@@ -755,7 +865,7 @@ class AutomationSerializer(serializers.ModelSerializer):
# CORS, REST and Scopes aren't currently working
# Scopes are evaluating before REST has authenticated the user assiging a None space
# Scopes are evaluating before REST has authenticated the user assigning a None space
# I've made the change below to fix the bookmarklet, other serializers likely need a similar/better fix
class BookmarkletImportSerializer(serializers.ModelSerializer):
def create(self, validated_data):
@@ -800,7 +910,7 @@ class FoodExportSerializer(FoodSerializer):
class Meta:
model = Food
fields = ('name', 'ignore_shopping', 'supermarket_category')
fields = ('name', 'ignore_shopping', 'supermarket_category',)
class IngredientExportSerializer(WritableNestedModelSerializer):
@@ -826,7 +936,7 @@ class StepExportSerializer(WritableNestedModelSerializer):
class Meta:
model = Step
fields = ('name', 'type', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')
fields = ('name', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')
class RecipeExportSerializer(WritableNestedModelSerializer):
@@ -845,3 +955,24 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
validated_data['created_by'] = self.context['request'].user
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class RecipeShoppingUpdateSerializer(serializers.ModelSerializer):
list_recipe = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("Existing shopping list to update"))
ingredients = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_(
"List of ingredient IDs from the recipe to add, if not provided all ingredients will be added."))
servings = serializers.IntegerField(default=1, write_only=True, allow_null=True, required=False, help_text=_("Providing a list_recipe ID and servings of 0 will delete that shopping list."))
class Meta:
model = Recipe
fields = ['id', 'list_recipe', 'ingredients', 'servings', ]
class FoodShoppingUpdateSerializer(serializers.ModelSerializer):
amount = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("Amount of food to add to the shopping list"))
unit = serializers.IntegerField(write_only=True, allow_null=True, required=False, help_text=_("ID of unit to use for the shopping list"))
delete = serializers.ChoiceField(choices=['true'], write_only=True, allow_null=True, allow_blank=True, help_text=_("When set to true will delete all food from active shopping lists."))
class Meta:
model = Recipe
fields = ['id', 'amount', 'unit', 'delete', ]

View File

@@ -1,47 +1,131 @@
from decimal import Decimal
from functools import wraps
from django.conf import settings
from django.contrib.postgres.search import SearchVector
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import translation
from cookbook.models import Recipe, Step
from cookbook.helper.shopping_helper import list_from_recipe
from cookbook.managers import DICTIONARY
from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe,
ShoppingListEntry, Step)
SQLITE = True
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
SQLITE = False
# wraps a signal with the ability to set 'skip_signal' to avoid creating recursive signals
def skip_signal(signal_func):
@wraps(signal_func)
def _decorator(sender, instance, **kwargs):
if not instance:
return None
if hasattr(instance, 'skip_signal'):
return None
return signal_func(sender, instance, **kwargs)
return _decorator
# TODO there is probably a way to generalize this
@receiver(post_save, sender=Recipe)
@skip_signal
def update_recipe_search_vector(sender, instance=None, created=False, **kwargs):
if not instance:
if SQLITE:
return
# needed to ensure search vector update doesn't trigger recursion
if hasattr(instance, '_dirty'):
return
language = DICTIONARY.get(translation.get_language(), 'simple')
instance.name_search_vector = SearchVector('name__unaccent', weight='A', config=language)
instance.desc_search_vector = SearchVector('description__unaccent', weight='C', config=language)
try:
instance._dirty = True
instance.skip_signal = True
instance.save()
finally:
del instance._dirty
del instance.skip_signal
@receiver(post_save, sender=Step)
@skip_signal
def update_step_search_vector(sender, instance=None, created=False, **kwargs):
if SQLITE:
return
language = DICTIONARY.get(translation.get_language(), 'simple')
instance.search_vector = SearchVector('instruction__unaccent', weight='B', config=language)
try:
instance.skip_signal = True
instance.save()
finally:
del instance.skip_signal
@receiver(post_save, sender=Food)
@skip_signal
def update_food_inheritance(sender, instance=None, created=False, **kwargs):
if not instance:
return
# needed to ensure search vector update doesn't trigger recursion
if hasattr(instance, '_dirty'):
inherit = instance.inherit_fields.all()
# nothing to apply from parent and nothing to apply to children
if (not instance.parent or inherit.count() == 0) and instance.numchild == 0:
return
language = DICTIONARY.get(translation.get_language(), 'simple')
instance.search_vector = SearchVector('instruction__unaccent', weight='B', config=language)
inherit = inherit.values_list('field', flat=True)
# apply changes from parent to instance for each inherited field
if instance.parent and inherit.count() > 0:
parent = instance.get_parent()
if 'ignore_shopping' in inherit:
instance.ignore_shopping = parent.ignore_shopping
# if supermarket_category is not set, do not cascade - if this becomes non-intuitive can change
if 'supermarket_category' in inherit and parent.supermarket_category:
instance.supermarket_category = parent.supermarket_category
try:
instance.skip_signal = True
instance.save()
finally:
del instance.skip_signal
try:
instance._dirty = True
instance.save()
finally:
del instance._dirty
# TODO figure out how to generalize this
# apply changes to direct children - depend on save signals for those objects to cascade inheritance down
_save = []
for child in instance.get_children().filter(inherit_fields__field='ignore_shopping'):
child.ignore_shopping = instance.ignore_shopping
_save.append(child)
# don't cascade empty supermarket category
if instance.supermarket_category:
# apply changes to direct children - depend on save signals for those objects to cascade inheritance down
for child in instance.get_children().filter(inherit_fields__field='supermarket_category'):
child.supermarket_category = instance.supermarket_category
_save.append(child)
for child in set(_save):
child.save()
@receiver(post_save, sender=MealPlan)
def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs):
user = instance.get_owner()
if not user.userpreference.mealplan_autoadd_shopping:
return
if not created and instance.shoppinglistrecipe_set.exists():
for x in instance.shoppinglistrecipe_set.all():
if instance.servings != x.servings:
list_recipe = list_from_recipe(list_recipe=x, servings=instance.servings, space=instance.space)
elif created:
# if creating a mealplan - perform shopping list activities
kwargs = {
'mealplan': instance,
'space': instance.space,
'created_by': user,
'servings': instance.servings
}
list_recipe = list_from_recipe(**kwargs)
# user = self.context['request'].user
# if user.userpreference.shopping_add_onhand:
# if checked := validated_data.get('checked', None):
# instance.food.onhand_users.add(*user.userpreference.shopping_share.all(), user)
# elif checked == False:
# instance.food.onhand_users.remove(*user.userpreference.shopping_share.all(), user)

File diff suppressed because one or more lines are too long

View File

@@ -10461,4 +10461,9 @@ textarea, input:not([type="submit"]):not([class="multiselect__input"]):not([clas
.form-control-search {
font-size: 20px;
}
.ghost {
opacity: 0.5 !important;
background: #b98766 !important;
}

View File

@@ -19,7 +19,7 @@
<div class="row">
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
<hr>
<hr>
<form class="login" method="POST" action="{% url 'account_login' %}">
{% csrf_token %}
{{ form | crispy }}
@@ -29,12 +29,16 @@
{% endif %}
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
<a class="btn btn-secondary" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
{% if SIGNUP_ENABLED %}
<a class="btn btn-secondary" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
{% endif %}
{% if EMAIL_ENABLED %}
<a class="btn btn-warning float-right d-none d-xl-block d-lg-block"
href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a>
<p class="d-xl-none d-lg-none">{% trans 'Lost your password?' %} <a href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a></p>
<p class="d-xl-none d-lg-none">{% trans 'Lost your password?' %} <a
href="{% url 'account_reset_password' %}">{% trans "Reset My Password" %}</a></p>
{% endif %}
</form>
</div>
@@ -44,7 +48,7 @@
{% if socialaccount_providers %}
<div class="row" style="margin-top: 2vh">
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
<h5>{% trans "Social Login" %}</h5>
<span>{% trans 'You can use any of the following providers to sign in.' %}</span>
@@ -62,5 +66,8 @@
</div>
{% endif %}
<script>
$('#id_login').focus()
</script>
{% endblock %}

View File

@@ -71,4 +71,8 @@
</div>
<script>
$('#id_username').focus()
</script>
{% endblock %}

View File

@@ -67,7 +67,7 @@
</button>
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="/" aria-label="Tandoor">
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}" aria-label="Tandoor">
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
</a>
{% endif %}
@@ -122,7 +122,7 @@
<i class="fas fa-leaf fa-2x"></i>
</div>
<div class="card-body text-break text-center p-0 no-gutters text-muted">
{% trans 'Ingredients' %}
{% trans 'Foods' %}
</div>
</div>
</a>
@@ -336,6 +336,9 @@
{% block content_fluid %}
{% endblock %}
{% user_prefs request as prefs%}
{{ prefs|json_script:'user_preference' }}
</div>
{% block script %}
@@ -345,6 +348,7 @@
localStorage.setItem('SCRIPT_NAME', "{% base_path request 'script' %}")
localStorage.setItem('BASE_PATH', "{% base_path request 'base' %}")
localStorage.setItem('STATIC_URL', "{% base_path request 'static_base' %}")
localStorage.setItem('DEBUG', "{% is_debug %}")
window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("{% url 'service_worker' %}", {scope: "{% base_path request 'base' %}" + '/'}).then(function (reg) {

View File

@@ -1,32 +0,0 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% comment %} {% load l10n %} {% endcomment %}
{% block title %}{{ title }}{% endblock %}
{% block content_fluid %}
<div id="app" >
<checklist-view></checklist-view>
</div>
{% endblock %}
{% block script %}
{{ config | json_script:"model_config" }}
{% if debug %}
<script src="{% url 'js_reverse' %}"></script>
{% else %}
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
{% endif %}
<script type="application/javascript">
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
</script>
{% render_bundle 'checklist_view' %}
{% endblock %}

View File

@@ -18,12 +18,23 @@
{% endif %}
<div class="table-container">
<h3 style="margin-bottom: 2vh">{{ title }} {% trans 'List' %}
{% if create_url %}
<a href="{% url create_url %}"> <i class="fas fa-plus-circle"></i>
<span class="col col-md-9">
<h3 style="margin-bottom: 2vh">{{ title }} {% trans 'List' %}
{% if create_url %}
<a href="{% url create_url %}"> <i class="fas fa-plus-circle"></i>
</a>
{% endif %}
</h3>
</span>
{% if request.resolver_match.url_name in 'list_shopping_list' %}
<span class="col-md-3">
<a href="{% url 'view_shopping_new' %}" class="float-right">
<button class="btn btn-outline-secondary shadow-none">
<i class="fas fa-star"></i> {% trans 'Try the new shopping list' %}
</button>
</a>
{% endif %}
</h3>
</span>
{% endif %}
{% if filter %}
<br/>

View File

@@ -1,4 +1,5 @@
{% load i18n %}
{% comment %} TODO: Deprecate {% endcomment %}
<div class="modal" tabindex="-1" role="dialog" id="id_modal_cook_log">
<div class="modal-dialog" role="document">
@@ -77,4 +78,4 @@
$('#id_rating_show').html(rating.val() + '/5')
});
</script>
</script>

View File

@@ -1,48 +1,48 @@
{
"name": "Tandoor Recipes",
"short_name" : "Tandoor",
"description": "Application to manage, tag and search recipes.",
"icons": [
{
"src": "/static/assets/logo_color144.png",
"type": "image/png",
"sizes": "144x144"
},
{
"src": "/static/assets/logo_color512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "/search",
"background_color": "#ffcb76",
"display": "standalone",
"scope": "/",
"theme_color": "#ffcb76",
"shortcuts": [
{
"name": "Plan",
"short_name": "Plan",
"description": "View your meal Plan",
"url": "/plan"
},
{
"name": "Books",
"short_name": "Cookbooks",
"description": "View your cookbooks",
"url": "/books"
},
{
"name": "Shopping",
"short_name": "Shopping",
"description": "View your shopping lists",
"url": "/list/shopping-list/"
},
{
"name": "Latest Shopping List",
"short_name": "Shopping List",
"description": "View the latest shopping list",
"url": "/shopping/latest/"
}
]
"name": "Tandoor Recipes",
"short_name": "Tandoor",
"description": "Application to manage, tag and search recipes.",
"icons": [
{
"src": "/static/assets/logo_color144.png",
"type": "image/png",
"sizes": "144x144"
},
{
"src": "/static/assets/logo_color512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "./search",
"background_color": "#ffcb76",
"display": "standalone",
"scope": ".",
"theme_color": "#ffcb76",
"shortcuts": [
{
"name": "Plan",
"short_name": "Plan",
"description": "View your meal Plan",
"url": "./plan"
},
{
"name": "Books",
"short_name": "Cookbooks",
"description": "View your cookbooks",
"url": "./books"
},
{
"name": "Shopping",
"short_name": "Shopping",
"description": "View your shopping lists",
"url": "./list/shopping-list/"
},
{
"name": "Latest Shopping List",
"short_name": "Shopping List",
"description": "View the latest shopping list",
"url": "./shopping/latest/"
}
]
}

View File

@@ -54,7 +54,7 @@
<h2>{% trans 'Formatting' %}</h2>
<pre class="intro-code code-block"><code>
{% trans 'Line breaks are inserted by adding two spaces after the end of a line' %}
{% trans 'or by leaving a blank line inbetween.' %}
{% trans 'or by leaving a blank line in between.' %}
**{% trans 'This text is bold' %}**
*{% trans 'This text is italic' %}*
@@ -70,7 +70,7 @@
<div class="card">
<div class="card-body">
{% trans 'Line breaks are inserted by adding two spaces after the end of a line' %}<br/>
{% trans 'or by leaving a blank line inbetween.' %}<br/><br/>
{% trans 'or by leaving a blank line in between.' %}<br/><br/>
<b>{% trans 'This text is bold' %}</b><br/>
<i>{% trans 'This text is italic' %}</i>
<blockquote>
@@ -82,7 +82,7 @@
<br/>
<h2>{% trans 'Lists' %}</h2>
{% trans 'Lists can ordered or unorderd. It is <b>important to leave a blank line before the list!</b>' %}
{% trans 'Lists can ordered or unordered. It is <b>important to leave a blank line before the list!</b>' %}
<pre class="intro-code code-block"><code>
{% trans 'Ordered List' %}

View File

@@ -27,7 +27,7 @@
{% endblocktrans %}</p>
<h4>{% trans 'Simple' %}</h4>
<p> {% blocktrans %}
Simple searches ignore punctuation and common words such as 'the', 'a', 'and'. And will treat seperate words as required.
Simple searches ignore punctuation and common words such as 'the', 'a', 'and'. And will treat separate words as required.
Searching for 'apple or flour' will return any recipe that includes both 'apple' and 'flour' anywhere in the fields that have been selected for a full text search.
{% endblocktrans %}</p>
<h4>{% trans 'Phrase' %}</h4>
@@ -39,7 +39,7 @@
<p> {% blocktrans %}
Web searches simulate functionality found on many web search sites supporting special syntax.
Placing quotes around several words will convert those words into a phrase.
'or' is recongized as searching for the word (or phrase) immediately before 'or' OR the word (or phrase) directly after.
'or' is recognized as searching for the word (or phrase) immediately before 'or' OR the word (or phrase) directly after.
'-' is recognized as searching for recipes that do not include the word (or phrase) that comes immediately after.
For example searching for 'apple pie' or cherry -butter will return any recipe that includes the phrase 'apple pie' or the word 'cherry'
in any field included in the full text search but exclude any recipe that has the word 'butter' in any field included.
@@ -59,7 +59,7 @@
{% blocktrans %}
Another approach to searching that also requires Postgresql is fuzzy search or trigram similarity. A trigram is a group of three consecutive characters.
For example searching for 'apple' will create x trigrams 'app', 'ppl', 'ple' and will create a score of how closely words match the generated trigrams.
One benefit of searching trigams is that a search for 'sandwich' will find mispelled words such as 'sandwhich' that would be missed by other methods.
One benefit of searching trigams is that a search for 'sandwich' will find misspelled words such as 'sandwhich' that would be missed by other methods.
{% endblocktrans %}
</div>

View File

@@ -28,10 +28,10 @@
{% trans 'Account' %}</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link {% if active_tab == 'prefernces' %} active {% endif %}" id="preferences-tab"
<a class="nav-link {% if active_tab == 'preferences' %} active {% endif %}" id="preferences-tab"
data-toggle="tab" href="#preferences" role="tab"
aria-controls="preferences"
aria-selected="{% if active_tab == 'prefernces' %} 'true' {% else %} 'false' {% endif %}">
aria-selected="{% if active_tab == 'preferences' %} 'true' {% else %} 'false' {% endif %}">
{% trans 'Preferences' %}</a>
</li>
<li class="nav-item" role="presentation">
@@ -48,6 +48,13 @@
aria-selected="{% if active_tab == 'search' %} 'true' {% else %} 'false' {% endif %}">
{% trans 'Search-Settings' %}</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link {% if active_tab == 'shopping' %} active {% endif %}" id="shopping-tab" data-toggle="tab"
href="#shopping" role="tab"
aria-controls="search"
aria-selected="{% if active_tab == 'shopping' %} 'true' {% else %} 'false' {% endif %}">
{% trans 'Shopping-Settings' %}</a>
</li>
</ul>
@@ -195,6 +202,17 @@
class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
</div>
<div class="tab-pane {% if active_tab == 'shopping' %} active {% endif %}" id="shopping" role="tabpanel"
aria-labelledby="shopping-tab">
<h4>{% trans 'Shopping Settings' %}</h4>
<form action="./#shopping" method="post" id="id_shopping_form">
{% csrf_token %}
{{ shopping_form|crispy }}
<button class="btn btn-success" type="submit" name="shopping_form" id="shopping_form_button"><i
class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
</div>
</div>
@@ -224,5 +242,26 @@
$('.nav-tabs a').on('shown.bs.tab', function (e) {
window.location.hash = e.target.hash;
})
// listen for events
{% comment %} $(document).ready(function(){
hideShow()
// call hideShow when the user clicks on the mealplan_autoadd checkbox
$("#id_shopping-mealplan_autoadd_shopping").click(function(event){
hideShow()
});
})
function hideShow(){
if(document.getElementById('id_shopping-mealplan_autoadd_shopping').checked == true)
{
$('#div_id_shopping-mealplan_autoexclude_onhand').show();
$('#div_id_shopping-mealplan_autoinclude_related').show();
}
else
{
$('#div_id_shopping-mealplan_autoexclude_onhand').hide();
$('#div_id_shopping-mealplan_autoinclude_related').hide();
} {% endcomment %}
}
</script>
{% endblock %}
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
{% extends "base.html" %} {% load render_bundle from webpack_loader %} {% load static %} {% load i18n %} {% block title %} {{ title }} {% endblock %} {% block content_fluid %}
<div id="app">
<shopping-list-view></shopping-list-view>
</div>
{% endblock %} {% block script %} {% if debug %}
<script src="{% url 'js_reverse' %}"></script>
{% else %}
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
{% endif %}
<script type="application/javascript">
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
</script>
{% render_bundle 'shopping_list_view' %} {% endblock %}

View File

@@ -1,165 +1,188 @@
{% extends "base.html" %}
{% load django_tables2 %}
{% load crispy_forms_tags %}
{% load crispy_forms_filters %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Space Settings" %}{% endblock %}
{%block title %} {% trans "Space Settings" %} {% endblock %}
{% block extra_head %}
{{ form.media }}
{{ space_form.media }}
{% include 'include/vue_base.html' %}
{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'view_space' %}">{% trans 'Space Settings' %}</a></li>
</ol>
</nav>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'view_space' %}">{% trans 'Space Settings' %}</a></li>
</ol>
</nav>
<h3><span class="text-muted">{% trans 'Space:' %}</span> {{ request.space.name }} <small>{% if HOSTED %}
<a href="https://tandoor.dev/manage">{% trans 'Manage Subscription' %}</a>{% endif %}</small></h3>
<h3>
<span class="text-muted">{% trans 'Space:' %}</span> {{ request.space.name }}
<small>{% if HOSTED %} <a href="https://tandoor.dev/manage">{% trans 'Manage Subscription' %}</a>{% endif %}</small>
</h3>
<br/>
<br />
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
{% trans 'Number of objects' %}
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">{% trans 'Recipes' %} : <span
class="badge badge-pill badge-info">{{ counts.recipes }} /
{% if request.space.max_recipes > 0 %}
{{ request.space.max_recipes }}{% else %}∞{% endif %}</span></li>
<li class="list-group-item">{% trans 'Keywords' %} : <span
class="badge badge-pill badge-info">{{ counts.keywords }}</span></li>
<li class="list-group-item">{% trans 'Units' %} : <span
class="badge badge-pill badge-info">{{ counts.units }}</span></li>
<li class="list-group-item">{% trans 'Ingredients' %} : <span
class="badge badge-pill badge-info">{{ counts.ingredients }}</span></li>
<li class="list-group-item">{% trans 'Recipe Imports' %} : <span
class="badge badge-pill badge-info">{{ counts.recipe_import }}</span></li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
{% trans 'Objects stats' %}
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">{% trans 'Recipes without Keywords' %} : <span
class="badge badge-pill badge-info">{{ counts.recipes_no_keyword }}</span></li>
<li class="list-group-item">{% trans 'External Recipes' %} : <span
class="badge badge-pill badge-info">{{ counts.recipes_external }}</span></li>
<li class="list-group-item">{% trans 'Internal Recipes' %} : <span
class="badge badge-pill badge-info">{{ counts.recipes_internal }}</span></li>
<li class="list-group-item">{% trans 'Comments' %} : <span
class="badge badge-pill badge-info">{{ counts.comments }}</span></li>
</ul>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">{% trans 'Number of objects' %}</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
{% trans 'Recipes' %} :
<span class="badge badge-pill badge-info"
>{{ counts.recipes }} / {% if request.space.max_recipes > 0 %} {{ request.space.max_recipes }}{%
else %}∞{% endif %}</span
>
</li>
<li class="list-group-item">
{% trans 'Keywords' %} : <span class="badge badge-pill badge-info">{{ counts.keywords }}</span>
</li>
<li class="list-group-item">
{% trans 'Units' %} : <span class="badge badge-pill badge-info">{{ counts.units }}</span>
</li>
<li class="list-group-item">
{% trans 'Ingredients' %} :
<span class="badge badge-pill badge-info">{{ counts.ingredients }}</span>
</li>
<li class="list-group-item">
{% trans 'Recipe Imports' %} :
<span class="badge badge-pill badge-info">{{ counts.recipe_import }}</span>
</li>
</ul>
</div>
</div>
<br/>
<br/>
<div class="row">
<div class="col col-md-12">
<h4>{% trans 'Members' %} <small class="text-muted">{{ space_users|length }}/
{% if request.space.max_users > 0 %}
{{ request.space.max_users }}{% else %}∞{% endif %}</small>
<a class="btn btn-success float-right" href="{% url 'new_invite_link' %}"><i
class="fas fa-plus-circle"></i> {% trans 'Invite User' %}</a>
</h4>
<div class="col-md-6">
<div class="card">
<div class="card-header">{% trans 'Objects stats' %}</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">
{% trans 'Recipes without Keywords' %} :
<span class="badge badge-pill badge-info">{{ counts.recipes_no_keyword }}</span>
</li>
<li class="list-group-item">
{% trans 'External Recipes' %} :
<span class="badge badge-pill badge-info">{{ counts.recipes_external }}</span>
</li>
<li class="list-group-item">
{% trans 'Internal Recipes' %} :
<span class="badge badge-pill badge-info">{{ counts.recipes_internal }}</span>
</li>
<li class="list-group-item">
{% trans 'Comments' %} : <span class="badge badge-pill badge-info">{{ counts.comments }}</span>
</li>
</ul>
</div>
</div>
<br>
<div class="row">
<div class="col col-md-12">
{% if space_users %}
<table class="table table-bordered">
<tr>
<th>{% trans 'User' %}</th>
<th>{% trans 'Groups' %}</th>
<th>{% trans 'Edit' %}</th>
</tr>
{% for u in space_users %}
<tr>
<td>
{{ u.user.username }}
</td>
<td>
{{ u.user.groups.all |join:", " }}
</td>
<td>
{% if u.user != request.user %}
<div class="input-group mb-3">
<select v-model="users['{{ u.pk }}']" class="custom-select form-control"
style="height: 44px">
<option value="admin">{% trans 'admin' %}</option>
<option value="user">{% trans 'user' %}</option>
<option value="guest">{% trans 'guest' %}</option>
<option value="remove">{% trans 'remove' %}</option>
</select>
<span class="input-group-append">
<a class="btn btn-warning"
:href="editUserUrl({{ u.pk }}, {{ u.space.pk }})">{% trans 'Update' %}</a>
</span>
</div>
{% else %}
{% trans 'You cannot edit yourself.' %}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>{% trans 'There are no members in your space yet!' %}</p>
{% endif %}
</div>
</div>
<br />
<br />
<form action="." method="post">{% csrf_token %} {{ user_name_form|crispy }}</form>
<div class="row">
<div class="col col-md-12">
<h4>
{% trans 'Members' %}
<small class="text-muted"
>{{ space_users|length }}/ {% if request.space.max_users > 0 %} {{ request.space.max_users }}{% else
%}∞{% endif %}</small
>
<a class="btn btn-success float-right" href="{% url 'new_invite_link' %}"
><i class="fas fa-plus-circle"></i> {% trans 'Invite User' %}</a
>
</h4>
</div>
</div>
<br />
<div class="row">
<div class="col col-md-12">
<h4>{% trans 'Invite Links' %}</h4>
{% render_table invite_links %}
</div>
<div class="row">
<div class="col col-md-12">
{% if space_users %}
<table class="table table-bordered">
<tr>
<th>{% trans 'User' %}</th>
<th>{% trans 'Groups' %}</th>
<th>{% trans 'Edit' %}</th>
</tr>
{% for u in space_users %}
<tr>
<td>{{ u.user.username }}</td>
<td>{{ u.user.groups.all |join:", " }}</td>
<td>
{% if u.user != request.user %}
<div class="input-group mb-3">
<select v-model="users['{{ u.pk }}']" class="custom-select form-control" style="height: 44px">
<option value="admin">{% trans 'admin' %}</option>
<option value="user">{% trans 'user' %}</option>
<option value="guest">{% trans 'guest' %}</option>
<option value="remove">{% trans 'remove' %}</option>
</select>
<span class="input-group-append">
<a class="btn btn-warning" :href="editUserUrl({{ u.pk }}, {{ u.space.pk }})"
>{% trans 'Update' %}</a
>
</span>
</div>
{% else %} {% trans 'You cannot edit yourself.' %} {% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>{% trans 'There are no members in your space yet!' %}</p>
{% endif %}
</div>
</div>
<br/>
<br/>
<br/>
<div class="row">
<div class="col col-md-12">
<h4>{% trans 'Invite Links' %}</h4>
{% render_table invite_links %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<h4>{% trans 'Space Settings' %}</h4>
<form action="." method="post">
{% csrf_token %}
{{ space_form|crispy }}
<button class="btn btn-success" type="submit" name="space_form"><i
class="fas fa-save"></i> {% trans 'Save' %}</button>
</form>
</div>
</div>
<br />
<br />
<br />
{% endblock %} {% block script %}
<script type="application/javascript">
let app = new Vue({
delimiters: ['[[', ']]'],
el: '#id_base_container',
data: {
users: {
{% for u in space_users %}
'{{ u.pk }}': 'none',
{% endfor %}
}
},
mounted: function () {
},
methods: {
editUserUrl: function (user_id, space_id) {
return '{% url 'change_space_member' 1234 5678 'role' %}'.replace('1234', user_id).replace('5678', space_id).replace('role', this.users[user_id])
}
}
});
</script>
{% endblock %}
{% block script %}
<script type="application/javascript">
let app = new Vue({
delimiters: ['[[', ']]'],
el: '#id_base_container',
data: {
users: {
{% for u in space_users %}
'{{ u.pk }}': 'none',
{% endfor %}
}
},
mounted: function () {
},
methods: {
editUserUrl: function (user_id, space_id) {
return '{% url 'change_space_member' 1234 5678 'role' %}'.replace('1234', user_id).replace('5678', space_id).replace('role', this.users[user_id])
}
}
});
</script>
{% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% comment %} TODO: refactor to be Vue app {% endcomment %}
{% load i18n %}
{% load static %}
{% load custom_tags %}
@@ -75,6 +76,7 @@
<option value="CHEFTAP">Cheftap</option>
<option value="CHOWDOWN">Chowdown</option>
<option value="COOKBOOKAPP">CookBookApp</option>
<option value="COPYMETHAT">CopyMeThat</option>
<option value="DOMESTICA">Domestica</option>
<option value="MEALIE">Mealie</option>
<option value="MEALMASTER">Mealmaster</option>
@@ -496,6 +498,8 @@
:clear-on-select="true"
:allow-empty="true"
:preserve-search="true"
:internal-search="false"
:limit="options_limit"
placeholder="{% trans 'Select one' %}"
tag-placeholder="{% trans 'Select' %}"
label="text"
@@ -534,6 +538,8 @@
:clear-on-select="true"
:allow-empty="false"
:preserve-search="true"
:internal-search="false"
:limit="options_limit"
label="text"
track-by="id"
:multiple="false"
@@ -584,6 +590,8 @@
:clear-on-select="true"
:hide-selected="true"
:preserve-search="true"
:internal-search="false"
:limit="options_limit"
placeholder="{% trans 'Select one' %}"
tag-placeholder="{% trans 'Add Keyword' %}"
:taggable="true"
@@ -652,7 +660,8 @@
</div>
<script src="{% url 'javascript-catalog' %}"></script>
<script src="{% url 'javascript-catalog' %}">
</script>
<script type="application/javascript">
let csrftoken = Cookies.get('csrftoken');
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
@@ -691,7 +700,8 @@
import_duplicates: false,
recipe_files: [],
images: [],
mode: 'url'
mode: 'url',
options_limit:25
},
directives: {
tabindex: {
@@ -701,9 +711,9 @@
}
},
mounted: function () {
this.searchKeywords('')
this.searchUnits('')
this.searchIngredients('')
// this.searchKeywords('')
// this.searchUnits('')
// this.searchIngredients('')
let uri = window.location.search.substring(1);
let params = new URLSearchParams(uri);
q = params.get("id")
@@ -713,7 +723,6 @@
},
methods: {
makeToast: function (title, message, variant = null) {
//TODO remove duplicate function in favor of central one
this.$bvToast.toast(message, {
title: title,
variant: variant,
@@ -883,7 +892,20 @@
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
})
// let apiFactory = new ApiApiFactory()
// this.keywords_loading = true
// apiFactory
// .listKeywords(query, undefined, undefined, 1, this.options_limit)
// .then((response) => {
// this.keywords = response.data.results
// this.keywords_loading = false
// })
// .catch((err) => {
// console.log(err)
// StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
// })
},
searchUnits: function (query) {
this.units_loading = true
@@ -921,6 +943,29 @@
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
// let apiFactory = new ApiApiFactory()
// this.foods_loading = true
// apiFactory
// .listFoods(query, undefined, undefined, 1, this.options_limit)
// .then((response) => {
// this.foods = response.data.results
// if (this.recipe !== undefined) {
// for (let s of this.recipe.steps) {
// for (let i of s.ingredients) {
// if (i.food !== null && i.food.id === undefined) {
// this.foods.push(i.food)
// }
// }
// }
// }
// this.foods_loading = false
// })
// .catch((err) => {
// StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
// })
},
deleteNode: function (node, item, e) {
e.stopPropagation()

View File

@@ -1,17 +1,20 @@
import re
from gettext import gettext as _
import bleach
import markdown as md
import re
from markdown.extensions.tables import TableExtension
from bleach_allowlist import markdown_attrs, markdown_tags
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.helper.mdx_urlize import UrlizeExtension
from cookbook.models import Space, get_model_name
from django import template
from django.db.models import Avg
from django.templatetags.static import static
from django.urls import NoReverseMatch, reverse
from recipes import settings
from rest_framework.authtoken.models import Token
from gettext import gettext as _
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.helper.mdx_urlize import UrlizeExtension
from cookbook.models import Space, get_model_name
from recipes import settings
register = template.Library()
@@ -47,7 +50,7 @@ def markdown(value):
parsed_md = md.markdown(
value,
extensions=[
'markdown.extensions.fenced_code', 'tables',
'markdown.extensions.fenced_code', TableExtension(),
UrlizeExtension(), MarkdownFormatExtension()
]
)
@@ -124,10 +127,10 @@ def markdown_link():
@register.simple_tag
def bookmarklet(request):
if request.is_secure():
prefix = "https://"
protocol = "https://"
else:
prefix = "http://"
server = prefix + request.get_host()
protocol = "http://"
server = protocol + request.get_host()
prefix = settings.JS_REVERSE_SCRIPT_PREFIX
# TODO is it safe to store the token in clear text in a bookmark?
if (api_token := Token.objects.filter(user=request.user).first()) is None:
@@ -155,3 +158,13 @@ def base_path(request, path_type):
return request.META.get('HTTP_X_SCRIPT_NAME', '')
elif path_type == 'static_base':
return static('vue/manifest.json').replace('vue/manifest.json', '')
@register.simple_tag
def user_prefs(request):
from cookbook.serializer import \
UserPreferenceSerializer # putting it with imports caused circular execution
try:
return UserPreferenceSerializer(request.user.userpreference, context={'request': request}).data
except AttributeError:
pass

View File

@@ -1,12 +1,14 @@
import json
import pytest
from django.contrib import auth
from django_scopes import scopes_disabled
from django.urls import reverse
from django_scopes import scope, scopes_disabled
from pytest_factoryboy import LazyFixture, register
from cookbook.models import Food, Ingredient, ShoppingList, ShoppingListEntry
from cookbook.models import Food, FoodInheritField, Ingredient, ShoppingList, ShoppingListEntry
from cookbook.tests.factories import (FoodFactory, IngredientFactory, ShoppingListEntryFactory,
SupermarketCategoryFactory)
# ------------------ IMPORTANT -------------------
#
@@ -27,78 +29,50 @@ else:
node_location = 'last-child'
@pytest.fixture()
def obj_1(space_1):
return Food.objects.get_or_create(name='test_1', space=space_1)[0]
register(FoodFactory, 'obj_1', space=LazyFixture('space_1'))
register(FoodFactory, 'obj_2', space=LazyFixture('space_1'))
register(FoodFactory, 'obj_3', space=LazyFixture('space_2'))
register(SupermarketCategoryFactory, 'cat_1', space=LazyFixture('space_1'))
@pytest.fixture()
def obj_1_1(obj_1, space_1):
return obj_1.add_child(name='test_1_1', space=space_1)
@pytest.fixture()
def obj_1_1_1(obj_1_1, space_1):
return obj_1_1.add_child(name='test_1_1_1', space=space_1)
# @pytest.fixture
# def true():
# return True
@pytest.fixture
def obj_2(space_1):
return Food.objects.get_or_create(name='test_2', space=space_1)[0]
def false():
return False
@pytest.fixture
def non_exist():
return {}
@pytest.fixture()
def obj_3(space_2):
return Food.objects.get_or_create(name='test_3', space=space_2)[0]
def obj_tree_1(request, space_1):
try:
params = request.param # request.param is a magic variable
except AttributeError:
params = {}
objs = []
inherit = params.pop('inherit', False)
objs.extend(FoodFactory.create_batch(3, space=space_1, **params))
# set all foods to inherit everything
if inherit:
inherit = Food.inheritable_fields
Through = Food.objects.filter(space=space_1).first().inherit_fields.through
for i in inherit:
Through.objects.bulk_create([
Through(food_id=x, foodinheritfield_id=i.id)
for x in Food.objects.filter(space=space_1).values_list('id', flat=True)
])
@pytest.fixture()
def ing_1_s1(obj_1, space_1):
return Ingredient.objects.create(food=obj_1, space=space_1)
@pytest.fixture()
def ing_2_s1(obj_2, space_1):
return Ingredient.objects.create(food=obj_2, space=space_1)
@pytest.fixture()
def ing_3_s2(obj_3, space_2):
return Ingredient.objects.create(food=obj_3, space=space_2)
@pytest.fixture()
def ing_1_1_s1(obj_1_1, space_1):
return Ingredient.objects.create(food=obj_1_1, space=space_1)
@pytest.fixture()
def sle_1_s1(obj_1, u1_s1, space_1):
e = ShoppingListEntry.objects.create(food=obj_1)
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.entries.add(e)
return e
@pytest.fixture()
def sle_2_s1(obj_2, u1_s1, space_1):
return ShoppingListEntry.objects.create(food=obj_2)
@pytest.fixture()
def sle_3_s2(obj_3, u1_s2, space_2):
e = ShoppingListEntry.objects.create(food=obj_3)
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s2), space=space_2, )
s.entries.add(e)
return e
@pytest.fixture()
def sle_1_1_s1(obj_1_1, u1_s1, space_1):
e = ShoppingListEntry.objects.create(food=obj_1_1)
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.entries.add(e)
return e
objs[0].move(objs[1], node_location)
objs[1].move(objs[2], node_location)
return Food.objects.get(id=objs[1].id) # whenever you move/merge a tree it's safest to re-get the object
@pytest.mark.parametrize("arg", [
@@ -128,7 +102,10 @@ def test_list_filter(obj_1, obj_2, u1_s1):
assert r.status_code == 200
response = json.loads(r.content)
assert response['count'] == 2
assert response['results'][0]['name'] == obj_1.name
assert obj_1.name in [x['name'] for x in response['results']]
assert obj_2.name in [x['name'] for x in response['results']]
assert response['results'][0]['name'] < response['results'][1]['name']
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?page_size=1').content)
assert len(response['results']) == 1
@@ -142,7 +119,7 @@ def test_list_filter(obj_1, obj_2, u1_s1):
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query=chicken').content)
assert response['count'] == 0
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[4:]}').content)
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?query={obj_1.name[:-4]}').content)
assert response['count'] == 1
@@ -194,7 +171,6 @@ def test_add(arg, request, u1_s2):
assert r.status_code == 404
@pytest.mark.django_db(transaction=True)
def test_add_duplicate(u1_s1, u1_s2, obj_1, obj_3):
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 1
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 1
@@ -220,9 +196,9 @@ def test_add_duplicate(u1_s1, u1_s2, obj_1, obj_3):
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 2
def test_delete(u1_s1, u1_s2, obj_1, obj_1_1, obj_1_1_1):
def test_delete(u1_s1, u1_s2, obj_1, obj_tree_1):
with scopes_disabled():
assert Food.objects.count() == 3
assert Food.objects.count() == 4
r = u1_s2.delete(
reverse(
@@ -232,18 +208,19 @@ def test_delete(u1_s1, u1_s2, obj_1, obj_1_1, obj_1_1_1):
)
assert r.status_code == 404
with scopes_disabled():
assert Food.objects.count() == 3
assert Food.objects.count() == 4
# should delete self and child, leaving parent
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={obj_1_1.id}
args={obj_tree_1.id}
)
)
assert r.status_code == 204
with scopes_disabled():
assert Food.objects.count() == 1
assert Food.objects.count() == 2
assert Food.find_problems() == ([], [], [], [], [])
@@ -283,13 +260,16 @@ def test_integrity(u1_s1, recipe_1_s1):
assert Ingredient.objects.count() == 9
def test_move(u1_s1, obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, space_1):
url = reverse(MOVE_URL, args=[obj_1_1.id, obj_2.id])
with scopes_disabled():
assert obj_1.get_num_children() == 1
assert obj_1.get_descendant_count() == 2
def test_move(u1_s1, obj_tree_1, obj_2, obj_3, space_1):
with scope(space=space_1):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
assert parent.get_num_children() == 1
assert parent.get_descendant_count() == 2
assert Food.get_root_nodes().filter(space=space_1).count() == 2
url = reverse(MOVE_URL, args=[obj_tree_1.id, obj_2.id])
# move child to new parent, only HTTP put method should work
r = u1_s1.get(url)
assert r.status_code == 405
@@ -301,61 +281,107 @@ def test_move(u1_s1, obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, space_1):
assert r.status_code == 200
with scopes_disabled():
# django-treebeard bypasses django ORM so object needs retrieved again
obj_1 = Food.objects.get(pk=obj_1.id)
parent = Food.objects.get(pk=parent.id)
obj_2 = Food.objects.get(pk=obj_2.id)
assert obj_1.get_num_children() == 0
assert obj_1.get_descendant_count() == 0
assert parent.get_num_children() == 0
assert parent.get_descendant_count() == 0
assert obj_2.get_num_children() == 1
assert obj_2.get_descendant_count() == 2
# move child to root
r = u1_s1.put(reverse(MOVE_URL, args=[obj_1_1.id, 0]))
assert r.status_code == 200
with scopes_disabled():
assert Food.get_root_nodes().filter(space=space_1).count() == 3
# attempt to move to non-existent parent
r = u1_s1.put(
reverse(MOVE_URL, args=[obj_1.id, 9999])
)
assert r.status_code == 404
# attempt to move to wrong space
r = u1_s1.put(
reverse(MOVE_URL, args=[obj_1_1.id, obj_3.id])
)
assert r.status_code == 404
# run diagnostic to find problems - none should be found
with scopes_disabled():
assert Food.find_problems() == ([], [], [], [], [])
def test_merge(
u1_s1,
obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3,
ing_1_s1, ing_2_s1, ing_3_s2, ing_1_1_s1,
sle_1_s1, sle_2_s1, sle_3_s2, sle_1_1_s1,
space_1
):
def test_move_errors(u1_s1, obj_tree_1, obj_3, space_1):
with scope(space=space_1):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
# move child to root
r = u1_s1.put(reverse(MOVE_URL, args=[obj_tree_1.id, 0]))
assert r.status_code == 200
with scopes_disabled():
assert obj_1.get_num_children() == 1
assert obj_1.get_descendant_count() == 2
assert Food.get_root_nodes().filter(space=space_1).count() == 2
assert Food.objects.filter(space=space_1).count() == 4
assert obj_1.ingredient_set.count() == 1
assert obj_2.ingredient_set.count() == 1
assert obj_3.ingredient_set.count() == 1
assert obj_1_1.ingredient_set.count() == 1
assert obj_1_1_1.ingredient_set.count() == 0
assert obj_1.shoppinglistentry_set.count() == 1
assert obj_2.shoppinglistentry_set.count() == 1
assert obj_3.shoppinglistentry_set.count() == 1
assert obj_1_1.shoppinglistentry_set.count() == 1
assert obj_1_1_1.shoppinglistentry_set.count() == 0
# merge food with no children and no ingredient/shopping list entry with another food, only HTTP put method should work
url = reverse(MERGE_URL, args=[obj_1_1_1.id, obj_2.id])
# attempt to move to non-existent parent
r = u1_s1.put(
reverse(MOVE_URL, args=[parent.id, 9999])
)
assert r.status_code == 404
# attempt to move non-existent mode to parent
r = u1_s1.put(
reverse(MOVE_URL, args=[9999, parent.id])
)
assert r.status_code == 404
# attempt to move to wrong space
r = u1_s1.put(
reverse(MOVE_URL, args=[obj_tree_1.id, obj_3.id])
)
assert r.status_code == 404
# TODO: figure out how to generalize this to be all related objects
def test_merge_ingredients(obj_tree_1, u1_s1, space_1):
with scope(space=space_1):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
IngredientFactory.create(food=parent, space=space_1)
IngredientFactory.create(food=child, space=space_1)
assert parent.get_num_children() == 1
assert parent.get_descendant_count() == 2
assert Ingredient.objects.count() == 2
assert parent.ingredient_set.count() == 1
assert obj_tree_1.ingredient_set.count() == 0
assert child.ingredient_set.count() == 1
# merge food (with connected ingredient) with children to another food
r = u1_s1.put(reverse(MERGE_URL, args=[child.id, obj_tree_1.id]))
assert r.status_code == 200
with scope(space=space_1):
# django-treebeard bypasses django ORM so object needs retrieved again
with pytest.raises(Food.DoesNotExist):
Food.objects.get(pk=child.id)
obj_tree_1 = Food.objects.get(pk=obj_tree_1.id)
assert obj_tree_1.ingredient_set.count() == 1 # now has child's ingredient
def test_merge_shopping_entries(obj_tree_1, u1_s1, space_1):
with scope(space=space_1):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
ShoppingListEntryFactory.create(food=parent, space=space_1)
ShoppingListEntryFactory.create(food=child, space=space_1)
assert parent.get_num_children() == 1
assert parent.get_descendant_count() == 2
assert ShoppingListEntry.objects.count() == 2
assert parent.shopping_entries.count() == 1
assert obj_tree_1.shopping_entries.count() == 0
assert child.shopping_entries.count() == 1
# merge food (with connected shoppinglistentry) with children to another food
r = u1_s1.put(reverse(MERGE_URL, args=[child.id, obj_tree_1.id]))
assert r.status_code == 200
with scope(space=space_1):
# django-treebeard bypasses django ORM so object needs retrieved again
with pytest.raises(Food.DoesNotExist):
Food.objects.get(pk=child.id)
obj_tree_1 = Food.objects.get(pk=obj_tree_1.id)
assert obj_tree_1.shopping_entries.count() == 1 # now has child's ingredient
def test_merge(u1_s1, obj_tree_1, obj_1, obj_3, space_1):
with scope(space=space_1):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
assert parent.get_num_children() == 1
assert parent.get_descendant_count() == 2
assert Food.get_root_nodes().filter(space=space_1).count() == 2
assert Food.objects.count() == 4
# merge food with no children with another food, only HTTP put method should work
url = reverse(MERGE_URL, args=[child.id, obj_tree_1.id])
r = u1_s1.get(url)
assert r.status_code == 405
r = u1_s1.post(url)
@@ -364,88 +390,163 @@ def test_merge(
assert r.status_code == 405
r = u1_s1.put(url)
assert r.status_code == 200
with scopes_disabled():
with scope(space=space_1):
# django-treebeard bypasses django ORM so object needs retrieved again
with pytest.raises(Food.DoesNotExist):
Food.objects.get(pk=child.id)
obj_tree_1 = Food.objects.get(pk=obj_tree_1.id)
assert parent.get_num_children() == 1
assert parent.get_descendant_count() == 1
# merge food with children with another food
r = u1_s1.put(reverse(MERGE_URL, args=[parent.id, obj_1.id]))
assert r.status_code == 200
with scope(space=space_1):
# django-treebeard bypasses django ORM so object needs retrieved again
with pytest.raises(Food.DoesNotExist):
Food.objects.get(pk=parent.id)
obj_1 = Food.objects.get(pk=obj_1.id)
obj_2 = Food.objects.get(pk=obj_2.id)
assert Food.objects.filter(pk=obj_1_1_1.id).count() == 0
assert obj_1.get_num_children() == 1
assert obj_1.get_descendant_count() == 1
assert obj_2.get_num_children() == 0
assert obj_2.get_descendant_count() == 0
assert obj_1.ingredient_set.count() == 1
assert obj_2.ingredient_set.count() == 1
assert obj_3.ingredient_set.count() == 1
assert obj_1_1.ingredient_set.count() == 1
assert obj_1.shoppinglistentry_set.count() == 1
assert obj_2.shoppinglistentry_set.count() == 1
assert obj_3.shoppinglistentry_set.count() == 1
assert obj_1_1.shoppinglistentry_set.count() == 1
# merge food (with connected ingredient/shoppinglistentry) with children to another food
r = u1_s1.put(reverse(MERGE_URL, args=[obj_1.id, obj_2.id]))
assert r.status_code == 200
with scopes_disabled():
# django-treebeard bypasses django ORM so object needs retrieved again
obj_2 = Food.objects.get(pk=obj_2.id)
assert Food.objects.filter(pk=obj_1.id).count() == 0
assert obj_2.get_num_children() == 1
assert obj_2.get_descendant_count() == 1
assert obj_2.ingredient_set.count() == 2
assert obj_3.ingredient_set.count() == 1
assert obj_1_1.ingredient_set.count() == 1
assert obj_2.shoppinglistentry_set.count() == 2
assert obj_3.shoppinglistentry_set.count() == 1
assert obj_1_1.shoppinglistentry_set.count() == 1
# attempt to merge with non-existent parent
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_1_1.id, 9999])
)
assert r.status_code == 404
# attempt to move to wrong space
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_2.id, obj_3.id])
)
assert r.status_code == 404
# attempt to merge with child
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_2.id, obj_1_1.id])
)
assert r.status_code == 403
# attempt to merge with self
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_2.id, obj_2.id])
)
assert r.status_code == 403
# run diagnostic to find problems - none should be found
with scopes_disabled():
assert Food.find_problems() == ([], [], [], [], [])
def test_root_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
def test_merge_errors(u1_s1, obj_tree_1, obj_3, space_1):
with scope(space=space_1):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
# attempt to merge with non-existent parent
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_tree_1.id, 9999])
)
assert r.status_code == 404
# attempt to merge non-existent node to parent
r = u1_s1.put(
reverse(MERGE_URL, args=[9999, obj_tree_1.id])
)
assert r.status_code == 404
# attempt to move to wrong space
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_tree_1.id, obj_3.id])
)
assert r.status_code == 404
# attempt to merge with child
r = u1_s1.put(
reverse(MERGE_URL, args=[parent.id, obj_tree_1.id])
)
assert r.status_code == 403
# attempt to merge with self
r = u1_s1.put(
reverse(MERGE_URL, args=[obj_tree_1.id, obj_tree_1.id])
)
assert r.status_code == 403
def test_root_filter(obj_tree_1, obj_2, obj_3, u1_s1):
with scope(space=obj_tree_1.space):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
# should return root objects in the space (obj_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root=0').content)
assert len(response['results']) == 2
with scopes_disabled():
obj_2.move(obj_1, node_location)
# should return direct children of obj_1 (obj_1_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={obj_1.id}').content)
obj_2.move(parent, node_location)
# should return direct children of parent (obj_tree_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={parent.id}').content)
assert response['count'] == 2
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={obj_1.id}&query={obj_2.name[4:]}').content)
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?root={parent.id}&query={obj_2.name[4:]}').content)
assert response['count'] == 2
def test_tree_filter(obj_1, obj_1_1, obj_1_1_1, obj_2, obj_3, u1_s1):
with scopes_disabled():
obj_2.move(obj_1, node_location)
# should return full tree starting at obj_1 (obj_1_1_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={obj_1.id}').content)
def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1):
with scope(space=obj_tree_1.space):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
obj_2.move(parent, node_location)
# should return full tree starting at parent (obj_tree_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}').content)
assert response['count'] == 4
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={obj_1.id}&query={obj_2.name[4:]}').content)
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}&query={obj_2.name[4:]}').content)
assert response['count'] == 4
# This is more about the model than the API - should this be moved to a different test?
@pytest.mark.parametrize("obj_tree_1, field, inherit, new_val", [
({'has_category': True, 'inherit': True}, 'supermarket_category', True, 'cat_1'),
({'has_category': True, 'inherit': False}, 'supermarket_category', False, 'cat_1'),
({'ignore_shopping': True, 'inherit': True}, 'ignore_shopping', True, 'false'),
({'ignore_shopping': True, 'inherit': False}, 'ignore_shopping', False, 'false'),
], indirect=['obj_tree_1']) # indirect=True populates magic variable request.param of obj_tree_1 with the parameter
def test_inherit(request, obj_tree_1, field, inherit, new_val, u1_s1):
with scope(space=obj_tree_1.space):
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
new_val = request.getfixturevalue(new_val)
# if this test passes it demonstrates that inheritance works
# when moving to a parent as each food is created with a different category
assert (getattr(parent, field) == getattr(obj_tree_1, field)) in [inherit, True]
assert (getattr(obj_tree_1, field) == getattr(child, field)) in [inherit, True]
# change parent to a new value
setattr(parent, field, new_val)
with scope(space=parent.space):
parent.save() # trigger post-save signal
# get the objects again because values are cached
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
child = Food.objects.get(id=child.id)
# when changing parent value the obj value should be same if inherited
assert (getattr(obj_tree_1, field) == new_val) == inherit
assert (getattr(child, field) == new_val) == inherit
@pytest.mark.parametrize("obj_tree_1", [
({'has_category': True, 'inherit': False, 'ignore_shopping': True}),
], indirect=['obj_tree_1'])
def test_reset_inherit(obj_tree_1, space_1):
with scope(space=space_1):
space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True)) # set default inherit fields
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
obj_tree_1.ignore_shopping = False
assert parent.ignore_shopping == child.ignore_shopping
assert parent.ignore_shopping != obj_tree_1.ignore_shopping
assert parent.supermarket_category != child.supermarket_category
assert parent.supermarket_category != obj_tree_1.supermarket_category
parent.reset_inheritance(space=space_1)
# djangotree bypasses ORM and need to be retrieved again
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
parent = obj_tree_1.get_parent()
child = obj_tree_1.get_descendants()[0]
assert parent.ignore_shopping == obj_tree_1.ignore_shopping == child.ignore_shopping
assert parent.supermarket_category == obj_tree_1.supermarket_category == child.supermarket_category
def test_onhand(obj_1, u1_s1, u2_s1):
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
u1_s1.patch(
reverse(
DETAIL_URL,
args={obj_1.id}
),
{'food_onhand': True},
content_type='application/json'
)
assert json.loads(u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == False
user1 = auth.get_user(u1_s1)
user2 = auth.get_user(u2_s1)
user1.userpreference.shopping_share.add(user2)
assert json.loads(u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).content)['food_onhand'] == True

View File

@@ -0,0 +1,96 @@
# test create
# test create units
# test amounts
# test create wrong space
# test sharing
# test delete
# test delete checked (nothing should happen)
# test delete not shared (nothing happens)
# test delete shared
import json
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scope, scopes_disabled
from cookbook.models import Food, ShoppingListEntry
from cookbook.tests.factories import FoodFactory
SHOPPING_LIST_URL = 'api:shoppinglistentry-list'
SHOPPING_FOOD_URL = 'api:food-shopping'
@pytest.fixture()
def food(request, space_1, u1_s1):
return FoodFactory(space=space_1)
def test_shopping_forbidden_methods(food, u1_s1):
r = u1_s1.post(
reverse(SHOPPING_FOOD_URL, args={food.id}))
assert r.status_code == 405
r = u1_s1.delete(
reverse(SHOPPING_FOOD_URL, args={food.id}))
assert r.status_code == 405
r = u1_s1.get(
reverse(SHOPPING_FOOD_URL, args={food.id}))
assert r.status_code == 405
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 204],
['u1_s2', 404],
['a1_s1', 204],
])
def test_shopping_food_create(request, arg, food):
c = request.getfixturevalue(arg[0])
r = c.put(reverse(SHOPPING_FOOD_URL, args={food.id}))
assert r.status_code == arg[1]
if r.status_code == 204:
assert len(json.loads(c.get(reverse(SHOPPING_LIST_URL)).content)) == 1
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 204],
['u1_s2', 404],
['a1_s1', 204],
])
def test_shopping_food_delete(request, arg, food):
c = request.getfixturevalue(arg[0])
r = c.put(
reverse(SHOPPING_FOOD_URL, args={food.id}),
{'_delete': "true"},
content_type='application/json'
)
assert r.status_code == arg[1]
if r.status_code == 204:
assert len(json.loads(c.get(reverse(SHOPPING_LIST_URL)).content)) == 0
def test_shopping_food_share(u1_s1, u2_s1, food, space_1):
with scope(space=space_1):
user1 = auth.get_user(u1_s1)
user2 = auth.get_user(u2_s1)
food2 = FoodFactory(space=space_1)
r = u1_s1.put(reverse(SHOPPING_FOOD_URL, args={food.id}))
r = u2_s1.put(reverse(SHOPPING_FOOD_URL, args={food2.id}))
sl_1 = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
sl_2 = json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert len(sl_1) == 1
assert len(sl_2) == 1
sl_1[0]['created_by']['id'] == user1.id
sl_2[0]['created_by']['id'] == user2.id
with scopes_disabled():
user1.userpreference.shopping_share.add(user2)
user1.userpreference.save()
assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 1
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 2

View File

@@ -4,13 +4,16 @@ from datetime import datetime, timedelta
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from django_scopes import scope, scopes_disabled
from cookbook.models import Food, MealPlan, MealType
from cookbook.tests.factories import RecipeFactory
LIST_URL = 'api:mealplan-list'
DETAIL_URL = 'api:mealplan-detail'
# NOTE: auto adding shopping list from meal plan is tested in test_shopping_recipe as tests are identical
@pytest.fixture()
def meal_type(space_1, u1_s1):
@@ -106,7 +109,7 @@ def test_add(arg, request, u1_s2, recipe_1_s1, meal_type):
r = c.post(
reverse(LIST_URL),
{'recipe': {'id': recipe_1_s1.id, 'name': recipe_1_s1.name, 'keywords': []}, 'meal_type': {'id': meal_type.id, 'name': meal_type.name},
'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test'},
'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test', 'shared': []},
content_type='application/json'
)
response = json.loads(r.content)
@@ -139,3 +142,17 @@ def test_delete(u1_s1, u1_s2, obj_1):
assert r.status_code == 204
with scopes_disabled():
assert MealPlan.objects.count() == 0
def test_add_with_shopping(u1_s1, meal_type):
space = meal_type.space
with scope(space=space):
recipe = RecipeFactory.create(space=space)
r = u1_s1.post(
reverse(LIST_URL),
{'recipe': {'id': recipe.id, 'name': recipe.name, 'keywords': []}, 'meal_type': {'id': meal_type.id, 'name': meal_type.name},
'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test', 'shared': [], 'addshopping': True},
content_type='application/json'
)
assert len(json.loads(u1_s1.get(reverse('api:shoppinglistentry-list')).content)) == 10

View File

@@ -1,6 +1,6 @@
import json
import pytest
import pytest
from django.urls import reverse
from django_scopes import scopes_disabled

View File

@@ -0,0 +1,73 @@
import json
import factory
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from pytest_factoryboy import LazyFixture, register
from cookbook.tests.factories import RecipeFactory
RELATED_URL = 'api:recipe-related'
@pytest.fixture()
def recipe(request, space_1, u1_s1):
try:
params = request.param # request.param is a magic variable
except AttributeError:
params = {}
step_recipe = params.get('steps__count', 1)
steps__recipe_count = params.get('steps__recipe_count', 0)
steps__food_recipe_count = params.get('steps__food_recipe_count', {})
created_by = params.get('created_by', auth.get_user(u1_s1))
return RecipeFactory.create(
steps__recipe_count=steps__recipe_count,
steps__food_recipe_count=steps__food_recipe_count,
created_by=created_by,
space=space_1,
)
@pytest.mark.parametrize("arg", [
['g1_s1', 200],
['u1_s1', 200],
['u1_s2', 404],
['a1_s1', 200],
])
@pytest.mark.parametrize("recipe, related_count", [
({}, 0),
({'steps__recipe_count': 1}, 1), # shopping list from recipe with StepRecipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 1), # shopping list from recipe with food recipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 2), # shopping list from recipe with StepRecipe and food recipe
], indirect=['recipe'])
def test_get_related_recipes(request, arg, recipe, related_count, u1_s1, space_2):
c = request.getfixturevalue(arg[0])
r = c.get(reverse(RELATED_URL, args={recipe.id}))
assert r.status_code == arg[1]
if r.status_code == 200:
assert len(json.loads(r.content)) == related_count
@pytest.mark.parametrize("recipe", [
({'steps__recipe_count': 1}), # shopping list from recipe with StepRecipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}}), # shopping list from recipe with food recipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}), # shopping list from recipe with StepRecipe and food recipe
], indirect=['recipe'])
def test_related_mixed_space(request, recipe, u1_s2):
with scopes_disabled():
recipe.space = auth.get_user(u1_s2).userpreference.space
recipe.save()
assert len(json.loads(
u1_s2.get(
reverse(RELATED_URL, args={recipe.id})).content)) == 0
# TODO if/when related recipes includes multiple levels (related recipes of related recipes) add the following tests
# -- step recipes included in step recipes
# -- step recipes included in food recipes
# -- food recipes included in step recipes
# -- food recipes included in food recipes
# -- -- included recipes in the wrong space

View File

@@ -5,7 +5,7 @@ from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import RecipeBook, Storage, Sync, SyncLog, ShoppingList
from cookbook.models import RecipeBook, ShoppingList, Storage, Sync, SyncLog
LIST_URL = 'api:shoppinglist-list'
DETAIL_URL = 'api:shoppinglist-detail'
@@ -56,6 +56,21 @@ def test_share(obj_1, u1_s1, u2_s1, u1_s2):
assert u1_s2.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404
def test_new_share(request, obj_1, u1_s1, u2_s1, u1_s2):
assert u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200
assert u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404
assert u1_s2.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404
with scopes_disabled():
user = auth.get_user(u1_s1)
user.userpreference.shopping_share.add(auth.get_user(u2_s1))
user.userpreference.save()
assert u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200
assert u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200
assert u1_s2.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 404],

View File

@@ -6,7 +6,7 @@ from django.forms import model_to_dict
from django.urls import reverse
from django_scopes import scopes_disabled
from cookbook.models import ShoppingList, ShoppingListEntry, Food
from cookbook.models import Food, ShoppingList, ShoppingListEntry
LIST_URL = 'api:shoppinglistentry-list'
DETAIL_URL = 'api:shoppinglistentry-detail'
@@ -14,7 +14,7 @@ DETAIL_URL = 'api:shoppinglistentry-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1):
e = ShoppingListEntry.objects.create(food=Food.objects.get_or_create(name='test 1', space=space_1)[0])
e = ShoppingListEntry.objects.create(created_by=auth.get_user(u1_s1), food=Food.objects.get_or_create(name='test 1', space=space_1)[0], space=space_1)
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.entries.add(e)
return e
@@ -22,7 +22,7 @@ def obj_1(space_1, u1_s1):
@pytest.fixture
def obj_2(space_1, u1_s1):
e = ShoppingListEntry.objects.create(food=Food.objects.get_or_create(name='test 2', space=space_1)[0])
e = ShoppingListEntry.objects.create(created_by=auth.get_user(u1_s1), food=Food.objects.get_or_create(name='test 2', space=space_1)[0], space=space_1)
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.entries.add(e)
return e
@@ -45,8 +45,11 @@ def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2):
with scopes_disabled():
s = ShoppingList.objects.first()
e = ShoppingListEntry.objects.first()
s.space = space_2
e.space = space_2
s.save()
e.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0

View File

@@ -0,0 +1,222 @@
import json
from datetime import timedelta
import factory
import pytest
from django.contrib import auth
from django.forms import model_to_dict
from django.urls import reverse
from django.utils import timezone
from django_scopes import scopes_disabled
from pytest_factoryboy import LazyFixture, register
from cookbook.models import ShoppingListEntry
from cookbook.tests.factories import ShoppingListEntryFactory
LIST_URL = 'api:shoppinglistentry-list'
DETAIL_URL = 'api:shoppinglistentry-detail'
@pytest.fixture
def sle(space_1, u1_s1):
user = auth.get_user(u1_s1)
return ShoppingListEntryFactory.create_batch(10, space=space_1, created_by=user)
@pytest.fixture
def sle_2(request):
try:
params = request.param # request.param is a magic variable
except AttributeError:
params = {}
u = request.getfixturevalue(params.get('user', 'u1_s1'))
user = auth.get_user(u)
count = params.get('count', 10)
return ShoppingListEntryFactory.create_batch(count, space=user.userpreference.space, created_by=user)
@ pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['u1_s1', 200],
['a1_s1', 200],
])
def test_list_permission(arg, request):
c = request.getfixturevalue(arg[0])
assert c.get(reverse(LIST_URL)).status_code == arg[1]
def test_list_space(sle, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 10
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
with scopes_disabled():
e = ShoppingListEntry.objects.first()
e.space = space_2
e.save()
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 9
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
def test_get_detail(u1_s1, sle):
r = u1_s1.get(reverse(
DETAIL_URL,
args={sle[0].id}
))
assert json.loads(r.content)['id'] == sle[0].id
@ pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 404],
['u1_s1', 200],
['a1_s1', 404],
['g1_s2', 404],
['u1_s2', 404],
['a1_s2', 404],
])
def test_update(arg, request, sle):
c = request.getfixturevalue(arg[0])
new_val = float(sle[0].amount + 1)
r = c.patch(
reverse(
DETAIL_URL,
args={sle[0].id}
),
{'amount': new_val},
content_type='application/json'
)
assert r.status_code == arg[1]
if r.status_code == 200:
response = json.loads(r.content)
assert response['amount'] == new_val
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 201],
['u1_s1', 201],
['a1_s1', 201],
])
def test_add(arg, request, sle):
c = request.getfixturevalue(arg[0])
r = c.post(
reverse(LIST_URL),
{'food': model_to_dict(sle[0].food), 'amount': 1},
content_type='application/json'
)
response = json.loads(r.content)
print(r.content)
assert r.status_code == arg[1]
if r.status_code == 201:
assert response['food']['id'] == sle[0].food.pk
def test_delete(u1_s1, u1_s2, sle):
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={sle[0].id}
)
)
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={sle[0].id}
)
)
assert r.status_code == 204
@pytest.mark.parametrize("shared, count, sle_2", [
('g1_s1', 20, {'user': 'g1_s1'}),
('g1_s2', 10, {'user': 'g1_s2'}),
('u2_s1', 20, {'user': 'u2_s1'}),
('u1_s2', 10, {'user': 'u1_s2'}),
('a1_s1', 20, {'user': 'a1_s1'}),
('a1_s2', 10, {'user': 'a1_s2'}),
], indirect=['sle_2'])
def test_sharing(request, shared, count, sle_2, sle, u1_s1):
user = auth.get_user(u1_s1)
shared_client = request.getfixturevalue(shared)
shared_user = auth.get_user(shared_client)
# confirm shared user can't access shopping list items created by u1_s1
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 10
assert len(json.loads(shared_client.get(reverse(LIST_URL)).content)) == 10
user.userpreference.shopping_share.add(shared_user)
# confirm sharing user only sees their shopping list
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 10
r = shared_client.get(reverse(LIST_URL))
# confirm shared user sees their list and the list that's shared with them
assert len(json.loads(r.content)) == count
def test_completed(sle, u1_s1):
# check 1 entry
#
u1_s1.patch(
reverse(DETAIL_URL, args={sle[0].id}),
{'checked': True},
content_type='application/json'
)
r = json.loads(u1_s1.get(reverse(LIST_URL)).content)
assert len(r) == 10
# count unchecked entries
assert [x['checked'] for x in r].count(False) == 9
# confirm completed_at is populated
assert [(x['completed_at'] != None) for x in r if x['checked']].count(True) == 1
assert len(json.loads(u1_s1.get(f'{reverse(LIST_URL)}?checked=0').content)) == 9
assert len(json.loads(u1_s1.get(f'{reverse(LIST_URL)}?checked=1').content)) == 1
# uncheck entry
u1_s1.patch(
reverse(DETAIL_URL, args={sle[0].id}),
{'checked': False},
content_type='application/json'
)
r = json.loads(u1_s1.get(reverse(LIST_URL)).content)
assert [x['checked'] for x in r].count(False) == 10
# confirm completed_at value cleared
assert [(x['completed_at'] != None) for x in r if x['checked']].count(True) == 0
def test_recent(sle, u1_s1):
user = auth.get_user(u1_s1)
today_start = timezone.now().replace(hour=0, minute=0, second=0)
# past_date within recent_days threshold
past_date = today_start - timedelta(days=user.userpreference.shopping_recent_days - 1)
sle[0].checked = True
sle[0].completed_at = past_date
sle[0].save()
r = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?recent=1').content)
assert len(r) == 10
assert [x['checked'] for x in r].count(False) == 9
# past_date outside recent_days threshold
past_date = today_start - timedelta(days=user.userpreference.shopping_recent_days + 2)
sle[0].completed_at = past_date
sle[0].save()
r = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?recent=1').content)
assert len(r) == 9
assert [x['checked'] for x in r].count(False) == 9
# user preference moved to include entry again
user.userpreference.shopping_recent_days = user.userpreference.shopping_recent_days + 4
user.userpreference.save()
r = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?recent=1').content)
assert len(r) == 10
assert [x['checked'] for x in r].count(False) == 9
# TODO test auto onhand

View File

@@ -0,0 +1,242 @@
import json
from datetime import timedelta
import factory
import pytest
# work around for bug described here https://stackoverflow.com/a/70312265/15762829
from django.conf import settings
from django.contrib import auth
from django.forms import model_to_dict
from django.urls import reverse
from django.utils import timezone
from django_scopes import scope, scopes_disabled
from pytest_factoryboy import LazyFixture, register
from cookbook.models import Food, Ingredient, ShoppingListEntry, Step
from cookbook.tests.factories import (IngredientFactory, MealPlanFactory, RecipeFactory,
StepFactory, UserFactory)
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
from django.db.backends.postgresql.features import DatabaseFeatures
DatabaseFeatures.can_defer_constraint_checks = False
SHOPPING_LIST_URL = 'api:shoppinglistentry-list'
SHOPPING_RECIPE_URL = 'api:recipe-shopping'
@pytest.fixture()
def user2(request, u1_s1):
try:
params = request.param # request.param is a magic variable
except AttributeError:
params = {}
user = auth.get_user(u1_s1)
user.userpreference.mealplan_autoadd_shopping = params.get('mealplan_autoadd_shopping', True)
user.userpreference.mealplan_autoinclude_related = params.get('mealplan_autoinclude_related', True)
user.userpreference.mealplan_autoexclude_onhand = params.get('mealplan_autoexclude_onhand', True)
user.userpreference.save()
return u1_s1
@pytest.fixture()
def recipe(request, space_1, u1_s1):
try:
params = request.param # request.param is a magic variable
except AttributeError:
params = {}
# step_recipe = params.get('steps__count', 1)
# steps__recipe_count = params.get('steps__recipe_count', 0)
# steps__food_recipe_count = params.get('steps__food_recipe_count', {})
params['created_by'] = params.get('created_by', auth.get_user(u1_s1))
params['space'] = space_1
return RecipeFactory(**params)
# return RecipeFactory.create(
# steps__recipe_count=steps__recipe_count,
# steps__food_recipe_count=steps__food_recipe_count,
# created_by=created_by,
# space=space_1,
# )
@pytest.mark.parametrize("arg", [
['g1_s1', 204],
['u1_s1', 204],
['u1_s2', 404],
['a1_s1', 204],
])
@pytest.mark.parametrize("recipe, sle_count", [
({}, 10),
({'steps__recipe_count': 1}, 20), # shopping list from recipe with StepRecipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19), # shopping list from recipe with food recipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29), # shopping list from recipe with StepRecipe and food recipe
], indirect=['recipe'])
def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1):
c = request.getfixturevalue(arg[0])
user = auth.get_user(c)
user.userpreference.mealplan_autoadd_shopping = True
user.userpreference.save()
assert len(json.loads(c.get(reverse(SHOPPING_LIST_URL)).content)) == 0
url = reverse(SHOPPING_RECIPE_URL, args={recipe.id})
r = c.put(url)
assert r.status_code == arg[1]
# only PUT method should work
if r.status_code == 204: # skip anonymous user
r = json.loads(c.get(reverse(SHOPPING_LIST_URL)).content)
assert len(r) == sle_count # recipe factory creates 10 ingredients by default
assert [x['created_by']['id'] for x in r].count(user.id) == sle_count
# user in space can't see shopping list
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
user.userpreference.shopping_share.add(auth.get_user(u2_s1))
# after share, user in space can see shopping list
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
# confirm that the author of the recipe doesn't have access to shopping list
if c != u1_s1:
assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
r = c.get(url)
assert r.status_code == 405
r = c.post(url)
assert r.status_code == 405
r = c.delete(url)
assert r.status_code == 405
@pytest.mark.parametrize("recipe, sle_count", [
({}, 10),
({'steps__recipe_count': 1}, 20), # shopping list from recipe with StepRecipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19), # shopping list from recipe with food recipe
({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29), # shopping list from recipe with StepRecipe and food recipe
], indirect=['recipe'])
@pytest.mark.parametrize("use_mealplan", [(False), (True), ])
def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u2_s1):
# tests editing shopping list via recipe or mealplan
with scopes_disabled():
user = auth.get_user(u1_s1)
user2 = auth.get_user(u2_s1)
user.userpreference.mealplan_autoinclude_related = True
user.userpreference.mealplan_autoadd_shopping = True
user.userpreference.shopping_share.add(user2)
user.userpreference.save()
if use_mealplan:
mealplan = MealPlanFactory(space=recipe.space, created_by=user, servings=recipe.servings, recipe=recipe)
else:
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}))
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert [x['created_by']['id'] for x in r].count(user.id) == sle_count
all_ing = [x['ingredient'] for x in r]
keep_ing = all_ing[1:-1] # remove first and last element
del keep_ing[int(len(keep_ing)/2)] # remove a middle element
list_recipe = r[0]['list_recipe']
amount_sum = sum([x['amount'] for x in r])
# test modifying shopping list as different user
# test increasing servings size of recipe shopping list
if use_mealplan:
mealplan.servings = 2*recipe.servings
mealplan.save()
else:
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
{'list_recipe': list_recipe, 'servings': 2*recipe.servings},
content_type='application/json'
)
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert sum([x['amount'] for x in r]) == amount_sum * 2
assert len(r) == sle_count
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
# testing decreasing servings size of recipe shopping list
if use_mealplan:
mealplan.servings = .5 * recipe.servings
mealplan.save()
else:
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
{'list_recipe': list_recipe, 'servings': .5 * recipe.servings},
content_type='application/json'
)
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert sum([x['amount'] for x in r]) == amount_sum * .5
assert len(r) == sle_count
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
# test removing 2 items from shopping list
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
{'list_recipe': list_recipe, 'ingredients': keep_ing},
content_type='application/json'
)
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert len(r) == sle_count - 3
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count - 3
# add all ingredients to existing shopping list - don't change serving size
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
{'list_recipe': list_recipe, 'ingredients': all_ing},
content_type='application/json'
)
r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)
assert sum([x['amount'] for x in r]) == amount_sum * .5
assert len(r) == sle_count
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
@pytest.mark.parametrize("user2, sle_count", [
({'mealplan_autoadd_shopping': False}, (0, 18)),
({'mealplan_autoinclude_related': False}, (9, 9)),
({'mealplan_autoexclude_onhand': False}, (20, 20)),
({'mealplan_autoexclude_onhand': False, 'mealplan_autoinclude_related': False}, (10, 10)),
], indirect=['user2'])
@pytest.mark.parametrize("use_mealplan", [(False), (True), ])
@pytest.mark.parametrize("recipe", [({'steps__recipe_count': 1})], indirect=['recipe'])
def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2):
with scopes_disabled():
user = auth.get_user(user2)
# setup recipe with 10 ingredients, 1 step recipe with 10 ingredients, 2 food onhand(from recipe and step_recipe)
ingredients = Ingredient.objects.filter(step__recipe=recipe)
food = Food.objects.get(id=ingredients[2].food.id)
food.onhand_users.add(user)
food.save()
food = recipe.steps.exclude(step_recipe=None).first().step_recipe.steps.first().ingredients.first().food
food = Food.objects.get(id=food.id)
food.onhand_users.add(user)
food.save()
if use_mealplan:
mealplan = MealPlanFactory(space=recipe.space, created_by=user, servings=recipe.servings, recipe=recipe)
assert len(json.loads(user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[0]
else:
user2.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}))
assert len(json.loads(user2.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count[1]
def test_shopping_recipe_mixed_authors(u1_s1, u2_s1):
with scopes_disabled():
user1 = auth.get_user(u1_s1)
user2 = auth.get_user(u2_s1)
space = user1.userpreference.space
user3 = UserFactory(space=space)
recipe1 = RecipeFactory(created_by=user1, space=space)
recipe2 = RecipeFactory(created_by=user2, space=space)
recipe3 = RecipeFactory(created_by=user3, space=space)
food = Food.objects.get(id=recipe1.steps.first().ingredients.first().food.id)
food.recipe = recipe2
food.save()
recipe1.steps.add(StepFactory(step_recipe=recipe3, ingredients__count=0, space=space))
recipe1.save()
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe1.id}))
assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 29
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
@pytest.mark.parametrize("recipe", [{'steps__ingredients__header': 1}], indirect=['recipe'])
def test_shopping_with_header_ingredient(u1_s1, recipe):
# with scope(space=recipe.space):
# recipe.step_set.first().ingredient_set.add(IngredientFactory(ingredients__header=1))
u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}))
assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 10
assert len(json.loads(u1_s1.get(reverse('api:ingredient-list')).content)) == 11

Some files were not shown because too many files have changed in this diff Show More