Compare commits

..

498 Commits
1.0.4 ... 1.0.6

Author SHA1 Message Date
vabene1111
6952e10390 Merge branch 'develop' 2022-02-03 14:40:24 +01:00
vabene1111
ed99da2d1e Merge pull request #1472 from smilerz/updated_dockerignore
updated .dockerignore
2022-02-03 14:33:18 +01:00
vabene1111
ed852b3246 Merge pull request #1437 from MaxJa4/improvement/wait_for_db
[Docker] Web_recipes waits for db_recipes to be ready
2022-02-03 14:32:37 +01:00
vabene1111
eec0a49cd6 Merge pull request #1435 from MaxJa4/docs/headers
[Docs] Added example configs and header information
2022-02-03 14:30:20 +01:00
vabene1111
382c08dc0c Merge pull request #1445 from thumm/feature/ldap_cacertfile
Add new env parameter to set ldap ca cert file
2022-02-03 14:28:29 +01:00
Mario Dvorsek
231d1695ff Translated using Weblate (Slovenian)
Currently translated at 15.3% (78 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/sl/
2022-02-02 15:31:32 +00:00
Sven
97febe9aa1 Translated using Weblate (German)
Currently translated at 96.1% (546 of 568 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2022-02-02 15:31:32 +00:00
Chris Scoggins
c5a435905b remove vue source 2022-02-02 07:50:27 -06:00
vabene1111
86e34593d5 Merge pull request #1464 from StephenBrown2/make_header
Fix Make_Header/Make_Ingredient text and reference
2022-02-01 17:40:47 +01:00
Stephen Brown II
3961c684f9 Fix Make_Header/Make_Ingredient text and reference 2022-02-01 09:17:30 -06:00
vabene1111
b2a415b333 Merge pull request #1447 from smilerz/mealplan_shopping_useability
Mealplan shopping useability
2022-02-01 08:28:59 +01:00
Chris Scoggins
13d144345e adjust height of viewport in mobile shopping view 2022-01-31 16:30:54 -06:00
Chris Scoggins
b633be9c13 fix display issues 2022-01-31 16:24:40 -06:00
Chris Scoggins
f45e09a5a5 add units to search fields 2022-01-31 14:41:18 -06:00
Chris Scoggins
5b3a0a6e29 refactor IngredientComponent, move shopping logic to card 2022-01-31 14:05:01 -06:00
Thomas Schumm
505bac514f Add new env parameter to set ldap ca cert file 2022-01-31 19:34:32 +01:00
MaxJa4
39c3ce7ab2 More emphasis on waiting for migrations 2022-01-31 15:42:48 +01:00
MaxJa4
419821733c Optimized healthcheck timings
1s faster interval (to increase retry speed on faster systems), 1s shorter timeout (since it db should react in a lot less than 1s), increased retries to 12 total so all adds up to 60s of potential retry-time for older systems (e.g. RPI 3)
2022-01-31 14:27:39 +01:00
vabene1111
8216d0c025 Merge pull request #1439 from oliviapinson/develop
Simple font size change
2022-01-31 11:41:29 +01:00
dudel
98128fabab Translated using Weblate (German)
Currently translated at 95.9% (545 of 568 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2022-01-30 15:07:01 +00:00
Olivia Pinson
2d36db7822 Simple font size change
Adjusted font size so longer months won't affect arrow buttons. I think this helps the meal plan calendar look more aesthetic.
2022-01-29 22:44:19 -05:00
MaxJa4
300d132266 Removed retry process since it's fixed now
Removed retry process since it's fixed now by improvement/wait_for_db
2022-01-30 01:24:17 +01:00
MaxJa4
6330d15ebe Update docker-compose.yml 2022-01-30 01:20:54 +01:00
MaxJa4
d7d37f9908 Update docker-compose.yml 2022-01-30 01:20:33 +01:00
MaxJa4
fb29db7aad Web container waits for DB container to be ready
Fixes the setup issue almost all Raspberry Pi users have and probably some others with slow or low spec systems.
The web_recipes container waits for db_recipes to be healthy.
db_recipes is healthy, as soon as postgres is completely ready.

Note: ps_isready doesn't work here for some reason.

Tested multiple times with a Raspberry Pi 4.
2022-01-30 01:19:50 +01:00
MaxJa4
76dac29f1c Added raspberry pi setup issue FAQ
Added raspberry pi setup issue FAQ/info
2022-01-30 00:28:27 +01:00
Chris Scoggins
e00794bbdf review shopping list in MealPlan modal 2022-01-29 14:10:14 -06:00
Chris Scoggins
a7796cbf5c fix shopping list sharing 2022-01-29 11:59:06 -06:00
Chris Scoggins
e2f8f29ec8 refactor list_from_recipe as class RecipeShoppingEditor 2022-01-29 10:28:01 -06:00
MaxJa4
6e8729bb58 Added example configs and header information
Added example configs for plain local nginx and Apache2.
Also added FAQ section for all generic issues when not setting all required headers correctly.
Added section for required headers in the Docker installation docs.
2022-01-28 17:44:52 +01:00
vabene1111
9fcfa17004 Merge pull request #1377 from MaxJa4/docs/docker-installation
Enhancement of installation instruction docs
2022-01-28 15:35:20 +01:00
vabene1111
58f1ce0331 fixed rendering issue with single step recipes 2022-01-28 15:26:11 +01:00
vabene1111
20b4c4fb36 fixed using undeclared unused field 2022-01-28 15:21:07 +01:00
vabene1111
965e1664af Merge pull request #1411 from TheHaf/feature/addPortionSizeModifiersToGui
Feature/add portion size modifiers to gui
2022-01-28 15:17:05 +01:00
vabene1111
8232c77ef6 Merge pull request #1432 from smilerz/mealplan_shopping_useability
fix servings reactivity
2022-01-28 15:16:01 +01:00
Chris Scoggins
85bbcb0010 fix servings reactivity 2022-01-28 07:38:36 -06:00
vabene1111
338d8459de Merge pull request #1429 from smilerz/fix_shopping_sharing
fix #1426
2022-01-28 08:54:08 +01:00
Mario Dvorsek
fbf9a81121 Translated using Weblate (Slovenian)
Currently translated at 97.2% (283 of 291 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/sl/
2022-01-28 00:07:08 +00:00
Alex Hanselmann
1f80936805 Translated using Weblate (German)
Currently translated at 93.1% (529 of 568 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2022-01-28 00:07:08 +00:00
Chris Scoggins
8d424d668d fix #1426 2022-01-27 16:46:46 -06:00
vabene1111
b2fcdaa14e wont load by default otherwise 2022-01-27 14:15:38 +01:00
vabene1111
d4d949b870 Merge pull request #1421 from TandoorRecipes/minor_fixes
Minor fixes
2022-01-27 08:02:06 +01:00
smilerz
759ae99b56 Update en.json 2022-01-26 18:58:45 -06:00
smilerz
7104b5b109 sort SupermarketCategory queryset
fixes #1422
2022-01-26 17:31:22 -06:00
smilerz
331a949623 fixed saving shopping shared users 2022-01-26 17:27:52 -06:00
smilerz
cd733d3190 Update ShoppingListView.vue 2022-01-26 17:10:24 -06:00
Chris Scoggins
6e4bb64b4e fix translation on ModelListMenu 2022-01-26 15:00:21 -06:00
Chris Scoggins
4a48019885 fix duplicate shopping entries when using old list 2022-01-26 14:47:33 -06:00
Chris Scoggins
47823132f0 add ignore_shopping && additional help 2022-01-26 12:57:29 -06:00
Leni
bb5c8bbbf1 Translated using Weblate (German)
Currently translated at 100.0% (291 of 291 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2022-01-26 14:07:05 +00:00
Rene
5a0a1ca6a9 Translated using Weblate (German)
Currently translated at 100.0% (291 of 291 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2022-01-26 14:07:05 +00:00
Leni
19cc1e11b9 Translated using Weblate (German)
Currently translated at 91.5% (520 of 568 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2022-01-26 14:07:05 +00:00
Rene
c070c5b0ed Translated using Weblate (German)
Currently translated at 91.5% (520 of 568 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2022-01-26 14:07:05 +00:00
Chris Scoggins
2e2080d8d1 fix Recipe Counts on food/keyword/unit serializer 2022-01-25 16:48:30 -06:00
TheHaf
381a7e76be Use custom input component in recipe view 2022-01-25 22:04:21 +01:00
TheHaf
6c619ab628 Add custom input component with spin button look&feel 2022-01-25 22:03:49 +01:00
vabene1111
ae14dde13d propperly fixed setting 2022-01-25 18:33:12 +01:00
vabene1111
e33cf08fca Revert "use old system to define frontend settings"
This reverts commit f2e9f50d94.
2022-01-25 18:26:51 +01:00
vabene1111
f2e9f50d94 use old system to define frontend settings 2022-01-25 18:14:25 +01:00
vabene1111
75259ec230 corrected comment 2022-01-25 17:38:35 +01:00
vabene1111
f581f17308 Merge branch 'master' into develop 2022-01-25 15:30:36 +01:00
vabene1111
8c49e6ba18 another small migration fix 2022-01-25 15:29:36 +01:00
vabene1111
4b0ed86c36 Merge branch 'develop' 2022-01-25 12:22:16 +01:00
vabene1111
44da3ed7a9 Merge pull request #1406 from smilerz/patch_1.0.5_migration
Patch 1.0.5 migration
2022-01-25 12:21:46 +01:00
Chris Scoggins
f3f50d179f fixed missing field 2022-01-24 19:16:14 -06:00
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
MaxJa4
075c88e5e8 Merge branch 'develop' into docs/docker-installation 2022-01-23 01:38:04 +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
MaxJa4
47dd3118b1 Enhanced manual installation docs 2022-01-19 19:42:38 +01:00
MaxJa4
2e85b01242 Enhanced kubernetes installation docs 2022-01-19 19:38:11 +01:00
MaxJa4
119379028d Enhanced unraid installation docs 2022-01-19 19:33:40 +01:00
MaxJa4
b8bb146422 Enhanced synology installation docs 2022-01-19 19:28:44 +01:00
MaxJa4
71a2f1955e Enhancement of docker install docs 2022-01-19 19:13:33 +01:00
MaxJa4
6b154b05a6 [Docs] Attempt to fix .yml inclusion
File inclusion broke some time ago. Changed back to original format like inclusion-plugin docs suggest.
2022-01-19 18:39:46 +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
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
smilerz
d67c5fcf1b change default status of shopping in recipe view 2021-12-23 08:38:43 -06:00
smilerz
17efc388ca fix 1129 2021-12-23 07:37:26 -06: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
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
219 changed files with 25791 additions and 12912 deletions

View File

@@ -1,4 +1,4 @@
node_modules
**/node_modules
npm-debug.log
Dockerfile*
docker-compose*
@@ -12,6 +12,21 @@ LICENSE
.env.template
.github
.idea
.prettierignore
LICENSE.md
docs
update.sh
update.sh
.pytest_cache
cookbook/tests
mediafiles
staticfiles
db.sqlite3
pytest.ini
vue/**/*.vue
vue/**/*.ts
**/.openapi-generator
mkdocs.yml
vue/babel.config*
vue/package.json
vue/tsconfig.json
vue/src/utils/openapi

View File

@@ -45,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 /
@@ -145,3 +146,8 @@ REVERSE_PROXY_AUTH=0
#AUTH_LDAP_BIND_DN=
#AUTH_LDAP_BIND_PASSWORD=
#AUTH_LDAP_USER_SEARCH_BASE_DN=
#AUTH_LDAP_TLS_CACERTFILE=
# 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]

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]
@@ -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) # disabled for now, causes to many unknown issues
#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'
@@ -153,13 +158,14 @@ class ImportExportBase(forms.Form):
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'), (COPYMETHAT, 'CopyMeThat'),
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'),
))
@@ -171,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):
@@ -223,6 +229,7 @@ class StorageForm(forms.ModelForm):
}
# TODO: Deprecate
class RecipeBookEntryForm(forms.ModelForm):
prefix = 'bookmark'
@@ -262,6 +269,7 @@ class SyncForm(forms.ModelForm):
}
# TODO deprecate
class BatchEditForm(forms.Form):
search = forms.CharField(label=_('Search String'))
keywords = forms.ModelMultipleChoiceField(
@@ -298,6 +306,7 @@ class ImportRecipeForm(forms.ModelForm):
}
# TODO deprecate
class MealPlanForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
@@ -349,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):
@@ -433,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'),
@@ -452,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")
}
@@ -465,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__ in ['ShoppingList', 'ShoppingListEntry']:
# 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)

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,309 @@
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, MealPlan, Recipe, 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')
class RecipeShoppingEditor():
def __init__(self, user, space, **kwargs):
self.created_by = user
self.space = space
self._kwargs = {**kwargs}
self.mealplan = self._kwargs.get('mealplan', None)
if type(self.mealplan) in [int, float]:
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
self.id = self._kwargs.get('id', None)
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
if self._shopping_list_recipe:
# created_by needs to be sticky to original creator as it is 'their' shopping list
# changing shopping list created_by can shift some items to new owner which may not share in the other direction
self.created_by = getattr(self._shopping_list_recipe.entries.first(), 'created_by', self.created_by)
self.recipe = getattr(self._shopping_list_recipe, 'recipe', None) or self._kwargs.get('recipe', None) or getattr(self.mealplan, 'recipe', None)
if type(self.recipe) in [int, float]:
self.recipe = Recipe.objects.filter(id=self.recipe, space=self.space)
try:
self.servings = float(self._kwargs.get('servings', None))
except (ValueError, TypeError):
self.servings = getattr(self._shopping_list_recipe, 'servings', None) or getattr(self.mealplan, 'servings', None) or getattr(self.recipe, 'servings', None)
@property
def _servings_factor(self):
return self.servings / self.recipe.servings
@property
def _shared_users(self):
return [*list(self.created_by.get_shopping_share()), self.created_by]
@staticmethod
def get_shopping_list_recipe(id, user, space):
return ShoppingListRecipe.objects.filter(id=id).filter(Q(shoppinglist__space=space) | Q(entries__space=space)).filter(
Q(shoppinglist__created_by=user)
| Q(shoppinglist__shared=user)
| Q(entries__created_by=user)
| Q(entries__created_by__in=list(user.get_shopping_share()))
).prefetch_related('entries').first()
def get_recipe_ingredients(self, id, exclude_onhand=False):
if exclude_onhand:
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(food__onhand_users__id__in=[x.id for x in self._shared_users])
else:
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
@property
def _include_related(self):
return self.created_by.userpreference.mealplan_autoinclude_related
@property
def _exclude_onhand(self):
return self.created_by.userpreference.mealplan_autoexclude_onhand
def create(self, **kwargs):
ingredients = kwargs.get('ingredients', None)
exclude_onhand = not ingredients and self._exclude_onhand
if servings := kwargs.get('servings', None):
self.servings = float(servings)
if mealplan := kwargs.get('mealplan', None):
self.mealplan = mealplan
self.recipe = mealplan.recipe
elif recipe := kwargs.get('recipe', None):
self.recipe = recipe
if not self.servings:
self.servings = getattr(self.mealplan, 'servings', None) or getattr(self.recipe, 'servings', 1.0)
self._shopping_list_recipe = ShoppingListRecipe.objects.create(recipe=self.recipe, mealplan=self.mealplan, servings=self.servings)
if ingredients:
self._add_ingredients(ingredients=ingredients)
else:
if self._include_related:
related = self.recipe.get_related_recipes()
self._add_ingredients(self.get_recipe_ingredients(self.recipe.id, exclude_onhand=exclude_onhand).exclude(food__recipe__in=related))
for r in related:
self._add_ingredients(self.get_recipe_ingredients(r.id, exclude_onhand=exclude_onhand).exclude(food__recipe__in=related))
else:
self._add_ingredients(self.get_recipe_ingredients(self.recipe.id, exclude_onhand=exclude_onhand))
return True
def add(self, **kwargs):
return
def edit(self, servings=None, ingredients=None, **kwargs):
if servings:
self.servings = servings
self._delete_ingredients(ingredients=ingredients)
if self.servings != self._shopping_list_recipe.servings:
self.edit_servings()
self._add_ingredients(ingredients=ingredients)
return True
def edit_servings(self, servings=None, **kwargs):
if servings:
self.servings = servings
if id := kwargs.get('id', None):
self._shopping_list_recipe = self.get_shopping_list_recipe(id, self.created_by, self.space)
if not self.servings:
raise ValueError(_("You must supply a servings size"))
if self._shopping_list_recipe.servings == self.servings:
return True
for sle in ShoppingListEntry.objects.filter(list_recipe=self._shopping_list_recipe):
sle.amount = sle.ingredient.amount * Decimal(self._servings_factor)
sle.save()
self._shopping_list_recipe.servings = self.servings
self._shopping_list_recipe.save()
return True
def delete(self, **kwargs):
try:
self._shopping_list_recipe.delete()
return True
except:
return False
def _add_ingredients(self, ingredients=None):
if not ingredients:
return
elif type(ingredients) == list:
ingredients = Ingredient.objects.filter(id__in=ingredients)
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
add_ingredients = ingredients.exclude(id__in=existing)
for i in [x for x in add_ingredients if x.food]:
ShoppingListEntry.objects.create(
list_recipe=self._shopping_list_recipe,
food=i.food,
unit=i.unit,
ingredient=i,
amount=i.amount * Decimal(self._servings_factor),
created_by=self.created_by,
space=self.space,
)
# deletes shopping list entries not in ingredients list
def _delete_ingredients(self, ingredients=None):
if not ingredients:
return
to_delete = self._shopping_list_recipe.entries.exclude(ingredient__in=ingredients)
ShoppingListEntry.objects.filter(id__in=to_delete).delete()
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
# # 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, food__ignore_shopping=False, 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, food__ignore_shopping=False, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
# else:
# x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__ignore_shopping=True)
# 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

@@ -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,7 +3,7 @@ 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
@@ -42,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)
@@ -53,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,
@@ -65,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):
"""
@@ -275,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

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
"PO-Revision-Date: 2021-11-16 06:06+0000\n"
"Last-Translator: Luka <storek00@gmail.com>\n"
"PO-Revision-Date: 2022-02-02 15:31+0000\n"
"Last-Translator: Mario Dvorsek <mario.dvorsek@gmail.com>\n"
"Language-Team: Slovenian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/sl/>\n"
"Language: sl\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
"%100==4 ? 2 : 3;\n"
"X-Generator: Weblate 4.8\n"
"X-Generator: Weblate 4.10.1\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
#: .\cookbook\templates\forms\ingredients.html:34
@@ -33,47 +33,47 @@ msgstr "Privzeta enota"
#: .\cookbook\forms.py:55
msgid "Use fractions"
msgstr ""
msgstr "Uporabi ulomke/frakcije"
#: .\cookbook\forms.py:56
msgid "Use KJ"
msgstr ""
msgstr "Uporabi KJ"
#: .\cookbook\forms.py:57
msgid "Theme"
msgstr ""
msgstr "Tema"
#: .\cookbook\forms.py:58
msgid "Navbar color"
msgstr ""
msgstr "Barva navigacijske vrstice"
#: .\cookbook\forms.py:59
msgid "Sticky navbar"
msgstr ""
msgstr "Lepljiva navigacijska vrstica"
#: .\cookbook\forms.py:60
msgid "Default page"
msgstr ""
msgstr "Privzeta stran"
#: .\cookbook\forms.py:61
msgid "Show recent recipes"
msgstr ""
msgstr "Prikaži nedavne recepte"
#: .\cookbook\forms.py:62
msgid "Search style"
msgstr ""
msgstr "Vrsta iskalnika"
#: .\cookbook\forms.py:63
msgid "Plan sharing"
msgstr ""
msgstr "Deli planer"
#: .\cookbook\forms.py:64
msgid "Ingredient decimal places"
msgstr ""
msgstr "Decimalno mesto pri sestavini"
#: .\cookbook\forms.py:65
msgid "Shopping list auto sync period"
msgstr ""
msgstr "Čas avtomatske sinhronizacije pri nakupovalnem listku"
#: .\cookbook\forms.py:66 .\cookbook\templates\recipe_view.html:21
#: .\cookbook\templates\space.html:62 .\cookbook\templates\stats.html:47
@@ -85,38 +85,44 @@ msgid ""
"Color of the top navigation bar. Not all colors work with all themes, just "
"try them out!"
msgstr ""
"Barva zgornje vrstice za krmarjenje. Ne delujejo vse barve z vsemi temami!"
#: .\cookbook\forms.py:72
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
msgstr ""
"Privzeta enota, ki se uporablja pri vstavljanju nove sestavine v recept."
#: .\cookbook\forms.py:74
msgid ""
"Enables support for fractions in ingredient amounts (e.g. convert decimals "
"to fractions automatically)"
msgstr ""
"Omogoča podporo ulomkom/frakcijam v količinah sestavin (npr. samodejno "
"pretvori decimalke v ulomke)"
#: .\cookbook\forms.py:76
msgid "Display nutritional energy amounts in joules instead of calories"
msgstr ""
msgstr "Prikazuj hranilne energijske količine v joules namesto v kalorijah"
#: .\cookbook\forms.py:78
msgid ""
"Users with whom newly created meal plan/shopping list entries should be "
"shared by default."
msgstr ""
"Uporabniki, s katerimi je privzeto deljen novo ustvarjen načrt ali "
"nakupovalni listek."
#: .\cookbook\forms.py:80
msgid "Show recently viewed recipes on search page."
msgstr ""
msgstr "Prikaži nedavno videne recepte na iskalniku."
#: .\cookbook\forms.py:81
msgid "Number of decimals to round ingredients."
msgstr ""
msgstr "Število decimalk, ki so zaokrožene pri sestavinah."
#: .\cookbook\forms.py:82
msgid "If you want to be able to create and see comments underneath recipes."
msgstr ""
msgstr "V primeru, da želite ustvariti in videti komentarje pod recepti."
#: .\cookbook\forms.py:84
msgid ""
@@ -125,21 +131,28 @@ msgid ""
"Useful when shopping with multiple people but might use a little bit of "
"mobile data. If lower than instance limit it is reset when saving."
msgstr ""
"Nastavitev na 0 bo onemogočila avtomatsko sinhronizacijo. V pogledu "
"nakupovalnega listka, se seznam osvežuje vsake toliko sekund, če nekdo drug "
"naredi spremembo. To je najbolj uporabno, če nakupovalni listek delimo z "
"večimi osebami. Paziti je potrebno, saj porabi nekaj podatkov v mobilnem "
"omrežju."
#: .\cookbook\forms.py:87
msgid "Makes the navbar stick to the top of the page."
msgstr ""
msgstr "Nastavi navigacijsko vrstico na vrh strani."
#: .\cookbook\forms.py:103
msgid ""
"Both fields are optional. If none are given the username will be displayed "
"instead"
msgstr ""
"Obe polji sta opcijski. V primeru, da ju pustimo prazni bo prikazano "
"uporabniško ime"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:289
#: .\cookbook\templates\url_import.html:158
msgid "Name"
msgstr ""
msgstr "Ime"
#: .\cookbook\forms.py:125 .\cookbook\forms.py:290
#: .\cookbook\templates\space.html:39 .\cookbook\templates\stats.html:24
@@ -150,47 +163,51 @@ msgstr "Ključne besede"
#: .\cookbook\forms.py:126
msgid "Preparation time in minutes"
msgstr ""
msgstr "Priprava v minutah"
#: .\cookbook\forms.py:127
msgid "Waiting time (cooking/baking) in minutes"
msgstr ""
msgstr "Čas čakanja v minutah"
#: .\cookbook\forms.py:128 .\cookbook\forms.py:259 .\cookbook\forms.py:291
msgid "Path"
msgstr ""
msgstr "Pot"
#: .\cookbook\forms.py:129
msgid "Storage UID"
msgstr ""
msgstr "UID shrambe"
#: .\cookbook\forms.py:157
msgid "Default"
msgstr ""
msgstr "Privzeto"
#: .\cookbook\forms.py:168 .\cookbook\templates\url_import.html:94
msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
msgstr ""
"V primeru, da želite preprečiti dvojnike receptov z enakim imenom kot so "
"obstoječi. Če želite uvoziti vse, potrdite to polje."
#: .\cookbook\forms.py:190
msgid "Add your comment: "
msgstr ""
msgstr "Dodaj komentar: "
#: .\cookbook\forms.py:205
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
msgstr "Pusti prazno za dropbox in vnesi geslo za nextcloud."
#: .\cookbook\forms.py:212
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr ""
msgstr "Pusti prazno za nextcloud in vnesi API žeton za dropbox."
#: .\cookbook\forms.py:221
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
msgstr ""
"Pusti prazno za dropbox in vnesi URL za nextcloud (<code>/remote.php/webdav/"
"</code> je dodano avtomatsko)"
#: .\cookbook\forms.py:258 .\cookbook\views\edit.py:166
msgid "Storage"
@@ -198,33 +215,35 @@ msgstr "Shramba"
#: .\cookbook\forms.py:260
msgid "Active"
msgstr ""
msgstr "Aktivno"
#: .\cookbook\forms.py:265
msgid "Search String"
msgstr ""
msgstr "Iskalni niz"
#: .\cookbook\forms.py:292
msgid "File ID"
msgstr ""
msgstr "ID datoteke"
#: .\cookbook\forms.py:313
msgid "You must provide at least a recipe or a title."
msgstr ""
msgstr "Vpisati moraš vsaj recept ali naslov."
#: .\cookbook\forms.py:326
msgid "You can list default users to share recipes with in the settings."
msgstr ""
msgstr "Seznam uporabnikov za deljenje receptov lahko vidiš v nastavitvah."
#: .\cookbook\forms.py:327
msgid ""
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
"\">docs here</a>"
msgstr ""
"Lahko uporabiš \"markdown\", da urediš to polje. Preveri <a href=\"/docs/"
"markdown/\">tukaj</a>"
#: .\cookbook\forms.py:353
msgid "Maximum number of users for this space reached."
msgstr ""
msgstr "Maksimalno število uporabnikov za ta prostor je doseženo."
#: .\cookbook\forms.py:359
msgid "Email address already taken!"
@@ -235,56 +254,71 @@ msgid ""
"An email address is not required but if present the invite link will be sent "
"to the user."
msgstr ""
"E-poštni naslov ni potreben, vendar če je vnešeno, bo povabilo poslano do "
"uporabnika."
#: .\cookbook\forms.py:382
msgid "Name already taken."
msgstr ""
msgstr "Ime je že zasedeno."
#: .\cookbook\forms.py:393
msgid "Accept Terms and Privacy"
msgstr ""
msgstr "Sprejmi pogoje uporabe"
#: .\cookbook\forms.py:425
msgid ""
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
"g. low values mean more typos are ignored)."
msgstr ""
"Določa, kakšno je iskanje, če uporablja trigram podobnost ujemanje (npr. "
"nizke vrednosti pomenijo več, tipkanje se prezre)."
#: .\cookbook\forms.py:435
msgid ""
"Select type method of search. Click <a href=\"/docs/search/\">here</a> for "
"full desciption of choices."
msgstr ""
"Izberi metodo iskanja. Klikni <a href=\"/docs/search/\">tukaj</a> za "
"prikaz vseh izbir."
#: .\cookbook\forms.py:436
msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
msgstr ""
"Pri urejanju in uvozu receptov uporabite mehka ujemanja na enotah, ključnih "
"besedah in sestavinah."
#: .\cookbook\forms.py:438
msgid ""
"Fields to search ignoring accents. Selecting this option can improve or "
"degrade search quality depending on language"
msgstr ""
"Polja za iskanje prezrtih naglasov. Če izberete to možnost, lahko izboljšate "
"ali poslabšate kakovost iskanja, odvisno od jezika"
#: .\cookbook\forms.py:440
msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
msgstr ""
"Polja za iskanje delnih ujemajev. (npr. iskanje \"Pie\" vrne \"pie\" in "
"\"piece\" in \"soapie\")"
#: .\cookbook\forms.py:442
msgid ""
"Fields to search for beginning of word matches. (e.g. searching for 'sa' "
"will return 'salad' and 'sandwich')"
msgstr ""
"Polja za iskanje začetka ujemanja besed. (npr. iskanje \"sa\" vrne \"salad\" "
"in \"sandwich\")"
#: .\cookbook\forms.py:444
msgid ""
"Fields to 'fuzzy' search. (e.g. searching for 'recpie' will find 'recipe'.) "
"Note: this option will conflict with 'web' and 'raw' methods of search."
msgstr ""
"Polja za \"mehko\" iskanje. (npr. iskanje \"recpie\" bo našlo \"recipe\".)"
#: .\cookbook\forms.py:446
msgid ""

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,149 @@
# 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.AddField(
model_name='space',
name='show_facet_count',
field=models.BooleanField(default=False),
),
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,19 @@
# 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),
# ),
# removed due to quick fix in 0159 migration to maintain correct order
]

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

@@ -0,0 +1,20 @@
from django.db import migrations
from cookbook.models import SearchFields
def create_searchfields(apps, schema_editor):
SearchFields.objects.create(name='Units', field='steps__ingredients__unit__name')
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0167_userpreference_left_handed'),
]
operations = [
migrations.RunPython(
create_searchfields
),
]

View File

@@ -35,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):
@@ -54,6 +67,9 @@ class TreeManager(MP_NodeManager):
except self.model.DoesNotExist:
with scopes_disabled():
try:
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}
@@ -61,7 +77,10 @@ class TreeManager(MP_NodeManager):
for field in many_to_many:
field_model = getattr(obj, field).model
for related_obj in many_to_many[field]:
getattr(obj, field).add(field_model.objects.get(**dict(related_obj)))
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]:
@@ -78,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()
@@ -124,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
@@ -157,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)
@@ -167,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
@@ -245,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)
@@ -363,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)
@@ -393,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)
@@ -413,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')
@@ -447,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)
@@ -489,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')
@@ -500,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)
@@ -534,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"]),
@@ -660,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')
@@ -677,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():
@@ -703,11 +853,14 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
return f'Shopping list entry {self.id}'
def get_shared(self):
return self.shoppinglist_set.first().shared.all()
try:
return self.shoppinglist_set.first().shared.all()
except AttributeError:
return self.created_by.userpreference.shopping_share.all()
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
@@ -729,6 +882,12 @@ class ShoppingList(ExportModelOperationsMixin('shopping_list'), models.Model, Pe
def __str__(self):
return f'Shopping list {self.id}'
def get_shared(self):
try:
return self.shared.all() or self.created_by.userpreference.shopping_share.all()
except AttributeError:
return []
class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)

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

@@ -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 RecipeShoppingEditor
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='recipe_count')
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,24 +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
def count_recipes(self, obj):
return Recipe.objects.filter(**{self.recipe_filter: obj}, space=obj.space).count()
if obj.recipe_image:
return MEDIA_URL + obj.recipe_image
class CustomDecimalField(serializers.Field):
@@ -92,9 +82,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,20 +144,48 @@ class UserNameSerializer(WritableNestedModelSerializer):
fields = ('id', 'username')
class UserPreferenceSerializer(serializers.ModelSerializer):
plan_share = UserNameSerializer(many=True, read_only=True)
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)
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
def get_food_children_exist(self, obj):
space = getattr(self.context.get('request', None), 'space', None)
return Food.objects.filter(depth__gt=0, space=space).exists()
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_fractions', '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', 'food_children_exist'
)
@@ -255,25 +291,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
@@ -286,26 +308,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
@@ -369,27 +378,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()
@@ -399,20 +397,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', 'ignore_shopping'
)
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
class IngredientSerializer(WritableNestedModelSerializer):
@@ -457,12 +478,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'
)
@@ -478,6 +499,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
@@ -501,7 +526,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:
@@ -510,7 +535,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
@@ -521,6 +546,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
@@ -533,7 +559,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']
@@ -621,53 +647,134 @@ 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)
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):
SLR = RecipeShoppingEditor(user=validated_data['created_by'], space=validated_data['space'])
SLR.create(mealplan=mealplan, servings=validated_data['servings'])
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):
# TODO remove once old shopping list
if 'servings' in validated_data and self.context.get('view', None).__class__.__name__ != 'ShoppingListViewSet':
SLR = RecipeShoppingEditor(user=self.context['request'].user, space=self.context['request'].space)
SLR.edit_servings(servings=validated_data['servings'], id=instance.id)
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)
@@ -688,6 +795,7 @@ class ShoppingListSerializer(WritableNestedModelSerializer):
read_only_fields = ('id', 'created_by',)
# TODO deprecate
class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer):
entries = ShoppingListEntryCheckedSerializer(many=True, allow_null=True)
@@ -757,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):
@@ -802,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):
@@ -828,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):
@@ -847,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,135 @@
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 django_scopes import scope
from cookbook.models import Recipe, Step
from cookbook.helper.shopping_helper import RecipeShoppingEditor
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):
if not instance:
return
user = instance.get_owner()
with scope(space=instance.space):
slr_exists = instance.shoppinglistrecipe_set.exists()
if not created and slr_exists:
for x in instance.shoppinglistrecipe_set.all():
# assuming that permissions checks for the MealPlan have happened upstream
if instance.servings != x.servings:
SLR = RecipeShoppingEditor(id=x.id, user=user, space=instance.space)
SLR.edit_servings(servings=instance.servings)
# list_recipe = list_from_recipe(list_recipe=x, servings=instance.servings, space=instance.space)
elif not user.userpreference.mealplan_autoadd_shopping or not instance.recipe:
return
if created:
# if creating a mealplan - perform shopping list activities
# kwargs = {
# 'mealplan': instance,
# 'space': instance.space,
# 'created_by': user,
# 'servings': instance.servings
# }
SLR = RecipeShoppingEditor(user=user, space=instance.space)
SLR.create(mealplan=instance, servings=instance.servings)
# list_recipe = list_from_recipe(**kwargs)

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

@@ -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>
@@ -339,7 +339,6 @@
{% user_prefs request as prefs%}
{{ prefs|json_script:'user_preference' }}
</div>
{% block script %}

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

@@ -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 %}

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

@@ -498,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"
@@ -536,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"
@@ -586,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"
@@ -654,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;
@@ -693,7 +700,8 @@
import_duplicates: false,
recipe_files: [],
images: [],
mode: 'url'
mode: 'url',
options_limit:25
},
directives: {
tabindex: {
@@ -703,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")
@@ -715,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,
@@ -885,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
@@ -923,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

@@ -3,6 +3,7 @@ from gettext import gettext as _
import bleach
import markdown as md
from markdown.extensions.tables import TableExtension
from bleach_allowlist import markdown_attrs, markdown_tags
from django import template
from django.db.models import Avg
@@ -49,7 +50,7 @@ def markdown(value):
parsed_md = md.markdown(
value,
extensions=[
'markdown.extensions.fenced_code', 'tables',
'markdown.extensions.fenced_code', TableExtension(),
UrlizeExtension(), MarkdownFormatExtension()
]
)

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','shared':[]},
'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,248 @@
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
# test shared user can mark complete
x = shared_client.patch(
reverse(DETAIL_URL, args={sle[0].id}),
{'checked': True},
content_type='application/json'
)
r = json.loads(shared_client.get(reverse(LIST_URL)).content)
assert len(r) == count
# count unchecked entries
if not x.status_code == 404:
count = count-1
assert [x['checked'] for x in r].count(False) == count
# test shared user can delete
x = shared_client.delete(
reverse(
DETAIL_URL,
args={sle[1].id}
)
)
r = json.loads(shared_client.get(reverse(LIST_URL)).content)
assert len(r) == count
# count unchecked entries
if not x.status_code == 404:
count = count-1
assert [x['checked'] for x in r].count(False) == 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 3 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

View File

@@ -23,8 +23,8 @@ def test_list_permission(arg, request):
def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 2
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 0
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 2
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 0
with scopes_disabled():
recipe_1_s1.space = space_2
@@ -32,9 +32,9 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
Step.objects.update(space=Subquery(Step.objects.filter(pk=OuterRef('pk')).values('recipe__space')[:1]))
Ingredient.objects.update(space=Subquery(Ingredient.objects.filter(pk=OuterRef('pk')).values('step__recipe__space')[:1]))
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 0
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 2
assert json.loads(u1_s1.get(reverse(LIST_URL)).content)['count'] == 0
assert json.loads(u1_s2.get(reverse(LIST_URL)).content)['count'] == 2
@pytest.mark.parametrize("arg", [
['a_u', 403],

View File

@@ -49,7 +49,7 @@ def ing_3_s2(obj_3, space_2, u2_s2):
@pytest.fixture()
def sle_1_s1(obj_1, u1_s1, space_1):
e = ShoppingListEntry.objects.create(unit=obj_1, food=random_food(space_1, u1_s1))
e = ShoppingListEntry.objects.create(unit=obj_1, food=random_food(space_1, u1_s1), created_by=auth.get_user(u1_s1), space=space_1,)
s = ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, )
s.entries.add(e)
return e
@@ -57,12 +57,12 @@ def sle_1_s1(obj_1, u1_s1, space_1):
@pytest.fixture()
def sle_2_s1(obj_2, u1_s1, space_1):
return ShoppingListEntry.objects.create(unit=obj_2, food=random_food(space_1, u1_s1))
return ShoppingListEntry.objects.create(unit=obj_2, food=random_food(space_1, u1_s1), created_by=auth.get_user(u1_s1), space=space_1,)
@pytest.fixture()
def sle_3_s2(obj_3, u2_s2, space_2):
e = ShoppingListEntry.objects.create(unit=obj_3, food=random_food(space_2, u2_s2))
e = ShoppingListEntry.objects.create(unit=obj_3, food=random_food(space_2, u2_s2), created_by=auth.get_user(u2_s2), space=space_2)
s = ShoppingList.objects.create(created_by=auth.get_user(u2_s2), space=space_2)
s.entries.add(e)
return e

View File

@@ -1,11 +1,11 @@
from cookbook.models import UserPreference
import json
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, UserPreference
LIST_URL = 'api:userpreference-list'
DETAIL_URL = 'api:userpreference-detail'
@@ -109,3 +109,32 @@ def test_preference_delete(u1_s1, u2_s1):
)
)
assert r.status_code == 204
def test_default_inherit_fields(u1_s1, u1_s2, space_1, space_2):
food_inherit_fields = Food.inheritable_fields
assert len([x.field for x in food_inherit_fields]) > 0
# by default space food will not inherit any fields, so all of them will be ignored
assert space_1.food_inherit.all().count() == 0
r = u1_s1.get(
reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}),
)
assert len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) == 0
# inherit all possible fields
with scope(space=space_1):
space_1.food_inherit.add(*Food.inheritable_fields.values_list('id', flat=True))
assert space_1.food_inherit.all().count() == Food.inheritable_fields.count() > 0
# now by default, food is inheriting all of the possible fields
r = u1_s1.get(
reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}),
)
assert len([x['field'] for x in json.loads(r.content)['food_inherit_default']]) == space_1.food_inherit.all().count()
# other spaces and users in those spaces not effected
r = u1_s2.get(
reverse(DETAIL_URL, args={auth.get_user(u1_s2).id}),
)
assert space_2.food_inherit.all().count() == 0 == len([x['field'] for x in json.loads(r.content)['food_inherit_default']])

View File

@@ -5,14 +5,22 @@ import uuid
import pytest
from django.contrib import auth
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import Group, User
from django_scopes import scopes_disabled
from pytest_factoryboy import LazyFixture, register
from cookbook.models import Space, Recipe, Step, Ingredient, Food, Unit
from cookbook.models import Food, Ingredient, Recipe, Space, Step, Unit
from cookbook.tests.factories import FoodFactory, SpaceFactory, UserFactory
register(SpaceFactory, 'space_1')
register(SpaceFactory, 'space_2')
# register(FoodFactory, space=LazyFixture('space_2'))
# TODO refactor clients to be factories
# hack from https://github.com/raphaelm/django-scopes to disable scopes for all fixtures
# does not work on yield fixtures as only one yield can be used per fixture (i think)
@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_setup(fixturedef, request):
if inspect.isgeneratorfunction(fixturedef.func):
@@ -27,23 +35,23 @@ def enable_db_access_for_all_tests(db):
pass
@pytest.fixture()
def space_1():
with scopes_disabled():
return Space.objects.get_or_create(name='space_1')[0]
# @pytest.fixture()
# def space_1():
# with scopes_disabled():
# return Space.objects.get_or_create(name='space_1')[0]
@pytest.fixture()
def space_2():
with scopes_disabled():
return Space.objects.get_or_create(name='space_2')[0]
# @pytest.fixture()
# def space_2():
# with scopes_disabled():
# return Space.objects.get_or_create(name='space_2')[0]
# ---------------------- OBJECT FIXTURES ---------------------
def get_random_recipe(space_1, u1_s1):
r = Recipe.objects.create(
name=uuid.uuid4(),
name=str(uuid.uuid4()),
waiting_time=20,
working_time=20,
servings=4,
@@ -52,8 +60,8 @@ def get_random_recipe(space_1, u1_s1):
internal=True,
)
s1 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), space=space_1, )
s2 = Step.objects.create(name=uuid.uuid4(), instruction=uuid.uuid4(), space=space_1, )
s1 = Step.objects.create(name=str(uuid.uuid4()), instruction=str(uuid.uuid4()), space=space_1, )
s2 = Step.objects.create(name=str(uuid.uuid4()), instruction=str(uuid.uuid4()), space=space_1, )
r.steps.add(s1)
r.steps.add(s2)
@@ -63,8 +71,8 @@ def get_random_recipe(space_1, u1_s1):
Ingredient.objects.create(
amount=1,
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0],
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
note=uuid.uuid4(),
unit=Unit.objects.create(name=str(uuid.uuid4()), space=space_1, ),
note=str(uuid.uuid4()),
space=space_1,
)
)
@@ -73,8 +81,8 @@ def get_random_recipe(space_1, u1_s1):
Ingredient.objects.create(
amount=1,
food=Food.objects.get_or_create(name=str(uuid.uuid4()), space=space_1)[0],
unit=Unit.objects.create(name=uuid.uuid4(), space=space_1, ),
note=uuid.uuid4(),
unit=Unit.objects.create(name=str(uuid.uuid4()), space=space_1, ),
note=str(uuid.uuid4()),
space=space_1,
)
)
@@ -176,25 +184,17 @@ def create_user(client, space, **kwargs):
c = copy.deepcopy(client)
with scopes_disabled():
group = kwargs.pop('group', None)
username = kwargs.pop('username', uuid.uuid4())
user = UserFactory(space=space, groups=group)
user = User.objects.create(username=username, **kwargs)
if group:
user.groups.add(Group.objects.get(name=group))
user.userpreference.space = space
user.userpreference.save()
c.force_login(user)
return c
# anonymous user
@pytest.fixture()
def a_u(client):
return copy.deepcopy(client)
# users without any group
@pytest.fixture()
def ng1_s1(client, space_1):
return create_user(client, space_1)

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