Compare commits

..

192 Commits

Author SHA1 Message Date
vabene1111
0c94cf1c2e Merge pull request #2870 from TandoorRecipes/dependabot/npm_and_yarn/vue/follow-redirects-1.15.4
Bump follow-redirects from 1.15.2 to 1.15.4 in /vue
2024-01-19 16:44:06 +08:00
vabene1111
1673254934 Merge pull request #2873 from TandoorRecipes/dependabot/pip/jinja2-3.1.3
Bump jinja2 from 3.1.2 to 3.1.3
2024-01-19 16:43:50 +08:00
vabene1111
0493ef7e3a Merge pull request #2872 from FaySmash/patch-1
Improved the understandability of the postgres upgrade steps
2024-01-19 16:13:28 +08:00
Tomasz Klimczak
1fd6a47e9c Translated using Weblate (Polish)
Currently translated at 100.0% (554 of 554 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/
2024-01-18 00:19:56 +00:00
vabene1111
bb52f8902d added max spaces per user + added custom app name + fixed theming tests 2024-01-15 07:41:51 +08:00
vabene1111
35eff630ff updated theming tests 2024-01-14 15:39:02 +08:00
vabene1111
8d90fada1d fixed theming breaking for users without space 2024-01-14 15:33:14 +08:00
vabene1111
2ba2b97f9c moved manifest to use main theming function 2024-01-14 08:40:05 +08:00
vabene1111
26408c33f4 removed debug code 2024-01-13 07:42:54 +08:00
Cilantro4858
72b0bd7f1e Translated using Weblate (German)
Currently translated at 100.0% (554 of 554 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2024-01-12 22:40:47 +00:00
dependabot[bot]
45f0413fb9 Bump jinja2 from 3.1.2 to 3.1.3
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-11 19:51:57 +00:00
FaySmash
38c464ebae Improved the understandability of the postgres upgrade steps
I improved the understandability some parts in the psql examples for someone not familiar with the psql syntax.
2024-01-11 17:56:45 +01:00
Jan Kubošek
b4f158b913 Translated using Weblate (Czech)
Currently translated at 100.0% (554 of 554 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2024-01-11 02:19:55 +00:00
dependabot[bot]
da49b6bda0 Bump follow-redirects from 1.15.2 to 1.15.4 in /vue
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-10 08:50:00 +00:00
Jan Kubošek
e7d9d7b7b3 Translated using Weblate (Czech)
Currently translated at 100.0% (554 of 554 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2024-01-09 21:53:32 +00:00
Mára Štěpánek
5f7a57a258 Translated using Weblate (Czech)
Currently translated at 92.0% (510 of 554 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2024-01-09 18:40:00 +00:00
Jan Kubošek
4b1a80a0ed Translated using Weblate (Czech)
Currently translated at 92.0% (510 of 554 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2024-01-09 18:40:00 +00:00
Mára Štěpánek
8efc3de11f Translated using Weblate (Czech)
Currently translated at 90.5% (491 of 542 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2024-01-09 12:07:20 +00:00
Jan Kubošek
1f3cd11964 Translated using Weblate (Czech)
Currently translated at 90.5% (491 of 542 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2024-01-09 12:07:20 +00:00
Jan Kubošek
94cfc36ed5 Translated using Weblate (Czech)
Currently translated at 100.0% (362 of 362 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/cs/
2024-01-09 12:07:20 +00:00
vabene1111
d493ba72a1 fixed mealplan to date set wrongly when open multiple times 2024-01-08 20:24:23 +08:00
vabene1111
71e5484f0c test for theming function + sticky nav 2024-01-07 22:34:59 +08:00
vabene1111
761e423bde fixed markdown info 2024-01-07 18:17:31 +08:00
vabene1111
c8e674da16 all themes in one function 2024-01-07 18:07:47 +08:00
vabene1111
6f3d4491ed implemented user settings 2024-01-07 17:51:33 +08:00
vabene1111
54e2615c86 cleaned up into single flow 2024-01-07 17:24:20 +08:00
vabene1111
77942a7144 Merge branch 'develop' into beta 2024-01-07 17:17:22 +08:00
vabene1111
5a5ce4d736 moved theming functions to main tag 2024-01-07 17:17:14 +08:00
vabene1111
0d966b5e59 Merge branch 'develop' into beta 2024-01-07 16:55:22 +08:00
vabene1111
1dda4126c1 fxied theming tags 2024-01-07 16:55:12 +08:00
vabene1111
5ffe821407 Merge branch 'develop' into beta 2024-01-07 08:13:36 +08:00
vabene1111
f9bfb8e258 added custom logo to space manage view 2024-01-07 08:12:20 +08:00
vabene1111
c6fa635af2 basics of custom icons 2024-01-06 23:23:17 +08:00
vabene1111
50e1eaf645 fixed bg color for unauthenticated nav 2024-01-06 22:35:22 +08:00
vabene1111
953dc75a8d added ability to change unauthenticated theme 2024-01-06 21:43:40 +08:00
vabene1111
ac5333d0e7 cleaned .env template and created dedicated docs page for environment configuration 2024-01-06 15:34:55 +08:00
vabene1111
ecf985f5e3 change gunicorn media default 2024-01-06 14:38:27 +08:00
vabene1111
b6d4c4c3b8 Merge branch 'develop' into beta 2024-01-03 15:13:51 +01:00
vabene1111
30f3a697f0 fixed space theme defaults in model 2024-01-03 15:13:39 +01:00
vabene1111
42ced25e10 improved settings override message 2024-01-03 15:05:44 +01:00
vabene1111
6011cf359f Merge pull request #2853 from AquaticLava/Auto-Planner
Auto planner bug fix
2024-01-03 14:37:37 +01:00
AquaticLava
f57acc412b Merge remote-tracking branch 'origin/Auto-Planner' into Auto-Planner 2024-01-02 18:44:39 -07:00
AquaticLava
200cacb9ac changed keywords to be index based. 2024-01-02 18:44:23 -07:00
AquaticLava
5c89173373 changed random recipe to be index based. 2024-01-02 18:43:22 -07:00
AquaticLava
61b67cd37a Merge remote-tracking branch 'origin/Auto-Planner' into Auto-Planner 2024-01-02 15:38:47 -07:00
vabene1111
12c2f2f7aa Merge branch 'develop' into beta 2024-01-01 22:14:27 +01:00
vabene1111
3d8b1d6ccb lots of theming related changes
- upload a custom logo for your space
    - space settings can override user settings for theming
    - spaces can upload custom CSS overrides
    - allow users to disable showing the tandoor/space logo
    - allow changing navigation background color to any color desired
    - allow switching navigation text color between dark/light (different effects depending on theme)
2024-01-01 22:14:01 +01:00
Arnon Meshoulam
aa0d6b5a6b Translated using Weblate (Hebrew)
Currently translated at 95.3% (517 of 542 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/he/
2024-01-01 20:19:56 +00:00
Murphy
64ed75156c Translated using Weblate (German)
Currently translated at 98.7% (535 of 542 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2024-01-01 20:19:56 +00:00
vabene1111
c6d41e8810 fixed migrations 200 and 204 2024-01-01 19:11:37 +01:00
vabene1111
2a10843101 compiled translations 2024-01-01 15:06:58 +01:00
vabene1111
f861b39d05 Merge pull request #2833 from luc-ass/develop
Fix truenas_portainer compose example/documentation
2024-01-01 14:55:28 +01:00
vabene1111
5c18c09944 Merge branch 'develop' into develop 2024-01-01 14:55:21 +01:00
vabene1111
1bd5f96029 Merge pull request #2792 from Sriyukthika26/my-contribution
Update truenas_portainer.md
2024-01-01 14:53:47 +01:00
vabene1111
988df4eb00 Merge pull request #2823 from smilerz/admin_updates
Admin updates
2024-01-01 14:53:01 +01:00
vabene1111
bf61b6474e fixed ingredient note field to high 2024-01-01 14:51:20 +01:00
AquaticLava
be177cf258 Merge remote-tracking branch 'origin/Auto-Planner' into Auto-Planner 2023-12-31 11:20:06 -07:00
Jaan
5059abc232 Translated using Weblate (Russian)
Currently translated at 63.4% (344 of 542 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/ru/
2023-12-30 15:19:57 +00:00
smilerz
cb63bb2615 avoid recursion in ingredient.__str__ 2023-12-28 10:44:51 -06:00
smilerz
7ca5a34b28 fixed recursion in Step.__str__() 2023-12-28 10:00:15 -06:00
Lucas Gasenzer
a7ea7a8987 fix code block and replace tab with spaces 2023-12-27 10:43:56 +01:00
vabene1111
8d7b4f614c improved mobile shopping entry adding layout 2023-12-22 09:25:30 +01:00
vabene1111
df67d3ce7b improved shopping context dark theme 2023-12-22 09:25:18 +01:00
vabene1111
54119ed1ec Merge branch 'develop' into beta 2023-12-22 08:38:25 +01:00
smilerz
26f694576a update __str__() on Step and Ingredient models 2023-12-20 15:55:02 -06:00
smilerz
7a5b744ff0 order recipes in admin 2023-12-20 15:49:54 -06:00
smilerz
4058c997de updates to admin pages 2023-12-20 15:46:28 -06:00
vabene1111
4de9be5c89 Merge pull request #2808 from smilerz/add_mealtype_filter
add ability to filter meal plans based on type
2023-12-20 15:55:09 +01:00
vabene1111
34ee03b720 Merge pull request #2818 from smilerz/modal_updates
Modal updates
2023-12-20 15:51:24 +01:00
smilerz
48dacf46c3 updated RecipeSwitcher with new MealPlan API format 2023-12-19 16:50:32 -06:00
smilerz
181c270b34 added substitute children to food edit modal 2023-12-19 15:22:26 -06:00
smilerz
e89c3887ec remove reference to facets on Space page 2023-12-19 15:09:18 -06:00
smilerz
99cd9bfb5b update meal_type filter on MealPlan to be a list 2023-12-19 12:59:24 -06:00
smilerz
8bbccad7a9 updated API correclty this time 2023-12-19 12:59:24 -06:00
smilerz
a59a78f44c update meal-plan API on MealPlanStore 2023-12-19 12:59:24 -06:00
smilerz
205bf5253d add ability to filter meal plans based on type 2023-12-19 12:59:19 -06:00
vabene1111
0fed6b9fb3 added migration status to system page 2023-12-16 14:03:32 +01:00
vabene1111
dd3e91e10d added ability to set rate limiting for url import 2023-12-16 09:19:12 +01:00
vabene1111
76b84898f6 lmit ingredient parser to 512 characters to prevent too complex computations 2023-12-16 09:08:50 +01:00
vabene1111
05d971835f autoamtically keep meal plan to date relative to from date 2023-12-16 08:40:20 +01:00
vabene1111
0a814fa896 dont hide ingredients in edit when hiding them in view 2023-12-16 08:18:05 +01:00
vabene1111
05ba11a48e Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2023-12-16 08:09:22 +01:00
vabene1111
6a7a22626e use commit hash as version number if not on a tagged release 2023-12-16 08:09:18 +01:00
vabene1111
1635a3a335 Merge pull request #2794 from TyreceDJ/mobileCalender/sort
Sorted by current day in meal plan
2023-12-16 07:57:18 +01:00
vabene1111
1d84e7851b Merge pull request #2706 from ambroisie/bump-allauth
Bump django-allauth from 0.54.0 to 0.58.1
2023-12-16 07:54:08 +01:00
vabene1111
44d1cc3a30 Merge pull request #2802 from smilerz/automation_fixes
kw automation not applying during url import
2023-12-16 07:48:49 +01:00
vabene1111
04b4f552f8 comment out orphaned files 2023-12-16 07:40:09 +01:00
vabene1111
6214176fe5 Merge pull request #2730 from smilerz/orphan_file_cleanup
view and delete orphaned files
2023-12-16 07:36:08 +01:00
vabene1111
205dc11125 changed raspi docs 2023-12-16 07:30:39 +01:00
smilerz
ba5112e138 kw automation not applying during url import 2023-12-12 13:49:23 -06:00
Khuslen Misheel
0e34cc72d5 Proper fix for Calendar 2023-12-10 13:46:55 -05:00
Khuslen Misheel
31c6defc93 Only fixed current day in meal plan 2023-12-10 13:44:01 -05:00
vabene1111
d4c544bb4b partially working replace logic 2023-12-10 16:32:12 +01:00
vabene1111
2b05efeff6 Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2023-12-10 16:03:18 +01:00
vabene1111
d7ddcd3214 playing around with codemirror 2023-12-10 16:03:14 +01:00
Robin Wilmet
29133f4236 Translated using Weblate (French)
Currently translated at 96.1% (521 of 542 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2023-12-10 14:19:57 +00:00
Robin Wilmet
b440b09be5 Translated using Weblate (French)
Currently translated at 93.0% (456 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/fr/
2023-12-10 14:19:57 +00:00
smilerz
ed1f656167 fix version information on system page 2023-12-10 07:54:44 -06:00
smilerz
4f3e6d3765 added Postgres version to system page.
Added warnings for out of date Postgres versions
2023-12-10 07:54:43 -06:00
smilerz
46a50d7835 view and delete orphaned files
miscelaneous bug fixes discovered during testing
2023-12-10 07:54:37 -06:00
Khuslen Misheel
65513a8f60 Sorted by current day in meal plan 2023-12-09 15:19:40 -05:00
Sriyukthika
044ed1ec18 Update truenas_portainer.md Spelling Error 2023-12-10 00:44:15 +05:30
Sriyukthika
8f53b399c6 Update truenas_portainer.md 2023-12-09 23:58:26 +05:30
Bruno BELANYI
702c1d67d3 Bump django-allauth from 0.54.0 to 0.58.1
See the backwards incompatible changes [1].

[1]: https://docs.allauth.org/en/latest/release-notes/recent.html#id10
2023-12-06 21:59:20 +00:00
smilerz
c654cc469a Update RecipeEditView.vue
fixes #2781
2023-12-05 07:53:33 -06:00
Ferenc
8df846c9c2 Translated using Weblate (Hungarian)
Currently translated at 85.3% (460 of 539 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/hu/
2023-12-05 09:15:12 +00:00
Ferenc
7070f6c964 Translated using Weblate (Hungarian)
Currently translated at 98.7% (484 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/hu/
2023-12-05 09:15:12 +00:00
vabene1111
b454960676 some playing around 2023-12-03 15:48:37 +01:00
vabene1111
abf8f79136 Merge branch 'develop'
# Conflicts:
#	docs/faq.md
2023-12-03 14:10:28 +01:00
vabene1111
fd028047d6 clean test view 2023-12-03 14:09:58 +01:00
vabene1111
bd0e1bcefe Merge branch 'develop' into beta 2023-12-02 21:32:24 +01:00
vabene1111
a2aa0dc3b9 automatically open ingredient editor in new tab 2023-12-02 21:26:54 +01:00
vabene1111
1758aebb73 choice input datatype detection 2023-12-02 21:13:14 +01:00
vabene1111
ecffe30062 dont show property warning for 0 values any more 2023-12-02 21:07:11 +01:00
vabene1111
21653465e0 show order and add property types from property editor 2023-12-02 20:37:23 +01:00
vabene1111
f3e11e6358 fixed and improvements to property editor 2023-12-02 20:14:12 +01:00
vabene1111
0c381ed46c fixed recipe card description overlay 2023-12-02 19:28:34 +01:00
vabene1111
fd978f9c19 fixed copying recipes with properties 2023-12-02 18:58:44 +01:00
vabene1111
b069a49954 Merge pull request #2771 from tourn/bugfix/database-url-with-port
Fix parsing DATABASE_URL with port number
2023-12-02 18:48:41 +01:00
vabene1111
11c8422fbb fixed youtube import and handle resize without ingredients 2023-12-02 18:45:34 +01:00
vabene1111
2cb010c8b4 Merge pull request #2763 from smilerz/fix_long_description
truncated long description
2023-12-02 18:11:59 +01:00
vabene1111
8f96c7f0a3 Merge pull request #2768 from TandoorRecipes/dependabot/pip/pytest-7.4.3
Bump pytest from 7.3.1 to 7.4.3
2023-12-02 18:11:34 +01:00
vabene1111
3054297357 improved error handling and fixed meal plan api 2023-12-02 18:11:09 +01:00
vabene1111
3e083e2168 fully integrated property editor 2023-12-02 18:02:22 +01:00
vabene1111
d1174ea50d fixed api comment 2023-12-02 17:42:04 +01:00
vabene1111
fe11b88fd0 pretty nice property editor 2023-12-02 17:41:02 +01:00
vabene1111
a3a2433d2a made to_date field optional in meal plan api 2023-12-02 16:29:37 +01:00
vabene1111
92be2db9fd mostly working property editor 2023-12-02 15:35:33 +01:00
vabene1111
be2f759048 add disabled capabilities to generic multiselect 2023-12-02 15:22:27 +01:00
Marco Agostino
52e88ddfd3 Translated using Weblate (Italian)
Currently translated at 86.4% (466 of 539 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/it/
2023-12-02 11:19:56 +00:00
Daniel Latzer
fe208e9844 Fix parsing DATABASE_URL with port number 2023-12-02 09:42:29 +01:00
dependabot[bot]
15e7f32001 Bump pytest from 7.3.1 to 7.4.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.1 to 7.4.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.3.1...7.4.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-01 00:59:26 +00:00
smilerz
745c045f06 truncated long description 2023-11-30 16:24:24 -06:00
Anders Obro
4b5abec458 Translated using Weblate (Danish)
Currently translated at 99.8% (535 of 536 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/da/
2023-11-30 13:19:57 +00:00
vabene1111
f6ed49b5c4 property editor 2023-11-29 22:04:23 +01:00
vabene1111
0a0e3a48c3 first working property editor prototype 2023-11-29 21:20:10 +01:00
vabene1111
cce2407bc0 Merge pull request #2758 from smilerz/updated_documentation
Updated documentation
2023-11-29 19:28:46 +01:00
vabene1111
9b18cab145 Update settings.py 2023-11-29 19:28:19 +01:00
smilerz
8b9a09b268 Update settings.py 2023-11-29 10:56:33 -06:00
vabene1111
db1709cef7 recipe context add to meal plan default to_date 2023-11-29 17:50:01 +01:00
vabene1111
4844e5cbc8 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-11-29 17:48:35 +01:00
vabene1111
e90781983f fixed meal plan multi period arrow breaking view #2678 2023-11-29 17:48:25 +01:00
vabene1111
86496069b3 Merge pull request #2728 from jrester/improve-import-error-msg
Improve import error messages
2023-11-29 17:24:06 +01:00
vabene1111
e1aee23c54 Merge pull request #2759 from TandoorRecipes/dependabot/pip/cryptography-41.0.6
Bump cryptography from 41.0.4 to 41.0.6
2023-11-29 17:20:22 +01:00
Jan-Niklas Weghorn
1000badd2f remove venv from .dockerignore 2023-11-29 11:41:19 +01:00
dependabot[bot]
9ae0b50558 Bump cryptography from 41.0.4 to 41.0.6
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.4 to 41.0.6.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.4...41.0.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-29 00:16:22 +00:00
smilerz
f69813f729 added installing extensions if necessary 2023-11-28 16:22:17 -06:00
smilerz
fcb2c07acd Update updating.md 2023-11-28 15:52:21 -06:00
smilerz
a076d20cba Update updating.md 2023-11-28 15:51:51 -06:00
smilerz
bae777bc69 Update updating.md 2023-11-28 15:51:06 -06:00
smilerz
49781bfa7f Update updating.md 2023-11-28 15:50:35 -06:00
smilerz
72d3ace0f9 Update updating.md 2023-11-28 15:49:20 -06:00
smilerz
7f44a6f187 Update updating.md 2023-11-28 15:48:33 -06:00
smilerz
92b8799d26 Update faq.md 2023-11-28 15:47:14 -06:00
smilerz
7f1eecddc4 updated documentation for postgres upgrade
installing pgbackup container
installing with DockSTARTer
2023-11-28 15:45:27 -06:00
vabene1111
4723a7ecbd added pg upgrade faq 2023-11-28 20:38:45 +01:00
smilerz
6af28e6fe5 alpine uses TZ to set OS timezone, to stay consistent
changed TIMEZONE env variable to TZ
added deprecated warning to TIMEZONE
2023-11-28 11:47:23 -06:00
vabene1111
899a9955fb Merge branch 'develop' into beta 2023-11-27 20:21:37 +01:00
Jan-Niklas Weghorn
3c08e3a3f1 Improve import error messages 2023-11-10 13:28:20 +01:00
vabene1111
3cabe85091 Merge branch 'develop' into beta 2023-10-05 19:05:09 +02:00
AquaticLava
530d6b0cb6 changed random recipe to be index based. 2023-07-23 11:51:07 -06:00
vabene1111
9da66c9f6c Merge branch 'develop' into beta 2023-06-29 17:26:54 +02:00
vabene1111
124211a2f4 Merge branch 'develop' into beta 2023-06-27 16:11:45 +02:00
vabene1111
71555fee28 Merge branch 'develop' into beta 2023-06-26 21:06:56 +02:00
vabene1111
05a99c9b64 Merge branch 'develop' into beta 2023-06-20 16:49:07 +02:00
vabene1111
32690f04b2 Merge branch 'develop' into beta 2023-06-20 15:46:51 +02:00
vabene1111
29b74557a6 Merge branch 'develop' into beta 2023-06-13 13:24:05 +02:00
vabene1111
c43e7e0331 Merge branch 'develop' into beta 2023-05-29 17:51:17 +02:00
vabene1111
fe7fd7700d Merge branch 'develop' into beta 2023-05-29 12:44:12 +02:00
vabene1111
c6ef0e0087 Merge branch 'develop' into beta 2023-05-26 16:11:30 +02:00
vabene1111
6149f693ab Merge branch 'develop' into beta 2023-04-26 07:46:56 +02:00
vabene1111
daef57823f Merge branch 'develop' into beta 2023-03-28 15:43:44 +02:00
vabene1111
5c7b9a93ae Merge branch 'master' into beta 2023-03-14 23:10:44 +01:00
vabene1111
b681364f95 Merge branch 'develop' into beta 2023-02-27 17:26:27 +01:00
vabene1111
40d14eeb9f Merge branch 'develop' into beta 2023-02-24 23:32:45 +01:00
vabene1111
46b09f11b6 Merge branch 'develop' into beta 2023-02-24 20:40:41 +01:00
vabene1111
900291dc5f Merge branch 'develop' into beta 2023-02-12 13:30:40 +01:00
vabene1111
e9f9134e2e Merge branch 'develop' into beta 2023-02-11 17:57:48 +01:00
vabene1111
8fe11b12f8 Merge branch 'develop' into beta 2023-01-27 15:52:54 +01:00
vabene1111
a1cfb7ad9f Merge branch 'develop' into beta 2023-01-20 14:58:31 +01:00
vabene1111
2bddf21175 Merge branch 'develop' into beta 2023-01-19 19:14:47 +01:00
vabene1111
aa5490adb3 Merge branch 'develop' into beta 2023-01-07 10:32:48 +01:00
vabene1111
bea089dd5e Merge branch 'develop' into beta 2022-11-09 13:23:48 +01:00
vabene1111
2c7237adaa Merge branch 'develop' into beta 2022-09-21 16:32:53 +02:00
vabene1111
98af1e1e4c Merge branch 'develop' into beta 2022-09-15 19:05:57 +02:00
vabene1111
4a1aee38a3 Merge branch 'develop' into beta 2022-08-05 18:02:48 +02:00
vabene1111
92c21bc382 Merge branch 'develop' into beta 2022-08-05 16:55:00 +02:00
vabene1111
ba748cc5fe Merge branch 'develop' into beta 2022-07-11 23:42:31 +02:00
vabene1111
22b1a9634a Merge branch 'develop' into beta 2022-07-07 19:17:21 +02:00
vabene1111
eeb5395efc Merge branch 'develop' into beta 2022-07-01 11:58:40 +02:00
vabene1111
6ea259596a Merge branch 'develop' into beta 2022-06-26 12:54:24 +02:00
vabene1111
49275a96fe Merge branch 'develop' into beta 2022-06-20 16:53:31 +02:00
106 changed files with 8090 additions and 1743 deletions

View File

@@ -1,185 +1,15 @@
# only set this to true when testing/debugging
# when unset: 1 (true) - dont unset this, just for development
DEBUG=0
SQL_DEBUG=0
DEBUG_TOOLBAR=0
# Gunicorn log level for debugging (default value is "info" when unset)
# (see https://docs.gunicorn.org/en/stable/settings.html#loglevel for available settings)
# GUNICORN_LOG_LEVEL="debug"
# HTTP port to bind to
# TANDOOR_PORT=8080
# hosts the application can run under e.g. recipes.mydomain.com,cooking.mydomain.com,...
ALLOWED_HOSTS=*
# Cross Site Request Forgery protection
# (https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-CSRF_TRUSTED_ORIGINS)
# CSRF_TRUSTED_ORIGINS = []
# Cross Origin Resource Sharing
# (https://github.com/adamchainz/django-cors-header)
# CORS_ALLOW_ALL_ORIGINS = True
# ---------------------------------------------------------------------------
# This template contains only required options.
# Visit the docs to find more https://docs.tandoor.dev/system/configuration/
# ---------------------------------------------------------------------------
# random secret key, use for example `base64 /dev/urandom | head -c50` to generate one
# ---------------------------- AT LEAST ONE REQUIRED -------------------------
SECRET_KEY=
SECRET_KEY_FILE=
# ---------------------------------------------------------------
# your default timezone See https://timezonedb.com/time-zones for a list of timezones
TIMEZONE=Europe/Berlin
# add only a database password if you want to run with the default postgres, otherwise change settings accordingly
DB_ENGINE=django.db.backends.postgresql
# DB_OPTIONS= {} # e.g. {"sslmode":"require"} to enable ssl
POSTGRES_HOST=db_recipes
POSTGRES_DB=djangodb
POSTGRES_PORT=5432
POSTGRES_USER=djangouser
# ---------------------------- AT LEAST ONE REQUIRED -------------------------
POSTGRES_PASSWORD=
POSTGRES_PASSWORD_FILE=
# ---------------------------------------------------------------
POSTGRES_DB=djangodb
# database connection string, when used overrides other database settings.
# format might vary depending on backend
# DATABASE_URL = engine://username:password@host:port/dbname
# the default value for the user preference 'fractions' (enable/disable fraction support)
# default: disabled=0
FRACTION_PREF_DEFAULT=0
# the default value for the user preference 'comments' (enable/disable commenting system)
# default comments enabled=1
COMMENT_PREF_DEFAULT=1
# Users can set a amount of time after which the shopping list is refreshed when they are in viewing mode
# This is the minimum interval users can set. Setting this to low will allow users to refresh very frequently which
# might cause high load on the server. (Technically they can obviously refresh as often as they want with their own scripts)
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/)
# 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 /
# this is not required if you are just using a subfolder
# This can either be a relative path from the applications base path or the url of an external host
# STATIC_URL=/static/
# If mediafiles are stored at a different location uncomment and change accordingly, MUST END IN /
# this is not required if you are just using a subfolder
# This can either be a relative path from the applications base path or the url of an external host
# MEDIA_URL=/media/
# Serve mediafiles directly using gunicorn. Basically everyone recommends not doing this. Please use any of the examples
# provided that include an additional nxginx container to handle media file serving.
# If you know what you are doing turn this back on (1) to serve media files using djangos serve() method.
# when unset: 1 (true) - this is temporary until an appropriate amount of time has passed for everyone to migrate
GUNICORN_MEDIA=0
# GUNICORN SERVER RELATED SETTINGS (see https://docs.gunicorn.org/en/stable/design.html#how-many-workers for recommended settings)
# GUNICORN_WORKERS=1
# GUNICORN_THREADS=1
# S3 Media settings: store mediafiles in s3 or any compatible storage backend (e.g. minio)
# as long as S3_ACCESS_KEY is not set S3 features are disabled
# S3_ACCESS_KEY=
# S3_SECRET_ACCESS_KEY=
# S3_BUCKET_NAME=
# S3_REGION_NAME= # default none, set your region might be required
# S3_QUERYSTRING_AUTH=1 # default true, set to 0 to serve media from a public bucket without signed urls
# S3_QUERYSTRING_EXPIRE=3600 # number of seconds querystring are valid for
# S3_ENDPOINT_URL= # when using a custom endpoint like minio
# S3_CUSTOM_DOMAIN= # when using a CDN/proxy to S3 (see https://github.com/TandoorRecipes/recipes/issues/1943)
# Email Settings, see https://docs.djangoproject.com/en/3.2/ref/settings/#email-host
# Required for email confirmation and password reset (automatically activates if host is set)
# EMAIL_HOST=
# EMAIL_PORT=
# EMAIL_HOST_USER=
# EMAIL_HOST_PASSWORD=
# EMAIL_USE_TLS=0
# EMAIL_USE_SSL=0
# email sender address (default 'webmaster@localhost')
# DEFAULT_FROM_EMAIL=
# prefix used for account related emails (default "[Tandoor Recipes] ")
# ACCOUNT_EMAIL_SUBJECT_PREFIX=
# allow authentication via the REMOTE-USER header (can be used for e.g. authelia).
# ATTENTION: Leave off if you don't know what you are doing! Enabling this without proper configuration will enable anybody
# to login with any username!
# See docs for additional information: https://docs.tandoor.dev/features/authentication/#reverse-proxy-authentication
# when unset: 0 (false)
REMOTE_USER_AUTH=0
# Default settings for spaces, apply per space and can be changed in the admin view
# SPACE_DEFAULT_MAX_RECIPES=0 # 0=unlimited recipes
# SPACE_DEFAULT_MAX_USERS=0 # 0=unlimited users per space
# SPACE_DEFAULT_MAX_FILES=0 # Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.
# SPACE_DEFAULT_ALLOW_SHARING=1 # Allow users to share recipes with public links
# allow people to create local accounts on your application instance (without an invite link)
# social accounts will always be able to sign up
# when unset: 0 (false)
# ENABLE_SIGNUP=0
# If signup is enabled you might want to add a captcha to it to prevent spam
# HCAPTCHA_SITEKEY=
# HCAPTCHA_SECRET=
# if signup is enabled you might want to provide urls to data protection policies or terms and conditions
# TERMS_URL=
# PRIVACY_URL=
# IMPRINT_URL=
# enable serving of prometheus metrics under the /metrics path
# ATTENTION: view is not secured (as per the prometheus default way) so make sure to secure it
# trough your web server (or leave it open of you dont care if the stats are exposed)
# ENABLE_METRICS=0
# allows you to setup OAuth providers
# see docs for more information https://docs.tandoor.dev/features/authentication/
# SOCIAL_PROVIDERS = allauth.socialaccount.providers.github, allauth.socialaccount.providers.nextcloud,
# Should a newly created user from a social provider get assigned to the default space and given permission by default ?
# ATTENTION: This feature might be deprecated in favor of a space join and public viewing system in the future
# default 0 (false), when 1 (true) users will be assigned space and group
# SOCIAL_DEFAULT_ACCESS = 1
# if SOCIAL_DEFAULT_ACCESS is used, which group should be added
# SOCIAL_DEFAULT_GROUP=guest
# Django session cookie settings. Can be changed to allow a single django application to authenticate several applications
# when running under the same database
# SESSION_COOKIE_DOMAIN=.example.com
# SESSION_COOKIE_NAME=sessionid # use this only to not interfere with non unified django applications under the same top level domain
# by default SORT_TREE_BY_NAME is disabled this will store all Keywords and Food in the order they are created
# enabling this setting makes saving new keywords and foods very slow, which doesn't matter in most usecases.
# however, when doing large imports of recipes that will create new objects, can increase total run time by 10-15x
# Keywords and Food can be manually sorted by name in Admin
# This value can also be temporarily changed in Admin, it will revert the next time the application is started
# This will be fixed/changed in the future by changing the implementation or finding a better workaround for sorting
# SORT_TREE_BY_NAME=0
# LDAP authentication
# default 0 (false), when 1 (true) list of allowed users will be fetched from LDAP server
#LDAP_AUTH=
#AUTH_LDAP_SERVER_URI=
#AUTH_LDAP_BIND_DN=
#AUTH_LDAP_BIND_PASSWORD=
#AUTH_LDAP_USER_SEARCH_BASE_DN=
#AUTH_LDAP_TLS_CACERTFILE=
#AUTH_LDAP_START_TLS=
# Enables exporting PDF (see export docs)
# Disabled by default, uncomment to enable
# ENABLE_PDF_EXPORT=1
# Recipe exports are cached for a certain time by default, adjust time if needed
# EXPORT_FILE_CACHE_DURATION=600

View File

@@ -3,6 +3,7 @@
<words>
<w>pinia</w>
<w>selfhosted</w>
<w>unapplied</w>
</words>
</dictionary>
</component>

View File

@@ -1,5 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Default" />
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>

View File

@@ -60,9 +60,9 @@ admin.site.register(UserSpace, UserSpaceAdmin)
class UserPreferenceAdmin(admin.ModelAdmin):
list_display = ('name', 'theme', 'nav_color', 'default_page',)
list_display = ('name', 'theme', 'default_page')
search_fields = ('user__username',)
list_filter = ('theme', 'nav_color', 'default_page',)
list_filter = ('theme', 'default_page',)
date_hierarchy = 'created_at'
filter_horizontal = ('plan_share', 'shopping_share',)
@@ -108,11 +108,16 @@ class SupermarketCategoryInline(admin.TabularInline):
class SupermarketAdmin(admin.ModelAdmin):
list_display = ('name', 'space',)
inlines = (SupermarketCategoryInline,)
class SupermarketCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'space',)
admin.site.register(Supermarket, SupermarketAdmin)
admin.site.register(SupermarketCategory)
admin.site.register(SupermarketCategory, SupermarketCategoryAdmin)
class SyncLogAdmin(admin.ModelAdmin):
@@ -163,10 +168,18 @@ def delete_unattached_steps(modeladmin, request, queryset):
class StepAdmin(admin.ModelAdmin):
list_display = ('name', 'order',)
search_fields = ('name',)
list_display = ('recipe_and_name', 'order', 'space')
ordering = ('recipe__name', 'name', 'space',)
search_fields = ('name', 'recipe__name')
actions = [delete_unattached_steps]
@staticmethod
@admin.display(description="Name")
def recipe_and_name(obj):
if not obj.recipe_set.exists():
return f"Orphaned Step{'':s if not obj.name else f': {obj.name}'}"
return f"{obj.recipe_set.first().name}: {obj.name}" if obj.name else obj.recipe_set.first().name
admin.site.register(Step, StepAdmin)
@@ -183,8 +196,9 @@ def rebuild_index(modeladmin, request, queryset):
class RecipeAdmin(admin.ModelAdmin):
list_display = ('name', 'internal', 'created_by', 'storage')
list_display = ('name', 'internal', 'created_by', 'storage', 'space')
search_fields = ('name', 'created_by__username')
ordering = ('name', 'created_by__username',)
list_filter = ('internal',)
date_hierarchy = 'created_at'
@@ -198,7 +212,14 @@ class RecipeAdmin(admin.ModelAdmin):
admin.site.register(Recipe, RecipeAdmin)
admin.site.register(Unit)
class UnitAdmin(admin.ModelAdmin):
list_display = ('name', 'space')
ordering = ('name', 'space',)
search_fields = ('name',)
admin.site.register(Unit, UnitAdmin)
# admin.site.register(FoodInheritField)
@@ -229,10 +250,16 @@ def delete_unattached_ingredients(modeladmin, request, queryset):
class IngredientAdmin(admin.ModelAdmin):
list_display = ('food', 'amount', 'unit')
search_fields = ('food__name', 'unit__name')
list_display = ('recipe_name', 'amount', 'unit', 'food', 'space')
search_fields = ('food__name', 'unit__name', 'step__recipe__name')
actions = [delete_unattached_ingredients]
@staticmethod
@admin.display(description="Recipe")
def recipe_name(obj):
recipes = obj.step_set.first().recipe_set.all() if obj.step_set.exists() else None
return recipes.first().name if recipes else 'Orphaned Ingredient'
admin.site.register(Ingredient, IngredientAdmin)
@@ -258,7 +285,7 @@ admin.site.register(RecipeImport, RecipeImportAdmin)
class RecipeBookAdmin(admin.ModelAdmin):
list_display = ('name', 'user_name')
list_display = ('name', 'user_name', 'space')
search_fields = ('name', 'created_by__username')
@staticmethod
@@ -334,11 +361,11 @@ class ShoppingListEntryAdmin(admin.ModelAdmin):
admin.site.register(ShoppingListEntry, ShoppingListEntryAdmin)
class ShoppingListAdmin(admin.ModelAdmin):
list_display = ('id', 'created_by', 'created_at')
# class ShoppingListAdmin(admin.ModelAdmin):
# list_display = ('id', 'created_by', 'created_at')
admin.site.register(ShoppingList, ShoppingListAdmin)
# admin.site.register(ShoppingList, ShoppingListAdmin)
class ShareLinkAdmin(admin.ModelAdmin):
@@ -349,7 +376,9 @@ admin.site.register(ShareLink, ShareLinkAdmin)
class PropertyTypeAdmin(admin.ModelAdmin):
list_display = ('id', 'name')
search_fields = ('space',)
list_display = ('id', 'space', 'name', 'fdc_id')
admin.site.register(PropertyType, PropertyTypeAdmin)

View File

@@ -33,64 +33,6 @@ class DateWidget(forms.DateInput):
super().__init__(**kwargs)
class UserPreferenceForm(forms.ModelForm):
prefix = 'preference'
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['plan_share'].queryset = User.objects.filter(userspace__space=space).all()
class Meta:
model = UserPreference
fields = (
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed', 'show_step_ingredients',
)
labels = {
'default_unit': _('Default unit'),
'use_fractions': _('Use fractions'),
'use_kj': _('Use KJ'),
'theme': _('Theme'),
'nav_color': _('Navbar color'),
'sticky_navbar': _('Sticky navbar'),
'default_page': _('Default page'),
'plan_share': _('Plan sharing'),
'ingredient_decimals': _('Ingredient decimal places'),
'shopping_auto_sync': _('Shopping list auto sync period'),
'comments': _('Comments'),
'left_handed': _('Left-handed mode'),
'show_step_ingredients': _('Show step ingredients table')
}
help_texts = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
'use_fractions': _(
'Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'),
'use_kj': _('Display nutritional energy amounts in joules instead of calories'),
'plan_share': _('Users with whom newly created meal plans should be shared by default.'),
'shopping_share': _('Users with whom to share shopping lists.'),
'ingredient_decimals': _('Number of decimals to round ingredients.'),
'comments': _('If you want to be able to create and see comments underneath recipes.'),
'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 '
'of mobile data. If lower than instance limit it is reset when saving.'
),
'sticky_navbar': _('Makes the navbar stick to the top of the page.'),
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
'mealplan_autoexclude_onhand': _('Exclude ingredients that are on hand.'),
'left_handed': _('Will optimize the UI for use with your left hand.'),
'show_step_ingredients': _('Add ingredients table next to recipe steps. Applies at creation time for manually created and URL imported recipes. Individual steps can be overridden in the edit recipe view.')
}
widgets = {
'plan_share': MultiSelectWidget,
'shopping_share': MultiSelectWidget,
}
class UserNameForm(forms.ModelForm):
prefix = 'name'

View File

@@ -7,7 +7,7 @@ class Round(Func):
def str2bool(v):
if type(v) == bool or v is None:
if isinstance(v, bool) or v is None:
return v
else:
return v.lower() in ("yes", "true", "1")

View File

@@ -0,0 +1,19 @@
import json
def get_all_nutrient_types():
f = open('') # <--- download the foundation food or any other dataset and retrieve all nutrition ID's from it https://fdc.nal.usda.gov/download-datasets.html
json_data = json.loads(f.read())
nutrients = {}
for food in json_data['FoundationFoods']:
for entry in food['foodNutrients']:
nutrients[entry['nutrient']['id']] = {'name': entry['nutrient']['name'], 'unit': entry['nutrient']['unitName']}
nutrient_ids = list(nutrients.keys())
nutrient_ids.sort()
for nid in nutrient_ids:
print('{', f'value: {nid}, text: "{nutrients[nid]["name"]} [{nutrients[nid]["unit"]}] ({nid})"', '},')
get_all_nutrient_types()

View File

@@ -169,6 +169,9 @@ class IngredientParser:
if len(ingredient) == 0:
raise ValueError('string to parse cannot be empty')
if len(ingredient) > 512:
raise ValueError('cannot parse ingredients with more than 512 characters')
# some people/languages put amount and unit at the end of the ingredient string
# if something like this is detected move it to the beginning so the parser can handle it
if len(ingredient) < 1000 and re.search(r'^([^\W\d_])+(.)*[1-9](\d)*\s*([^\W\d_])+', ingredient):

View File

@@ -309,7 +309,7 @@ class RecipeSearch():
def _favorite_recipes(self, times_cooked=None):
if self._sort_includes('favorite') or times_cooked:
less_than = '-' in (times_cooked or []) and not self._sort_includes('-favorite')
less_than = '-' in (str(times_cooked) or []) and not self._sort_includes('-favorite')
if less_than:
default = 1000
else:

View File

@@ -163,10 +163,9 @@ def get_from_scraper(scrape, request):
if len(recipe_json['steps']) == 0:
recipe_json['steps'].append({'instruction': '', 'ingredients': [], })
recipe_json['description'] = recipe_json['description'][:512]
if len(recipe_json['description']) > 256: # split at 256 as long descriptions don't look good on recipe cards
recipe_json['steps'][0]['instruction'] = f"*{recipe_json['description']}* \n\n" + recipe_json['steps'][0]['instruction']
else:
recipe_json['description'] = recipe_json['description'][:512]
try:
for x in scrape.ingredients():
@@ -259,13 +258,14 @@ def get_from_youtube_scraper(url, request):
]
}
# TODO add automation here
try:
automation_engine = AutomationEngine(request, source=url)
video = YouTube(url=url)
video = YouTube(url)
video.streams.first() # this is required to execute some kind of generator/web request that fetches the description
default_recipe_json['name'] = automation_engine.apply_regex_replace_automation(video.title, Automation.NAME_REPLACE)
default_recipe_json['image'] = video.thumbnail_url
default_recipe_json['steps'][0]['instruction'] = automation_engine.apply_regex_replace_automation(video.description, Automation.INSTRUCTION_REPLACE)
if video.description:
default_recipe_json['steps'][0]['instruction'] = automation_engine.apply_regex_replace_automation(video.description, Automation.INSTRUCTION_REPLACE)
except Exception:
pass
@@ -415,8 +415,8 @@ def parse_keywords(keyword_json, request):
# if alias exists use that instead
if len(kw) != 0:
automation_engine.apply_keyword_automation(kw)
if k := Keyword.objects.filter(name=kw, space=request.space).first():
kw = automation_engine.apply_keyword_automation(kw)
if k := Keyword.objects.filter(name__iexact=kw, space=request.space).first():
keywords.append({'label': str(k), 'name': k.name, 'id': k.id})
else:
keywords.append({'label': kw, 'name': kw})

View File

@@ -11,8 +11,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-09 18:01+0100\n"
"PO-Revision-Date: 2023-07-31 14:19+0000\n"
"Last-Translator: Mára Štěpánek <stepanekm7@gmail.com>\n"
"PO-Revision-Date: 2024-01-09 12:07+0000\n"
"Last-Translator: Jan Kubošek <kuboja@outlook.cz>\n"
"Language-Team: Czech <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/cs/>\n"
"Language: cs\n"
@@ -190,7 +190,7 @@ msgid ""
"Leave empty for dropbox and enter only base url for nextcloud "
"(<code>/remote.php/webdav/</code> is added automatically)"
msgstr ""
"Pro dropbox ponechejte nevyplňené pole. Pro nextcloud použijte pouze "
"Pro dropbox ponechejte nevyplné pole. Pro nextcloud použijte pouze "
"základní url (<code>/remote.php/webdav/</code> bude přidán automaticky)."
#: .\cookbook\forms.py:263
@@ -529,7 +529,7 @@ msgstr "Dávková úprava receptu"
#: .\cookbook\templates\batch\edit.html:20
msgid "Add the specified keywords to all recipes containing a word"
msgstr "Přidat štítek ke všem receptům, které obsahují specifické slovo."
msgstr "Přidat štítek ke všem receptům, které obsahují specifické slovo"
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:66
msgid "Sync"

View File

@@ -14,8 +14,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
"PO-Revision-Date: 2023-10-12 20:19+0000\n"
"Last-Translator: pharok <pharok@free.fr>\n"
"PO-Revision-Date: 2023-12-10 14:19+0000\n"
"Last-Translator: Robin Wilmet <wilmetrobin@hotmail.com>\n"
"Language-Team: French <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/fr/>\n"
"Language: fr\n"
@@ -551,7 +551,7 @@ msgstr "sens inverse"
#: .\cookbook\helper\recipe_url_import.py:267
msgid "careful rotation"
msgstr ""
msgstr "sens horloger"
#: .\cookbook\helper\recipe_url_import.py:268
msgid "knead"

Binary file not shown.

View File

@@ -11,7 +11,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-18 14:28+0200\n"
"PO-Revision-Date: 2023-10-20 14:05+0000\n"
"PO-Revision-Date: 2023-12-05 09:15+0000\n"
"Last-Translator: Ferenc <ugyes@freemail.hu>\n"
"Language-Team: Hungarian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/hu/>\n"
@@ -282,16 +282,12 @@ msgstr ""
"hibát figyelmen kívül hagynak)."
#: .\cookbook\forms.py:461
#, fuzzy
#| msgid ""
#| "Select type method of search. Click <a href=\"/docs/search/\">here</a> "
#| "for full desciption of choices."
msgid ""
"Select type method of search. Click <a href=\"/docs/search/\">here</a> for "
"full description of choices."
msgstr ""
"Válassza ki a keresés típusát. Kattintson <a href=\"/docs/search/\">ide</a> "
"a lehetőségek teljes leírásáért."
"Válassza ki a keresés típusát. Kattintson <a href=\"/docs/search/\">ide</"
"a> a lehetőségek teljes leírásáért."
#: .\cookbook\forms.py:462
msgid ""
@@ -536,10 +532,8 @@ msgid "One of queryset or hash_key must be provided"
msgstr "A queryset vagy a hash_key valamelyikét meg kell adni"
#: .\cookbook\helper\recipe_url_import.py:266
#, fuzzy
#| msgid "Use fractions"
msgid "reverse rotation"
msgstr "Törtek használata"
msgstr "Ellentétes irány"
#: .\cookbook\helper\recipe_url_import.py:267
msgid "careful rotation"
@@ -770,7 +764,7 @@ msgstr "Elérte a fájlfeltöltési limitet."
#: .\cookbook\serializer.py:291
msgid "Cannot modify Space owner permission."
msgstr ""
msgstr "A Hely tulajdonosi engedélye nem módosítható."
#: .\cookbook\serializer.py:1093
msgid "Hello"
@@ -1226,10 +1220,8 @@ msgstr "Admin"
#: .\cookbook\templates\base.html:312
#: .\cookbook\templates\space_overview.html:25
#, fuzzy
#| msgid "No Space"
msgid "Your Spaces"
msgstr "Nincs hely"
msgstr "Ön Helye"
#: .\cookbook\templates\base.html:323
#: .\cookbook\templates\space_overview.html:6
@@ -2132,6 +2124,8 @@ msgstr "Csatlakozás %(provider)s"
#, python-format
msgid "You are about to connect a new third party account from %(provider)s."
msgstr ""
"Ön egy új, harmadik féltől származó fiókot készül csatlakoztatni "
"a%(provider)-tól/től."
#: .\cookbook\templates\socialaccount\login.html:13
#, python-format

View File

@@ -1,7 +1,7 @@
# Generated by Django 4.1.10 on 2023-08-29 11:59
from django.db import migrations
from django.db.models import F, Value
from django.db.models import F, Value, Count
from django.db.models.functions import Concat
from django_scopes import scopes_disabled
@@ -13,9 +13,24 @@ def migrate_icons(apps, schema_editor):
PropertyType = apps.get_model('cookbook', 'PropertyType')
RecipeBook = apps.get_model('cookbook', 'RecipeBook')
duplicate_meal_types = MealType.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1).all()
if len(duplicate_meal_types) > 0:
raise RuntimeError(f'Duplicate MealTypes found, please remove/rename them and run migrations again/restart the container. {duplicate_meal_types}')
MealType.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
duplicate_meal_types = Keyword.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1).all()
if len(duplicate_meal_types) > 0:
raise RuntimeError(f'Duplicate Keyword found, please remove/rename them and run migrations again/restart the container. {duplicate_meal_types}')
Keyword.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
duplicate_meal_types = PropertyType.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1).all()
if len(duplicate_meal_types) > 0:
raise RuntimeError(f'Duplicate PropertyType found, please remove/rename them and run migrations again/restart the container. {duplicate_meal_types}')
PropertyType.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
duplicate_meal_types = RecipeBook.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1).all()
if len(duplicate_meal_types) > 0:
raise RuntimeError(f'Duplicate RecipeBook found, please remove/rename them and run migrations again/restart the container. {duplicate_meal_types}')
RecipeBook.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
@@ -25,10 +40,7 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(
migrate_icons
),
migrations.RunPython( migrate_icons),
migrations.AlterModelOptions(
name='propertytype',
options={'ordering': ('order',)},

View File

@@ -1,15 +1,23 @@
# Generated by Django 4.2.7 on 2023-11-27 21:09
from django.db import migrations, models
from django_scopes import scopes_disabled
def fix_fdc_ids(apps, schema_editor):
with scopes_disabled():
# in case any food had a non digit fdc ID before this migration, remove it
Food = apps.get_model('cookbook', 'Food')
Food.objects.exclude(fdc_id__regex=r'^\d+$').exclude(fdc_id=None).update(fdc_id=None)
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0203_alter_unique_contstraints'),
]
operations = [
migrations.RunPython(fix_fdc_ids),
migrations.AddField(
model_name='propertytype',
name='fdc_id',

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.7 on 2023-11-29 19:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0204_propertytype_fdc_id'),
]
operations = [
migrations.AlterField(
model_name='food',
name='fdc_id',
field=models.IntegerField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='propertytype',
name='fdc_id',
field=models.IntegerField(blank=True, default=None, null=True),
),
]

View File

@@ -0,0 +1,128 @@
# Generated by Django 4.2.7 on 2024-01-01 18:44
import django
from django.db import migrations, models
from django_scopes import scopes_disabled
TANDOOR = 'TANDOOR'
TANDOOR_DARK = 'TANDOOR_DARK'
BOOTSTRAP = 'BOOTSTRAP'
DARKLY = 'DARKLY'
FLATLY = 'FLATLY'
SUPERHERO = 'SUPERHERO'
PRIMARY = 'PRIMARY'
SECONDARY = 'SECONDARY'
SUCCESS = 'SUCCESS'
INFO = 'INFO'
WARNING = 'WARNING'
DANGER = 'DANGER'
LIGHT = 'LIGHT'
DARK = 'DARK'
# ['light', 'warning', 'info', 'success'] --> light (theming_tags L45)
def get_nav_bg_color(theme, nav_color):
if theme == TANDOOR: # primary not actually primary color but override existed before update, same for dark
return {PRIMARY: '#ddbf86', SECONDARY: '#b55e4f', SUCCESS: '#82aa8b', INFO: '#385f84', WARNING: '#eaaa21', DANGER: '#a7240e', LIGHT: '#cfd5cd', DARK: '#221e1e'}[nav_color]
if theme == TANDOOR_DARK:
return {PRIMARY: '#ddbf86', SECONDARY: '#b55e4f', SUCCESS: '#82aa8b', INFO: '#385f84', WARNING: '#eaaa21', DANGER: '#a7240e', LIGHT: '#cfd5cd', DARK: '#221e1e'}[nav_color]
if theme == BOOTSTRAP:
return {PRIMARY: '#007bff', SECONDARY: '#6c757d', SUCCESS: '#28a745', INFO: '#17a2b8', WARNING: '#ffc107', DANGER: '#dc3545', LIGHT: '#f8f9fa', DARK: '#343a40'}[nav_color]
if theme == DARKLY:
return {PRIMARY: '#375a7f', SECONDARY: '#444', SUCCESS: '#00bc8c', INFO: '#3498DB', WARNING: '#F39C12', DANGER: '#E74C3C', LIGHT: '#999', DARK: '#303030'}[nav_color]
if theme == FLATLY:
return {PRIMARY: '#2C3E50', SECONDARY: '#95a5a6', SUCCESS: '#18BC9C', INFO: '#3498DB', WARNING: '#F39C12', DANGER: '#E74C3C', LIGHT: '#ecf0f1', DARK: '#7b8a8b'}[nav_color]
if theme == SUPERHERO:
return {PRIMARY: '#DF691A', SECONDARY: '#4E5D6C', SUCCESS: '#5cb85c', INFO: '#5bc0de', WARNING: '#f0ad4e', DANGER: '#d9534f', LIGHT: '#abb6c2', DARK: '#4E5D6C'}[nav_color]
def get_nav_text_color(theme, nav_color):
if theme == TANDOOR:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: DARK, INFO: DARK, WARNING: DARK, DANGER: DARK, LIGHT: DARK, DARK: DARK}[nav_color]
if theme == TANDOOR_DARK:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: DARK, INFO: DARK, WARNING: DARK, DANGER: DARK, LIGHT: DARK, DARK: DARK}[nav_color]
if theme == BOOTSTRAP:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: DARK, INFO: DARK, WARNING: DARK, DANGER: DARK, LIGHT: DARK, DARK: DARK}[nav_color]
if theme == DARKLY:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: DARK, INFO: DARK, WARNING: DARK, DANGER: DARK, LIGHT: DARK, DARK: DARK}[nav_color]
if theme == FLATLY:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: LIGHT, INFO: LIGHT, WARNING: LIGHT, DANGER: DARK, LIGHT: LIGHT, DARK: DARK}[nav_color]
if theme == SUPERHERO:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: LIGHT, INFO: LIGHT, WARNING: LIGHT, DANGER: DARK, LIGHT: LIGHT, DARK: DARK}[nav_color]
def get_current_colors(apps, schema_editor):
with scopes_disabled():
# in case any food had a non digit fdc ID before this migration, remove it
UserPreference = apps.get_model('cookbook', 'UserPreference')
update_ups = []
for up in UserPreference.objects.all():
if up.theme != TANDOOR or up.nav_color != PRIMARY:
up.nav_bg_color = get_nav_bg_color(up.theme, up.nav_color)
up.nav_text_color = get_nav_text_color(up.theme, up.nav_color)
up.nav_show_logo = (up.theme == TANDOOR or up.theme == TANDOOR_DARK)
update_ups.append(up)
UserPreference.objects.bulk_update(update_ups, ['nav_bg_color', 'nav_text_color', 'nav_show_logo'])
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0205_alter_food_fdc_id_alter_propertytype_fdc_id'),
]
operations = [
migrations.RenameField(
model_name='userpreference',
old_name='sticky_navbar',
new_name='nav_sticky',
),
migrations.AddField(
model_name='userpreference',
name='nav_bg_color',
field=models.CharField(default='#ddbf86', max_length=8),
),
migrations.AddField(
model_name='userpreference',
name='nav_text_color',
field=models.CharField(choices=[('LIGHT', 'Light'), ('DARK', 'Dark')], default='DARK', max_length=16),
),
migrations.AddField(
model_name='userpreference',
name='nav_show_logo',
field=models.BooleanField(default=True),
),
migrations.RunPython(get_current_colors),
migrations.RemoveField(
model_name='userpreference',
name='nav_color',
),
migrations.AddField(
model_name='space',
name='nav_bg_color',
field=models.CharField(blank=True, default='', max_length=8),
),
migrations.AddField(
model_name='space',
name='nav_logo',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_nav_logo', to='cookbook.userfile'),
),
migrations.AddField(
model_name='space',
name='nav_text_color',
field=models.CharField(choices=[('BLANK', '-------'), ('LIGHT', 'Light'), ('DARK', 'Dark')], default='BLANK', max_length=16),
),
migrations.AddField(
model_name='space',
name='space_theme',
field=models.CharField(choices=[('BLANK', '-------'), ('TANDOOR', 'Tandoor'), ('BOOTSTRAP', 'Bootstrap'), ('DARKLY', 'Darkly'), ('FLATLY', 'Flatly'), ('SUPERHERO', 'Superhero'), ('TANDOOR_DARK', 'Tandoor Dark (INCOMPLETE)')],
default='BLANK',
max_length=128),
),
migrations.AddField(
model_name='space',
name='custom_space_theme',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_theme', to='cookbook.userfile'),
),
]

View File

@@ -0,0 +1,49 @@
# Generated by Django 4.2.7 on 2024-01-06 15:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0206_rename_sticky_navbar_userpreference_nav_sticky_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='logo_color_128',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_128', to='cookbook.userfile'),
),
migrations.AddField(
model_name='space',
name='logo_color_144',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_144', to='cookbook.userfile'),
),
migrations.AddField(
model_name='space',
name='logo_color_180',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_180', to='cookbook.userfile'),
),
migrations.AddField(
model_name='space',
name='logo_color_192',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_192', to='cookbook.userfile'),
),
migrations.AddField(
model_name='space',
name='logo_color_32',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_32', to='cookbook.userfile'),
),
migrations.AddField(
model_name='space',
name='logo_color_512',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_512', to='cookbook.userfile'),
),
migrations.AddField(
model_name='space',
name='logo_color_svg',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_svg', to='cookbook.userfile'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.7 on 2024-01-14 23:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0207_space_logo_color_128_space_logo_color_144_and_more'),
]
operations = [
migrations.AddField(
model_name='space',
name='app_name',
field=models.CharField(blank=True, max_length=40, null=True),
),
migrations.AddField(
model_name='userpreference',
name='max_owned_spaces',
field=models.IntegerField(default=100),
),
]

View File

@@ -24,7 +24,7 @@ from PIL import Image
from treebeard.mp_tree import MP_Node, MP_NodeManager
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, KJ_PREF_DEFAULT,
SORT_TREE_BY_NAME, STICKY_NAV_PREF_DEFAULT)
SORT_TREE_BY_NAME, STICKY_NAV_PREF_DEFAULT, MAX_OWNED_SPACES_PREF_DEFAULT)
def get_user_display_name(self):
@@ -251,8 +251,52 @@ class FoodInheritField(models.Model, PermissionModelMixin):
class Space(ExportModelOperationsMixin('space'), models.Model):
# TODO remove redundant theming constants
# Themes
BLANK = 'BLANK'
TANDOOR = 'TANDOOR'
TANDOOR_DARK = 'TANDOOR_DARK'
BOOTSTRAP = 'BOOTSTRAP'
DARKLY = 'DARKLY'
FLATLY = 'FLATLY'
SUPERHERO = 'SUPERHERO'
THEMES = (
(BLANK, '-------'),
(TANDOOR, 'Tandoor'),
(BOOTSTRAP, 'Bootstrap'),
(DARKLY, 'Darkly'),
(FLATLY, 'Flatly'),
(SUPERHERO, 'Superhero'),
(TANDOOR_DARK, 'Tandoor Dark (INCOMPLETE)'),
)
LIGHT = 'LIGHT'
DARK = 'DARK'
NAV_TEXT_COLORS = (
(BLANK, '-------'),
(LIGHT, 'Light'),
(DARK, 'Dark')
)
name = models.CharField(max_length=128, default='Default')
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_image')
space_theme = models.CharField(choices=THEMES, max_length=128, default=BLANK)
custom_space_theme = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_theme')
nav_logo = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_nav_logo')
nav_bg_color = models.CharField(max_length=8, default='', blank=True, )
nav_text_color = models.CharField(max_length=16, choices=NAV_TEXT_COLORS, default=BLANK)
app_name = models.CharField(max_length=40, null=True, blank=True, )
logo_color_32 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_32')
logo_color_128 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_128')
logo_color_144 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_144')
logo_color_180 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_180')
logo_color_192 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_192')
logo_color_512 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_512')
logo_color_svg = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_svg')
created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
created_at = models.DateTimeField(auto_now_add=True)
message = models.CharField(max_length=512, default='', blank=True)
@@ -338,22 +382,10 @@ class UserPreference(models.Model, PermissionModelMixin):
)
# Nav colors
PRIMARY = 'PRIMARY'
SECONDARY = 'SECONDARY'
SUCCESS = 'SUCCESS'
INFO = 'INFO'
WARNING = 'WARNING'
DANGER = 'DANGER'
LIGHT = 'LIGHT'
DARK = 'DARK'
COLORS = (
(PRIMARY, 'Primary'),
(SECONDARY, 'Secondary'),
(SUCCESS, 'Success'),
(INFO, 'Info'),
(WARNING, 'Warning'),
(DANGER, 'Danger'),
NAV_TEXT_COLORS = (
(LIGHT, 'Light'),
(DARK, 'Dark')
)
@@ -371,8 +403,13 @@ class UserPreference(models.Model, PermissionModelMixin):
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='user_image')
theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
nav_bg_color = models.CharField(max_length=8, default='#ddbf86')
nav_text_color = models.CharField(max_length=16, choices=NAV_TEXT_COLORS, default=DARK)
nav_show_logo = models.BooleanField(default=True)
nav_sticky = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT)
max_owned_spaces = models.IntegerField(default=MAX_OWNED_SPACES_PREF_DEFAULT)
default_unit = models.CharField(max_length=32, default='g')
use_fractions = models.BooleanField(default=FRACTION_PREF_DEFAULT)
use_kj = models.BooleanField(default=KJ_PREF_DEFAULT)
@@ -382,7 +419,6 @@ class UserPreference(models.Model, PermissionModelMixin):
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)
@@ -398,6 +434,15 @@ class UserPreference(models.Model, PermissionModelMixin):
created_at = models.DateTimeField(auto_now_add=True)
objects = ScopedManager(space='space')
def save(self, *args, **kwargs):
if not self.pk:
self.max_owned_spaces = MAX_OWNED_SPACES_PREF_DEFAULT
self.comments = COMMENT_PREF_DEFAULT
self.nav_sticky = STICKY_NAV_PREF_DEFAULT
self.use_kj = KJ_PREF_DEFAULT
self.use_fractions = FRACTION_PREF_DEFAULT
return super().save(*args, **kwargs)
def __str__(self):
return str(self.user)
@@ -591,7 +636,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
preferred_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_unit')
preferred_shopping_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_shopping_unit')
fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None)
fdc_id = models.IntegerField(null=True, default=None, blank=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
@@ -718,6 +763,9 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'{self.pk}: {self.amount} {self.food.name} {self.unit.name}'
class Meta:
ordering = ['order', 'pk']
indexes = (
@@ -745,7 +793,9 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
return render_instructions(self)
def __str__(self):
return f'{self.pk} {self.name}'
if not self.recipe_set.exists():
return f"{self.pk}: {_('Orphaned Step')}"
return f"{self.pk}: {self.name}" if self.name else f"Step: {self.pk}"
class Meta:
ordering = ['order', 'pk']
@@ -767,7 +817,7 @@ class PropertyType(models.Model, PermissionModelMixin):
(PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None)
fdc_id = models.IntegerField(null=True, default=None, blank=True)
# TODO show if empty property?
# TODO formatting property?
@@ -809,7 +859,7 @@ class FoodProperty(models.Model):
class Meta:
constraints = [
models.UniqueConstraint(fields=['food', 'property'], name='property_unique_food')
models.UniqueConstraint(fields=['food', 'property'], name='property_unique_food'),
]
@@ -1311,6 +1361,9 @@ class UserFile(ExportModelOperationsMixin('user_files'), models.Model, Permissio
self.file_size_kb = round(self.file.size / 1000)
super(UserFile, self).save(*args, **kwargs)
def __str__(self):
return f'{self.name} (#{self.id})'
class Automation(ExportModelOperationsMixin('automations'), models.Model, PermissionModelMixin):
FOOD_ALIAS = 'FOOD_ALIAS'

View File

@@ -19,6 +19,7 @@ from oauth2_provider.models import AccessToken
from PIL import Image
from rest_framework import serializers
from rest_framework.exceptions import NotFound, ValidationError
from rest_framework.fields import IntegerField
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
from cookbook.helper.HelperFunctions import str2bool
@@ -211,7 +212,7 @@ class UserFileSerializer(serializers.ModelSerializer):
Image.open(obj.file.file.file)
return self.context['request'].build_absolute_uri(obj.file.url)
except Exception:
traceback.print_exc()
# traceback.print_exc()
return ""
def check_file_limit(self, validated_data):
@@ -259,7 +260,7 @@ class UserFileViewSerializer(serializers.ModelSerializer):
Image.open(obj.file.file.file)
return self.context['request'].build_absolute_uri(obj.file.url)
except Exception:
traceback.print_exc()
# traceback.print_exc()
return ""
def create(self, validated_data):
@@ -280,6 +281,15 @@ class SpaceSerializer(WritableNestedModelSerializer):
file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
food_inherit = FoodInheritFieldSerializer(many=True)
image = UserFileViewSerializer(required=False, many=False, allow_null=True)
nav_logo = UserFileViewSerializer(required=False, many=False, allow_null=True)
custom_space_theme = UserFileViewSerializer(required=False, many=False, allow_null=True)
logo_color_32 = UserFileViewSerializer(required=False, many=False, allow_null=True)
logo_color_128 = UserFileViewSerializer(required=False, many=False, allow_null=True)
logo_color_144 = UserFileViewSerializer(required=False, many=False, allow_null=True)
logo_color_180 = UserFileViewSerializer(required=False, many=False, allow_null=True)
logo_color_192 = UserFileViewSerializer(required=False, many=False, allow_null=True)
logo_color_512 = UserFileViewSerializer(required=False, many=False, allow_null=True)
logo_color_svg = UserFileViewSerializer(required=False, many=False, allow_null=True)
def get_user_count(self, obj):
return UserSpace.objects.filter(space=obj).count()
@@ -301,7 +311,8 @@ class SpaceSerializer(WritableNestedModelSerializer):
fields = (
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
'image', 'use_plural',)
'image', 'nav_logo', 'space_theme', 'custom_space_theme', 'nav_bg_color', 'nav_text_color', 'use_plural',
'logo_color_32', 'logo_color_128', 'logo_color_144', 'logo_color_180', 'logo_color_192', 'logo_color_512', 'logo_color_svg',)
read_only_fields = (
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
'demo',)
@@ -371,8 +382,8 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
class Meta:
model = UserPreference
fields = (
'user', 'image', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj',
'plan_share', 'sticky_navbar',
'user', 'image', 'theme', 'nav_bg_color', 'nav_text_color', 'nav_show_logo', 'default_unit', 'default_page', 'use_fractions', 'use_kj',
'plan_share', 'nav_sticky',
'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',
@@ -524,6 +535,7 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataMo
class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer, UniqueFieldsMixin):
id = serializers.IntegerField(required=False)
order = IntegerField(default=0, required=False)
def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
@@ -985,6 +997,8 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
shared = UserSerializer(many=True, required=False, allow_null=True)
shopping = serializers.SerializerMethodField('in_shopping')
to_date = serializers.DateField(required=False)
def get_note_markdown(self, obj):
return markdown(obj.note)
@@ -993,6 +1007,10 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
if 'to_date' not in validated_data or validated_data['to_date'] is None:
validated_data['to_date'] = validated_data['from_date']
mealplan = super().create(validated_data)
if self.context['request'].data.get('addshopping', False) and self.context['request'].data.get('recipe', None):
SLR = RecipeShoppingEditor(user=validated_data['created_by'], space=validated_data['space'])
@@ -1013,7 +1031,7 @@ class AutoMealPlanSerializer(serializers.Serializer):
start_date = serializers.DateField()
end_date = serializers.DateField()
meal_type_id = serializers.IntegerField()
keywords = KeywordSerializer(many=True)
keyword_ids = serializers.ListField()
servings = CustomDecimalField()
shared = UserSerializer(many=True, required=False, allow_null=True)
addshopping = serializers.BooleanField()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,41 +0,0 @@
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Logo" transform="matrix(0.637323,0,0,0.637323,-243.095,-716.725)">
<g id="Kreis" transform="matrix(1.44936,0,0,1.50279,387.258,1039.34)">
<ellipse cx="273.123" cy="324.015" rx="259.822" ry="250.584" style="fill:url(#_Linear1);"/>
<clipPath id="_clip2">
<ellipse cx="273.123" cy="324.015" rx="259.822" ry="250.584"/>
</clipPath>
<g clip-path="url(#_clip2)">
<g id="Shadow" transform="matrix(1.10322,0,0,1.064,-5.58287,50.5786)">
<path d="M156.285,427.208L389.554,660.477L668.803,495.551L374.012,200.761L156.285,427.208Z" style="fill:rgb(22,22,22);"/>
<g transform="matrix(1,0,0,1,-4.22105,0.775864)">
<path d="M208.628,178.613L485.935,455.919L590.027,364.63L296.923,71.526L294.175,138.989L208.628,178.613Z" style="fill:rgb(22,22,22);"/>
</g>
<g transform="matrix(1,0,0,1,-85.3876,27.8512)">
<path d="M310.385,145.641L587.692,422.948L590.392,361.357L297.288,68.253L294.175,138.989L310.385,145.641Z" style="fill:rgb(22,22,22);"/>
</g>
</g>
</g>
</g>
<g transform="matrix(1.471,0,0,1.471,406.537,1149.69)">
<path d="M256.049,220C286.222,219.994 312.656,207.31 329.388,194.134C346.35,180.754 370.899,183.406 384.611,200.1C407.129,227.376 420.598,261.944 420.598,299.53C420.598,361.08 382.604,437.101 329.764,463.706C307.035,475.15 283.466,480.586 256.098,480.599L256.098,480.599L256.049,480.599L256,480.599L256,480.599C228.632,480.586 205.063,475.15 182.334,463.706C129.494,437.101 91.5,361.08 91.5,299.53C91.5,261.944 104.969,227.376 127.487,200.1C141.199,183.406 165.748,180.754 182.71,194.134C199.442,207.31 225.876,219.994 256.049,220Z" style="fill:rgb(255,203,118);"/>
</g>
<g id="Flame-2" transform="matrix(0.965725,0,0,0.89175,164.497,436.391)">
<path d="M604.408,844.314C601.981,840.845 601.962,836.056 604.362,832.565C606.763,829.074 611.005,827.721 614.769,829.246C633.87,836.869 658.833,848.629 678.207,864.452C718.526,897.381 729.55,919.407 738.552,942.091C749.208,968.943 750.785,996.68 748.515,1016.08C742.018,1071.61 700.355,1117.5 641.034,1117.5C581.713,1117.5 534.493,1072.05 533.553,1016.08C532.986,982.372 543.985,955.443 555.988,936.22C558.982,931.437 564.594,929.469 569.609,931.444C574.623,933.419 577.757,938.831 577.215,944.58C575.493,956.716 574.362,969.372 574.932,979.484C576.863,1013.7 597.171,1022.5 618.083,1022.29C640.371,1022.08 662.925,1003.17 654.797,954.895C647.69,912.681 622.362,870.194 604.408,844.314Z" style="fill:rgb(255,111,0);"/>
<clipPath id="_clip3">
<path d="M604.408,844.314C601.981,840.845 601.962,836.056 604.362,832.565C606.763,829.074 611.005,827.721 614.769,829.246C633.87,836.869 658.833,848.629 678.207,864.452C718.526,897.381 729.55,919.407 738.552,942.091C749.208,968.943 750.785,996.68 748.515,1016.08C742.018,1071.61 700.355,1117.5 641.034,1117.5C581.713,1117.5 534.493,1072.05 533.553,1016.08C532.986,982.372 543.985,955.443 555.988,936.22C558.982,931.437 564.594,929.469 569.609,931.444C574.623,933.419 577.757,938.831 577.215,944.58C575.493,956.716 574.362,969.372 574.932,979.484C576.863,1013.7 597.171,1022.5 618.083,1022.29C640.371,1022.08 662.925,1003.17 654.797,954.895C647.69,912.681 622.362,870.194 604.408,844.314Z"/>
</clipPath>
<g clip-path="url(#_clip3)">
<g transform="matrix(1.28784,-0.270602,0.285942,1.59598,247.349,825.209)">
<path d="M255.004,46.957C279.547,58.545 306,85.447 313.307,120.161C325.437,177.791 291.571,193.789 262.496,192.403C215.889,190.181 200.194,153.246 231.326,108.9C250.631,81.401 232.663,36.408 255.004,46.957Z" style="fill:rgb(255,209,0);"/>
</g>
</g>
</g>
<g id="Hut" transform="matrix(1.521,0,0,1.521,393.566,1149.06)">
<path d="M228.197,408.524C222.698,408.524 217.813,406.688 214.024,403.619C211.776,401.794 210.92,398.752 211.888,396.024C212.856,393.295 215.437,391.472 218.332,391.472C232.214,391.4 256.112,391.396 256.112,391.396C256.112,391.396 280.009,391.4 293.891,391.472C296.786,391.472 299.367,393.295 300.335,396.024C301.303,398.752 300.447,401.794 298.199,403.619C294.41,406.688 289.526,408.524 284.027,408.524L228.197,408.524ZM217.24,378.877C214.208,378.877 211.3,377.671 209.158,375.525C207.015,373.379 205.814,370.469 205.82,367.436C205.831,361.119 205.842,354.539 205.842,354.539C205.842,350.423 203.097,346.814 199.131,345.714C185.313,341.841 175.2,329.468 175.2,314.823C175.2,297.07 190.059,282.657 208.362,282.657C208.362,282.657 208.362,282.657 208.362,282.657C215.401,282.657 221.675,278.218 224.017,271.581C227.243,262.39 236.411,252.015 256,251.998L256,251.998L256.223,251.998L256.223,251.998C275.812,252.015 284.98,262.39 288.206,271.581C290.549,278.218 296.822,282.657 303.861,282.657C303.861,282.657 303.861,282.657 303.861,282.657C322.164,282.657 337.023,297.07 337.023,314.823C337.023,329.468 326.911,341.841 313.093,345.714C309.127,346.814 306.382,350.423 306.381,354.539C306.381,354.539 306.386,361.127 306.391,367.447C306.394,370.478 305.191,373.385 303.049,375.529C300.907,377.672 298.001,378.877 294.971,378.877C275.615,378.877 236.604,378.877 217.24,378.877Z" style="fill:rgb(22,22,22);"/>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2e-06,0,0,2e-06,3755.77,81.7179)"><stop offset="0" style="stop-color:rgb(39,39,39);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(108,108,108);stop-opacity:1"/></linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -4457,28 +4457,28 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
}
.navbar-light .navbar-brand, .navbar-light .navbar-brand:focus, .navbar-light .navbar-brand:hover {
color: rgba(46, 46, 46, .9)
color: rgba(0, 0, 0, .9)
}
.navbar-light .navbar-nav .nav-link {
color: rgba(46, 46, 46, .5)
color: rgba(0, 0, 0, .5)
}
.navbar-light .navbar-nav .nav-link:focus, .navbar-light .navbar-nav .nav-link:hover {
color: rgba(46, 46, 46, .7)
color: rgba(0, 0, 0, .7)
}
.navbar-light .navbar-nav .nav-link.disabled {
color: rgba(46, 46, 46, .3)
color: rgba(0, 0, 0, .3)
}
.navbar-light .navbar-nav .active > .nav-link, .navbar-light .navbar-nav .nav-link.active, .navbar-light .navbar-nav .nav-link.show, .navbar-light .navbar-nav .show > .nav-link {
color: rgba(46, 46, 46, .9)
color: rgba(0, 0, 0, .9)
}
.navbar-light .navbar-toggler {
color: rgba(46, 46, 46, .5);
border-color: rgba(46, 46, 46, .1)
color: rgba(0, 0, 0, .5);
border-color: rgba(0, 0, 0, .1)
}
.navbar-light .navbar-toggler-icon {
@@ -4486,11 +4486,7 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
}
.navbar-light .navbar-text {
color: rgba(46, 46, 46, .5)
}
.navbar-light .navbar-text a, .navbar-light .navbar-text a:focus, .navbar-light .navbar-text a:hover {
color: rgba(46, 46, 46, .9)
color: rgba(0, 0, 0, .5)
}
.navbar-dark .navbar-brand, .navbar-dark .navbar-brand:focus, .navbar-dark .navbar-brand:hover {
@@ -4498,24 +4494,24 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
}
.navbar-dark .navbar-nav .nav-link {
color: hsla(0, 0%, 18%, .5)
color: rgba(255, 255, 255, .5)
}
.navbar-dark .navbar-nav .nav-link:focus, .navbar-dark .navbar-nav .nav-link:hover {
color: hsla(0, 0%, 18%, .75)
color: rgba(255, 255, 255, .75)
}
.navbar-dark .navbar-nav .nav-link.disabled {
color: hsla(0, 0%, 18%, .25)
color: rgba(255, 255, 255, .25)
}
.navbar-dark .navbar-nav .active > .nav-link, .navbar-dark .navbar-nav .nav-link.active, .navbar-dark .navbar-nav .nav-link.show, .navbar-dark .navbar-nav .show > .nav-link {
color: #2e2e2e
color: #FFF
}
.navbar-dark .navbar-toggler {
color: rgba(46, 46, 46, 0.5);
border-color: rgba(46, 46, 46, 0.5);
color: rgba(255, 255, 255, .5);
border-color: rgba(255, 255, 255, .1)
}
.navbar-dark .navbar-toggler-icon {
@@ -4523,7 +4519,7 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
}
.navbar-dark .navbar-text {
color: hsla(0, 0%, 18%, .5)
color: rgba(255, 255, 255, .5)
}
.navbar-dark .navbar-text a, .navbar-dark .navbar-text a:focus, .navbar-dark .navbar-text a:hover {

View File

@@ -4460,28 +4460,28 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
}
.navbar-light .navbar-brand, .navbar-light .navbar-brand:focus, .navbar-light .navbar-brand:hover {
color: rgba(46, 46, 46, .9)
color: rgba(0, 0, 0, .9)
}
.navbar-light .navbar-nav .nav-link {
color: rgba(46, 46, 46, .5)
color: rgba(0, 0, 0, .5)
}
.navbar-light .navbar-nav .nav-link:focus, .navbar-light .navbar-nav .nav-link:hover {
color: rgba(46, 46, 46, .7)
color: rgba(0, 0, 0, .7)
}
.navbar-light .navbar-nav .nav-link.disabled {
color: rgba(46, 46, 46, .3)
color: rgba(0, 0, 0, .3)
}
.navbar-light .navbar-nav .active > .nav-link, .navbar-light .navbar-nav .nav-link.active, .navbar-light .navbar-nav .nav-link.show, .navbar-light .navbar-nav .show > .nav-link {
color: rgba(46, 46, 46, .9)
color: rgba(0, 0, 0, .9)
}
.navbar-light .navbar-toggler {
color: rgba(46, 46, 46, .5);
border-color: rgba(46, 46, 46, .1)
color: rgba(0, 0, 0, .5);
border-color: rgba(0, 0, 0, .1)
}
.navbar-light .navbar-toggler-icon {
@@ -4489,11 +4489,11 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
}
.navbar-light .navbar-text {
color: rgba(46, 46, 46, .5)
color: rgba(0, 0, 0, .5)
}
.navbar-light .navbar-text a, .navbar-light .navbar-text a:focus, .navbar-light .navbar-text a:hover {
color: rgba(46, 46, 46, .9)
color: rgba(0, 0, 0, .9)
}
.navbar-dark .navbar-brand, .navbar-dark .navbar-brand:focus, .navbar-dark .navbar-brand:hover {
@@ -4501,24 +4501,24 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
}
.navbar-dark .navbar-nav .nav-link {
color: hsla(0, 0%, 5%, .5)
color: rgba(255, 255, 255, .5)
}
.navbar-dark .navbar-nav .nav-link:focus, .navbar-dark .navbar-nav .nav-link:hover {
color: hsla(0, 0%, 5%, .75)
color: rgba(255, 255, 255, .75)
}
.navbar-dark .navbar-nav .nav-link.disabled {
color: hsla(0, 0%, 5%, .25)
color: rgba(255, 255, 255, .25)
}
.navbar-dark .navbar-nav .active > .nav-link, .navbar-dark .navbar-nav .nav-link.active, .navbar-dark .navbar-nav .nav-link.show, .navbar-dark .navbar-nav .show > .nav-link {
color: #1E1E1E
color: #FFF
}
.navbar-dark .navbar-toggler {
color: rgba(46, 46, 46, 0.5);
border-color: rgba(46, 46, 46, 0.5);
color: rgba(255, 255, 255, .5);
border-color: rgba(255, 255, 255, .1)
}
.navbar-dark .navbar-toggler-icon {
@@ -4526,7 +4526,7 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
}
.navbar-dark .navbar-text {
color: hsla(0, 0%, 18%, .5)
color: rgba(255, 255, 255, .5)
}
.navbar-dark .navbar-text a, .navbar-dark .navbar-text a:focus, .navbar-dark .navbar-text a:hover {

View File

@@ -3,6 +3,8 @@
{% load theming_tags %}
{% load custom_tags %}
{% theme_values request as theme_values %}
<html>
<head>
<title>{% block title %}
@@ -11,28 +13,28 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex,nofollow"/>
<link rel="shortcut icon" type="image/x-icon" href="{% static 'assets/favicon.svg' %}">
<link rel="shortcut icon" href="{% static 'assets/favicon.svg' %}">
<link rel="icon" type="image/png" href="{% static 'assets/favicon-32x32.png' %}" sizes="32x32">
<link rel="icon" type="image/png" href="{% static 'assets/favicon-16x16.png' %}" sizes="16x16">
<link rel="mask-icon" href="{% static 'assets/safari-pinned-tab.svg' %}" color="#161616">
<link rel="apple-touch-icon" href="{% static 'assets/apple-touch-icon.png' %}" sizes="180x180">
<link rel="icon" href="{{ theme_values.logo_color_svg }}">
<link rel="icon" href="{{ theme_values.logo_color_32 }}" sizes="32x32">
<link rel="icon" href="{{ theme_values.logo_color_128 }}" sizes="128x128">
<link rel="icon" href="{{ theme_values.logo_color_192 }}" sizes="192x192">
<link rel="apple-touch-icon" href="{{ theme_values.logo_color_180 }}" sizes="180x180">
<link rel="manifest" crossorigin="use-credentials" href="{% url 'web_manifest' %}">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
<meta name="msapplication-TileColor" content="{{ theme_values.nav_bg_color }}">
<meta name="msapplication-TileImage" content="{{ theme_values.logo_color_144 }}">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161616">
<meta name="msapplication-TileColor" content="#161616">
<meta name="theme-color" content="#ffffff">
<meta name="theme-color" content="{{ theme_values.nav_bg_color }}">
<meta name="apple-mobile-web-app-capable" content="yes"/>
<!-- Bootstrap 4 -->
<link id="id_main_css" href="{% theme_url request %}" rel="stylesheet">
<link id="id_main_css" href="{{ theme_values.theme }}" rel="stylesheet">
{% if theme_values.custom_theme %}
<link id="id_custom_css" href="{{ theme_values.custom_theme }}" rel="stylesheet">
{% endif %}
<link href="{% static 'css/app.min.css' %}" rel="stylesheet">
<script src="{% static 'js/jquery-3.5.1.min.js' %}"></script>
@@ -74,15 +76,15 @@
</head>
<body>
<nav class="navbar navbar-expand-lg {% nav_color request %}"
<nav class="navbar navbar-expand-lg {{ theme_values.nav_text_class }}"
id="id_main_nav"
style="{% sticky_nav request %}">
style="{{ theme_values.sticky_nav }}; background-color: {{ theme_values.nav_bg_color }}">
{% if not request.user.userpreference.left_handed %}
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
{% if not request.user.is_authenticated or request.user.userpreference.nav_show_logo %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
aria-label="Tandoor">
<img class="brand-icon" src="{% logo_url request %}" alt="Logo">
<img class="brand-icon" src="{{ theme_values.nav_logo }}" alt="Logo">
</a>
{% endif %}
{% endif %}
@@ -93,10 +95,10 @@
</button>
{% if request.user.userpreference.left_handed %}
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
{% if not request.user.is_authenticated or request.user.userpreference.nav_show_logo %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
aria-label="Tandoor">
<img class="brand-icon" src="{% logo_url request %}" alt="Logo">
<img class="brand-icon" src="{{ theme_values.nav_logo }}" alt="Logo">
</a>
{% endif %}
{% endif %}

View File

@@ -129,7 +129,7 @@
[](https://github.com/vabene1111/recipes)
[GitHub](https://github.com/vabene1111/recipes)
![{% trans 'This will become an image' %}]({% static 'assets/favicon.svg' %})
![{% trans 'This will become an image' %}]({% static 'assets/logo_color_svg.svg' %})
</code></pre>
<div style="text-align: center">
@@ -142,7 +142,7 @@
<div class="card-body">
<a href="https://github.com/vabene1111/recipes">https://github.com/vabene1111/recipes</a> <br/>
<a href="https://github.com/vabene1111/recipes">GitHub</a> <br/>
<img src="{% static 'assets/favicon.svg' %}" class="img-fluid" alt="{% trans 'This will become an image' %}"
<img src="{% static 'assets/logo_color_svg.svg' %}" class="img-fluid" alt="{% trans 'This will become an image' %}"
style="height: 3vw">
</div>

View File

@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% block title %}{% trans 'Property Editor' %}{% endblock %}
{% block content_fluid %}
<div id="app">
<property-editor-view></property-editor-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.RECIPE_ID = {{ recipe_id }}
</script>
{% render_bundle 'property_editor_view' %}
{% endblock %}

View File

@@ -83,23 +83,94 @@
{% trans 'Everything is fine!' %}
{% endif %}
<h4 class="mt-3">{% trans 'Database' %} <span
class="badge badge-{% if postgres %}warning{% else %}success{% endif %}">{% if postgres %}
{% trans 'Info' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
{% if postgres %}
<h4 class="mt-3">{% trans 'Database' %}
<span class="badge badge-{{ postgres_status }}">
{% if postgres_status == 'warning' %}
{% trans 'Info' %}
{% elif postgres_status == 'danger' %}
{% trans 'Warning' %}
{% else %}
{% trans 'Ok' %}
{% endif %}
</span>
</h4>
{{ postgres_message }}
<h4 class="mt-3">{% trans 'Migrations' %}
<span
class="badge badge-{% if missing_migration %}danger{% else %}success{% endif %}">{% if missing_migration %}
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
<p>
{% blocktrans %}
This application is not running with a Postgres database backend. This is ok but not recommended as some
features only work with postgres databases.
Migrations should never fail!
Failed migrations will likely cause major parts of the app to not function correctly.
If a migration fails make sure you are on the latest version and if so please post the migration log and the overview below in a GitHub issue.
{% endblocktrans %}
{% else %}
{% trans 'Everything is fine!' %}
{% endif %}
</p>
<table class="table mt-3">
<thead>
<tr>
<th>App</th>
<th class="text-right">{% trans 'Migrations' %}</th>
</tr>
</thead>
{% for key,value in migration_info.items %}
<tr>
<td>{{ value.app }}</td>
<td class="text-right">
<span class="badge badge-{% if value.unapplied_migrations|length > 0 %}danger{% else %}success{% endif %}">
{{ value.applied_migrations|length }} / {{ value.total }}
</span>
</td>
</tr>
{% for u in value.unapplied_migrations %}
<tr>
<td>
{{ u }}
</td>
<td></td>
</tr>
{% endfor %}
{% endfor %}
</table>
{# <h4 class="mt-3">#}
{# {% trans 'Orphaned Files' %}#}
{##}
{# <span class="badge badge-{% if orphans|length == 0 %}success{% elif orphans|length <= 25 %}warning{% else %}danger{% endif %}">#}
{# {% if orphans|length == 0 %}{% trans 'Success' %}#}
{# {% elif orphans|length <= 25 %}{% trans 'Warning' %}#}
{# {% else %}{% trans 'Danger' %}#}
{# {% endif %}#}
{# </span>#}
{# </h4>#}
{# {% if orphans|length == 0 %}#}
{# {% trans 'Everything is fine!' %}#}
{# {% else %}#}
{# {% blocktrans with orphan_count=orphans|length %}#}
{# There are currently {{ orphan_count }} orphaned files.#}
{# {% endblocktrans %}#}
{# <br>#}
{# <button id="toggle-button" class="btn btn-info btn-sm" onclick="toggleOrphans()">{% trans 'Show' %}</button>#}
{# <button class="btn btn-info btn-sm" onclick="deleteOrphans()">{% trans 'Delete' %}</button>#}
{# {% endif %}#}
{# <textarea id="orphans-list" style="display:none;" class="form-control" rows="20">#}
{#{% for orphan in orphans %}{{ orphan }}#}
{#{% endfor %}#}
{# </textarea>#}
<h4 class="mt-3">Debug</h4>
<textarea class="form-control" rows="20">
Gunicorn Media: {{ gunicorn_media }}
Sqlite: {{ postgres }}
Debug: {{ debug }}
Sqlite: {% if postgres %} {% trans 'False' %} {% else %} {% trans 'True' %} {% endif %}
{% if postgres %}PostgreSQL: {{ postgres_version }} {% endif %}
Debug: {{ debug }}
{% for key,value in request.META.items %}{% if key in 'SERVER_PORT,REMOTE_HOST,REMOTE_ADDR,SERVER_PROTOCOL' %}{{ key }}:{{ value }}
{% endif %}{% endfor %}
@@ -110,4 +181,31 @@ Debug: {{ debug }}
</textarea>
<br/>
<br/>
{% endblock %}
<form method="POST" id="delete-form">
{% csrf_token %}
<input type="hidden" name="delete_orphans" value="false">
</form>
{% block script %}
<script>
function toggleOrphans() {
var orphansList = document.getElementById('orphans-list');
var button = document.getElementById('toggle-button');
if (orphansList.style.display === 'none') {
orphansList.style.display = 'block';
button.innerText = "{% trans 'Hide' %}";
} else {
orphansList.style.display = 'none';
button.innerText = "{% trans 'Show' %}";
}
}
function deleteOrphans() {
document.getElementById('delete-form').delete_orphans.value = 'true';
document.getElementById('delete-form').submit();
}
</script>
{% endblock script %}
{% endblock %}

View File

@@ -1,16 +1,26 @@
from django import template
from django.templatetags.static import static
from django_scopes import scopes_disabled
from cookbook.models import UserPreference
from recipes.settings import STICKY_NAV_PREF_DEFAULT
from cookbook.models import UserPreference, UserFile, Space
from recipes.settings import STICKY_NAV_PREF_DEFAULT, UNAUTHENTICATED_THEME_FROM_SPACE
register = template.Library()
@register.simple_tag
def theme_url(request):
if not request.user.is_authenticated:
return static('themes/tandoor.min.css')
def theme_values(request):
return get_theming_values(request)
def get_theming_values(request):
space = None
if getattr(request,'space',None):
space = request.space
if not request.user.is_authenticated and UNAUTHENTICATED_THEME_FROM_SPACE > 0:
with scopes_disabled():
space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first()
themes = {
UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css',
UserPreference.FLATLY: 'themes/flatly.min.css',
@@ -19,35 +29,51 @@ def theme_url(request):
UserPreference.TANDOOR: 'themes/tandoor.min.css',
UserPreference.TANDOOR_DARK: 'themes/tandoor_dark.min.css',
}
if request.user.userpreference.theme in themes:
return static(themes[request.user.userpreference.theme])
else:
raise AttributeError
nav_text_type_mapping = {Space.DARK: 'navbar-light',
Space.LIGHT: 'navbar-dark'} # inverted since navbar-dark means the background
tv = {
'logo_color_32': static('assets/logo_color_32.png'),
'logo_color_128': static('assets/logo_color_128.png'),
'logo_color_144': static('assets/logo_color_144.png'),
'logo_color_180': static('assets/logo_color_180.png'),
'logo_color_192': static('assets/logo_color_192.png'),
'logo_color_512': static('assets/logo_color_512.png'),
'logo_color_svg': static('assets/logo_color_svg.svg'),
'custom_theme': None,
'theme': static(themes[UserPreference.TANDOOR]),
'nav_logo': static('assets/brand_logo.png'),
'nav_bg_color': '#ddbf86',
'nav_text_class': 'navbar-light',
'sticky_nav': 'position: sticky; top: 0; left: 0; z-index: 1000;',
'app_name': 'Tandoor Recipes',
}
@register.simple_tag
def logo_url(request):
if request.user.is_authenticated and getattr(getattr(request, "space", {}), 'image', None):
return request.space.image.file.url
else:
return static('assets/brand_logo.png')
if request.user.is_authenticated:
if request.user.userpreference.theme in themes:
tv['theme'] = static(themes[request.user.userpreference.theme])
if request.user.userpreference.nav_bg_color:
tv['nav_bg_color'] = request.user.userpreference.nav_bg_color
if request.user.userpreference.nav_text_color and request.user.userpreference.nav_text_color in nav_text_type_mapping:
tv['nav_text_class'] = nav_text_type_mapping[request.user.userpreference.nav_text_color]
if not request.user.userpreference.nav_sticky:
tv['sticky_nav'] = ''
if space:
for logo in list(tv.keys()):
if logo.startswith('logo_color_') and getattr(space, logo, None):
tv[logo] = getattr(space, logo).file.url
@register.simple_tag
def nav_color(request):
if not request.user.is_authenticated:
return 'navbar-light bg-primary'
if request.user.userpreference.nav_color.lower() in ['light', 'warning', 'info', 'success']:
return f'navbar-light bg-{request.user.userpreference.nav_color.lower()}'
else:
return f'navbar-dark bg-{request.user.userpreference.nav_color.lower()}'
@register.simple_tag
def sticky_nav(request):
if (not request.user.is_authenticated and STICKY_NAV_PREF_DEFAULT) or \
(request.user.is_authenticated and request.user.userpreference.sticky_navbar): # noqa: E501
return 'position: sticky; top: 0; left: 0; z-index: 1000;'
else:
return ''
if space.custom_space_theme:
tv['custom_theme'] = space.custom_space_theme.file.url
if space.space_theme in themes:
tv['theme'] = static(themes[space.space_theme])
if space.nav_logo:
tv['nav_logo'] = space.nav_logo.file.url
if space.nav_bg_color:
tv['nav_bg_color'] = space.nav_bg_color
if space.nav_text_color and space.nav_text_color in nav_text_type_mapping:
tv['nav_text_class'] = nav_text_type_mapping[space.nav_text_color]
if space.app_name:
tv['app_name'] = space.app_name
return tv

View File

@@ -61,6 +61,12 @@ def test_list_filter(obj_1, u1_s1):
response = json.loads(r.content)
assert len(response) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?meal_type={response[0]["meal_type"]["id"]}').content)
assert len(response) == 1
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?meal_type=0').content)
assert len(response) == 0
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?from_date={(datetime.now() + timedelta(days=2)).strftime("%Y-%m-%d")}').content)
assert len(response) == 0

View File

@@ -0,0 +1,68 @@
from django.contrib import auth
from django.templatetags.static import static
from django.test import RequestFactory
from django_scopes import scopes_disabled
from cookbook.models import Space, UserPreference, UserFile
from cookbook.templatetags.theming_tags import theme_values, get_theming_values
def test_theming_function(space_1, u1_s1):
# uf = UserFile.objects.create(name='test', space=space_1, created_by=user) #TODO add file tests
user = auth.get_user(u1_s1)
request = RequestFactory()
request.user = auth.get_user(u1_s1)
request.space = space_1
# defaults apply without setting anything (user preference is automatically created with these defaults)
assert get_theming_values(request)['theme'] == static('themes/tandoor.min.css')
assert get_theming_values(request)['nav_bg_color'] == '#ddbf86'
assert get_theming_values(request)['nav_text_class'] == 'navbar-light'
assert get_theming_values(request)['nav_logo'] == static('assets/brand_logo.png')
assert get_theming_values(request)['sticky_nav'] == 'position: sticky; top: 0; left: 0; z-index: 1000;'
assert get_theming_values(request)['app_name'] == 'Tandoor Recipes'
with scopes_disabled():
up = UserPreference.objects.filter(user=request.user).first()
up.theme = UserPreference.TANDOOR_DARK
up.nav_bg_color = '#ffffff'
up.nav_text_color = UserPreference.LIGHT
up.nav_sticky = False
up.save()
request = RequestFactory()
request.user = auth.get_user(u1_s1)
request.space = space_1
# user values apply if only those are present
assert get_theming_values(request)['theme'] == static('themes/tandoor_dark.min.css')
assert get_theming_values(request)['nav_bg_color'] == '#ffffff'
assert get_theming_values(request)['nav_text_class'] == 'navbar-dark'
assert get_theming_values(request)['sticky_nav'] == ''
assert get_theming_values(request)['app_name'] == 'Tandoor Recipes'
space_1.space_theme = Space.BOOTSTRAP
space_1.nav_bg_color = '#000000'
space_1.nav_text_color = UserPreference.DARK
space_1.app_name = 'test_app_name'
space_1.save()
request = RequestFactory()
request.user = auth.get_user(u1_s1)
request.space = space_1
# space settings apply when set
assert get_theming_values(request)['theme'] == static('themes/bootstrap.min.css')
assert get_theming_values(request)['nav_bg_color'] == '#000000'
assert get_theming_values(request)['nav_text_class'] == 'navbar-light'
assert get_theming_values(request)['app_name'] == 'test_app_name'
user.userspace_set.all().delete()
request = RequestFactory()
request.user = auth.get_user(u1_s1)
# default user settings should apply when user has no space
assert get_theming_values(request)['nav_bg_color'] == '#ffffff'
assert get_theming_values(request)['nav_text_class'] == 'navbar-dark'
assert get_theming_values(request)['nav_logo'] == static('assets/brand_logo.png')

View File

@@ -43,7 +43,7 @@ router.register(r'recipe', api.RecipeViewSet)
router.register(r'recipe-book', api.RecipeBookViewSet)
router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
router.register(r'unit-conversion', api.UnitConversionViewSet)
router.register(r'food-property-type', api.PropertyTypeViewSet)
router.register(r'food-property-type', api.PropertyTypeViewSet) # TODO rename + regenerate
router.register(r'food-property', api.PropertyViewSet)
router.register(r'shopping-list', api.ShoppingListViewSet)
router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet)
@@ -91,6 +91,7 @@ urlpatterns = [
path('history/', views.history, name='view_history'),
path('supermarket/', views.supermarket, name='view_supermarket'),
path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'),
path('property-editor/<int:pk>', views.property_editor, name='view_property_editor'),
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
path('api/import/', api.import_files, name='view_import'),
@@ -128,7 +129,7 @@ urlpatterns = [
path('api/sync_all/', api.sync_all, name='api_sync'),
path('api/log_cooking/<int:recipe_id>/', api.log_cooking, name='api_log_cooking'),
path('api/plan-ical/<slug:from_date>/<slug:to_date>/', api.get_plan_ical, name='api_get_plan_ical'),
path('api/recipe-from-source/', api.recipe_from_source, name='api_recipe_from_source'),
path('api/recipe-from-source/', api.RecipeUrlImportView.as_view(), name='api_recipe_from_source'),
path('api/backup/', api.get_backup, name='api_backup'),
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
@@ -161,8 +162,7 @@ urlpatterns = [
path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript', )),
name='service_worker'),
path('manifest.json', (TemplateView.as_view(template_name="manifest.json", content_type='application/json', )),
name='web_manifest'),
path('manifest.json', views.web_manifest, name='web_manifest'),
]
generic_models = (

View File

@@ -46,7 +46,7 @@ from rest_framework.pagination import PageNumberPagination
from rest_framework.parsers import MultiPartParser
from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
from rest_framework.response import Response
from rest_framework.throttling import AnonRateThrottle
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from treebeard.exceptions import InvalidMoveToDescendant, InvalidPosition, PathOverflow
@@ -70,12 +70,13 @@ from cookbook.helper.recipe_url_import import (clean_dict, get_from_youtube_scra
from cookbook.helper.scrapers.scrapers import text_scraper
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilter, ExportLog, Food,
FoodInheritField, ImportLog, Ingredient, InviteLink, Keyword, MealPlan,
MealType, Property, PropertyType, Recipe, RecipeBook, RecipeBookEntry,
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog)
FoodInheritField, FoodProperty, ImportLog, Ingredient, InviteLink,
Keyword, MealPlan, MealType, Property, PropertyType, Recipe,
RecipeBook, RecipeBookEntry, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
ViewLog)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
@@ -104,6 +105,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
UserSerializer, UserSpaceSerializer, ViewLogSerializer)
from cookbook.views.import_export import get_integration
from recipes import settings
from recipes.settings import FDC_API_KEY, DRF_THROTTLE_RECIPE_URL_IMPORT
class StandardFilterMixin(ViewSetMixin):
@@ -595,6 +597,54 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
created_by=request.user)
return Response(content, status=status.HTTP_204_NO_CONTENT)
@decorators.action(detail=True, methods=['POST'], )
def fdc(self, request, pk):
"""
updates the food with all possible data from the FDC Api
if properties with a fdc_id already exist they will be overridden, if existing properties don't have a fdc_id they won't be changed
"""
food = self.get_object()
response = requests.get(f'https://api.nal.usda.gov/fdc/v1/food/{food.fdc_id}?api_key={FDC_API_KEY}')
if response.status_code == 429:
return JsonResponse({'msg', 'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. Configure your key in Tandoor using environment FDC_API_KEY variable.'}, status=429,
json_dumps_params={'indent': 4})
try:
data = json.loads(response.content)
food_property_list = []
# delete all properties where the property type has a fdc_id as these should be overridden
for fp in food.properties.all():
if fp.property_type.fdc_id:
fp.delete()
for pt in PropertyType.objects.filter(space=request.space, fdc_id__gte=0).all():
if pt.fdc_id:
for fn in data['foodNutrients']:
if fn['nutrient']['id'] == pt.fdc_id:
food_property_list.append(Property(
property_type_id=pt.id,
property_amount=round(fn['amount'], 2),
import_food_id=food.id,
space=self.request.space,
))
Property.objects.bulk_create(food_property_list, ignore_conflicts=True, unique_fields=('space', 'import_food_id', 'property_type',))
property_food_relation_list = []
for p in Property.objects.filter(space=self.request.space, import_food_id=food.id).values_list('import_food_id', 'id', ):
property_food_relation_list.append(Food.properties.through(food_id=p[0], property_id=p[1]))
FoodProperty.objects.bulk_create(property_food_relation_list, ignore_conflicts=True, unique_fields=('food_id', 'property_id',))
Property.objects.filter(space=self.request.space, import_food_id=food.id).update(import_food_id=None)
return self.retrieve(request, pk)
except Exception:
traceback.print_exc()
return JsonResponse({'msg': 'there was an error parsing the FDC data, please check the server logs'}, status=500, json_dumps_params={'indent': 4})
def destroy(self, *args, **kwargs):
try:
return (super().destroy(self, *args, **kwargs))
@@ -649,11 +699,18 @@ class MealPlanViewSet(viewsets.ModelViewSet):
- **from_date**: filter from (inclusive) a certain date onward
- **to_date**: filter upward to (inclusive) certain date
- **meal_type**: filter meal plans based on meal_type ID
"""
queryset = MealPlan.objects
serializer_class = MealPlanSerializer
permission_classes = [(CustomIsOwner | CustomIsShared) & CustomTokenHasReadWriteScope]
query_params = [
QueryParam(name='from_date', description=_('Filter meal plans from date (inclusive) in the format of YYYY-MM-DD.'), qtype='string'),
QueryParam(name='to_date', description=_('Filter meal plans to date (inclusive) in the format of YYYY-MM-DD.'), qtype='string'),
QueryParam(name='meal_type', description=_('Filter meal plans with MealType ID. For multiple repeat parameter.'), qtype='int'),
]
schema = QueryParamAutoSchema()
def get_queryset(self):
queryset = self.queryset.filter(
@@ -668,6 +725,11 @@ class MealPlanViewSet(viewsets.ModelViewSet):
to_date = self.request.query_params.get('to_date', None)
if to_date is not None:
queryset = queryset.filter(to_date__lte=to_date)
meal_type = self.request.query_params.getlist('meal_type', [])
if meal_type:
queryset = queryset.filter(meal_type__in=meal_type)
return queryset
@@ -676,7 +738,7 @@ class AutoPlanViewSet(viewsets.ViewSet):
serializer = AutoMealPlanSerializer(data=request.data)
if serializer.is_valid():
keywords = serializer.validated_data['keywords']
keyword_ids = serializer.validated_data['keyword_ids']
start_date = serializer.validated_data['start_date']
end_date = serializer.validated_data['end_date']
servings = serializer.validated_data['servings']
@@ -691,8 +753,8 @@ class AutoPlanViewSet(viewsets.ViewSet):
recipes = Recipe.objects.values('id', 'name')
meal_plans = list()
for keyword in keywords:
recipes = recipes.filter(keywords__name=keyword['name'])
for keyword_id in keyword_ids:
recipes = recipes.filter(keywords__id=keyword_id)
if len(recipes) == 0:
return Response(serializer.data)
@@ -1249,6 +1311,10 @@ class AuthTokenThrottle(AnonRateThrottle):
rate = '10/day'
class RecipeImportThrottle(UserRateThrottle):
rate = DRF_THROTTLE_RECIPE_URL_IMPORT
class CustomAuthToken(ObtainAuthToken):
throttle_classes = [AuthTokenThrottle]
@@ -1274,114 +1340,114 @@ class CustomAuthToken(ObtainAuthToken):
})
@api_view(['POST'])
# @schema(AutoSchema()) #TODO add proper schema
@permission_classes([CustomIsUser & CustomTokenHasReadWriteScope])
# TODO add rate limiting
def recipe_from_source(request):
"""
function to retrieve a recipe from a given url or source string
:param request: standard request with additional post parameters
- url: url to use for importing recipe
- data: if no url is given recipe is imported from provided source data
- (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes
:return: JsonResponse containing the parsed json and images
"""
scrape = None
serializer = RecipeFromSourceSerializer(data=request.data)
if serializer.is_valid():
class RecipeUrlImportView(APIView):
throttle_classes = [RecipeImportThrottle]
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
if (b_pk := serializer.validated_data.get('bookmarklet', None)) and (
bookmarklet := BookmarkletImport.objects.filter(pk=b_pk).first()):
serializer.validated_data['url'] = bookmarklet.url
serializer.validated_data['data'] = bookmarklet.html
bookmarklet.delete()
def post(self, request, *args, **kwargs):
"""
function to retrieve a recipe from a given url or source string
:param request: standard request with additional post parameters
- url: url to use for importing recipe
- data: if no url is given recipe is imported from provided source data
- (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes
:return: JsonResponse containing the parsed json and images
"""
scrape = None
serializer = RecipeFromSourceSerializer(data=request.data)
if serializer.is_valid():
url = serializer.validated_data.get('url', None)
data = unquote(serializer.validated_data.get('data', None))
if not url and not data:
return Response({
'error': True,
'msg': _('Nothing to do.')
}, status=status.HTTP_400_BAD_REQUEST)
if (b_pk := serializer.validated_data.get('bookmarklet', None)) and (
bookmarklet := BookmarkletImport.objects.filter(pk=b_pk).first()):
serializer.validated_data['url'] = bookmarklet.url
serializer.validated_data['data'] = bookmarklet.html
bookmarklet.delete()
elif url and not data:
if re.match('^(https?://)?(www\\.youtube\\.com|youtu\\.be)/.+$', url):
if validators.url(url, public=True):
return Response({
'recipe_json': get_from_youtube_scraper(url, request),
'recipe_images': [],
}, status=status.HTTP_200_OK)
if re.match(
'^(.)*/view/recipe/[0-9]+/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
url):
recipe_json = requests.get(
url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1],
'') + '?share=' +
re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
recipe_json = clean_dict(recipe_json, 'id')
serialized_recipe = RecipeExportSerializer(data=recipe_json, context={'request': request})
if serialized_recipe.is_valid():
recipe = serialized_recipe.save()
if validators.url(recipe_json['image'], public=True):
recipe.image = File(handle_image(request,
File(io.BytesIO(requests.get(recipe_json['image']).content),
name='image'),
filetype=pathlib.Path(recipe_json['image']).suffix),
name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}')
recipe.save()
return Response({
'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))
}, status=status.HTTP_201_CREATED)
else:
try:
url = serializer.validated_data.get('url', None)
data = unquote(serializer.validated_data.get('data', None))
if not url and not data:
return Response({
'error': True,
'msg': _('Nothing to do.')
}, status=status.HTTP_400_BAD_REQUEST)
elif url and not data:
if re.match('^(https?://)?(www\\.youtube\\.com|youtu\\.be)/.+$', url):
if validators.url(url, public=True):
scrape = scrape_me(url_path=url, wild_mode=True)
return Response({
'recipe_json': get_from_youtube_scraper(url, request),
'recipe_images': [],
}, status=status.HTTP_200_OK)
if re.match(
'^(.)*/view/recipe/[0-9]+/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
url):
recipe_json = requests.get(
url.replace('/view/recipe/', '/api/recipe/').replace(re.split('/view/recipe/[0-9]+', url)[1],
'') + '?share=' +
re.split('/view/recipe/[0-9]+', url)[1].replace('/', '')).json()
recipe_json = clean_dict(recipe_json, 'id')
serialized_recipe = RecipeExportSerializer(data=recipe_json, context={'request': request})
if serialized_recipe.is_valid():
recipe = serialized_recipe.save()
if validators.url(recipe_json['image'], public=True):
recipe.image = File(handle_image(request,
File(io.BytesIO(requests.get(recipe_json['image']).content),
name='image'),
filetype=pathlib.Path(recipe_json['image']).suffix),
name=f'{uuid.uuid4()}_{recipe.pk}{pathlib.Path(recipe_json["image"]).suffix}')
recipe.save()
return Response({
'link': request.build_absolute_uri(reverse('view_recipe', args={recipe.pk}))
}, status=status.HTTP_201_CREATED)
else:
try:
if validators.url(url, public=True):
scrape = scrape_me(url_path=url, wild_mode=True)
else:
else:
return Response({
'error': True,
'msg': _('Invalid Url')
}, status=status.HTTP_400_BAD_REQUEST)
except NoSchemaFoundInWildMode:
pass
except requests.exceptions.ConnectionError:
return Response({
'error': True,
'msg': _('Invalid Url')
'msg': _('Connection Refused.')
}, status=status.HTTP_400_BAD_REQUEST)
except NoSchemaFoundInWildMode:
except requests.exceptions.MissingSchema:
return Response({
'error': True,
'msg': _('Bad URL Schema.')
}, status=status.HTTP_400_BAD_REQUEST)
else:
try:
data_json = json.loads(data)
if '@context' not in data_json:
data_json['@context'] = 'https://schema.org'
if '@type' not in data_json:
data_json['@type'] = 'Recipe'
data = "<script type='application/ld+json'>" + json.dumps(data_json) + "</script>"
except JSONDecodeError:
pass
except requests.exceptions.ConnectionError:
return Response({
'error': True,
'msg': _('Connection Refused.')
}, status=status.HTTP_400_BAD_REQUEST)
except requests.exceptions.MissingSchema:
return Response({
'error': True,
'msg': _('Bad URL Schema.')
}, status=status.HTTP_400_BAD_REQUEST)
else:
try:
data_json = json.loads(data)
if '@context' not in data_json:
data_json['@context'] = 'https://schema.org'
if '@type' not in data_json:
data_json['@type'] = 'Recipe'
data = "<script type='application/ld+json'>" + json.dumps(data_json) + "</script>"
except JSONDecodeError:
pass
scrape = text_scraper(text=data, url=url)
if not url and (found_url := scrape.schema.data.get('url', None)):
scrape = text_scraper(text=data, url=found_url)
scrape = text_scraper(text=data, url=url)
if not url and (found_url := scrape.schema.data.get('url', None)):
scrape = text_scraper(text=data, url=found_url)
if scrape:
return Response({
'recipe_json': helper.get_from_scraper(scrape, request),
'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))),
}, status=status.HTTP_200_OK)
if scrape:
return Response({
'recipe_json': helper.get_from_scraper(scrape, request),
'recipe_images': list(dict.fromkeys(get_images_from_soup(scrape.soup, url))),
}, status=status.HTTP_200_OK)
else:
return Response({
'error': True,
'msg': _('No usable data could be found.')
}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response({
'error': True,
'msg': _('No usable data could be found.')
}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET'])
@@ -1454,7 +1520,7 @@ def import_files(request):
"""
limit, msg = above_space_limit(request.space)
if limit:
return Response({'error': msg}, status=status.HTTP_400_BAD_REQUEST)
return Response({'error': True, 'msg': _('File is above space limit')}, status=status.HTTP_400_BAD_REQUEST)
form = ImportForm(request.POST, request.FILES)
if form.is_valid() and request.FILES != {}:

View File

@@ -1,16 +1,23 @@
import json
import os
import re
from datetime import datetime
from io import StringIO
from uuid import UUID
import subprocess
from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import Group
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.core.management import call_command
from django.db import models
from django.http import HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.templatetags.static import static
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext as _
@@ -18,13 +25,15 @@ from django_scopes import scopes_disabled
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, SpaceCreateForm,
SpaceJoinForm, User, UserCreateForm, UserPreference)
from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.permission_helper import (group_required, has_group_permission,
share_link_valid, switch_user_active_space)
from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference,
ShareLink, Space, UserSpace, ViewLog)
from cookbook.tables import CookLogTable, ViewLogTable
from cookbook.templatetags.theming_tags import get_theming_values
from cookbook.version_info import VERSION_INFO
from recipes.settings import PLUGINS
from recipes.settings import PLUGINS, BASE_DIR
def index(request):
@@ -70,6 +79,11 @@ def space_overview(request):
messages.add_message(request, messages.WARNING, _('This feature is not available in the demo version!'))
else:
if create_form.is_valid():
if Space.objects.filter(created_by=request.user).count() >= request.user.userpreference.max_owned_spaces:
messages.add_message(request, messages.ERROR,
_('You have the reached the maximum amount of spaces that can be owned by you.') + f' ({request.user.userpreference.max_owned_spaces})')
return HttpResponseRedirect(reverse('view_space_overview'))
created_space = Space.objects.create(
name=create_form.cleaned_data['name'],
created_by=request.user,
@@ -204,6 +218,11 @@ def ingredient_editor(request):
return render(request, 'ingredient_editor.html', template_vars)
@group_required('user')
def property_editor(request, pk):
return render(request, 'property_editor.html', {'recipe_id': pk})
@group_required('guest')
def shopping_settings(request):
if request.space.demo:
@@ -220,10 +239,10 @@ def shopping_settings(request):
if not sp:
sp = SearchPreferenceForm(user=request.user)
fields_searched = (
len(search_form.cleaned_data['icontains'])
+ len(search_form.cleaned_data['istartswith'])
+ len(search_form.cleaned_data['trigram'])
+ len(search_form.cleaned_data['fulltext'])
len(search_form.cleaned_data['icontains'])
+ len(search_form.cleaned_data['istartswith'])
+ len(search_form.cleaned_data['trigram'])
+ len(search_form.cleaned_data['fulltext'])
)
if search_form.cleaned_data['preset'] == 'fuzzy':
sp.search = SearchPreference.SIMPLE
@@ -309,17 +328,75 @@ def system(request):
if not request.user.is_superuser:
return HttpResponseRedirect(reverse('index'))
postgres_ver = None
postgres = settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'
if postgres:
postgres_current = 16 # will need to be updated as PostgreSQL releases new major versions
from decimal import Decimal
from django.db import connection
postgres_ver = Decimal(str(connection.pg_version).replace('00', '.'))
if postgres_ver >= postgres_current:
database_status = 'success'
database_message = _('Everything is fine!')
elif postgres_ver < postgres_current - 2:
database_status = 'danger'
database_message = _('PostgreSQL %(v)s is deprecated. Upgrade to a fully supported version!') % {
'v': postgres_ver}
else:
database_status = 'info'
database_message = _('You are running PostgreSQL %(v1)s. PostgreSQL %(v2)s is recommended') % {
'v1': postgres_ver, 'v2': postgres_current}
else:
database_status = 'info'
database_message = _(
'This application is not running with a Postgres database backend. This is ok but not recommended as some features only work with postgres databases.')
secret_key = False if os.getenv('SECRET_KEY') else True
if request.method == "POST":
del_orphans = request.POST.get('delete_orphans')
orphans = get_orphan_files(delete_orphans=str2bool(del_orphans))
else:
orphans = get_orphan_files()
out = StringIO()
call_command('showmigrations', stdout=out)
missing_migration = False
migration_info = {}
current_app = None
for row in out.getvalue().splitlines():
if '[ ]' in row and current_app:
migration_info[current_app]['unapplied_migrations'].append(row.replace('[ ]', ''))
missing_migration = True
elif '[X]' in row and current_app:
migration_info[current_app]['applied_migrations'].append(row.replace('[x]', ''))
elif '(no migrations)' in row and current_app:
pass
else:
current_app = row
migration_info[current_app] = {'app': current_app, 'unapplied_migrations': [], 'applied_migrations': [],
'total': 0}
for key in migration_info.keys():
migration_info[key]['total'] = len(migration_info[key]['unapplied_migrations']) + len(
migration_info[key]['applied_migrations'])
return render(request, 'system.html', {
'gunicorn_media': settings.GUNICORN_MEDIA,
'debug': settings.DEBUG,
'postgres': postgres,
'postgres_version': postgres_ver,
'postgres_status': database_status,
'postgres_message': database_message,
'version_info': VERSION_INFO,
'plugins': PLUGINS,
'secret_key': secret_key
'secret_key': secret_key,
'orphans': orphans,
'migration_info': migration_info,
'missing_migration': missing_migration,
})
@@ -367,7 +444,8 @@ def invite_link(request, token):
link.used_by = request.user
link.save()
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=False)
user_space = UserSpace.objects.create(user=request.user, space=link.space,
internal_note=link.internal_note, invite_link=link, active=False)
if request.user.userspace_set.count() == 1:
user_space.active = True
@@ -408,6 +486,60 @@ def report_share_abuse(request, token):
return HttpResponseRedirect(reverse('index'))
def web_manifest(request):
theme_values = get_theming_values(request)
icons = [
{"src": theme_values['logo_color_svg'], "sizes": "any"},
{"src": theme_values['logo_color_144'], "type": "image/png", "sizes": "144x144"},
{"src": theme_values['logo_color_512'], "type": "image/png", "sizes": "512x512"}
]
manifest_info = {
"name": theme_values['app_name'],
"short_name": theme_values['app_name'],
"description": _("Manage recipes, shopping list, meal plans and more."),
"icons": icons,
"start_url": "./search",
"background_color": theme_values['nav_bg_color'],
"display": "standalone",
"scope": ".",
"theme_color": theme_values['nav_bg_color'],
"shortcuts": [
{
"name": _("Plan"),
"short_name": _("Plan"),
"description": _("View your meal Plan"),
"url": "./plan"
},
{
"name": _("Books"),
"short_name": _("Books"),
"description": _("View your cookbooks"),
"url": "./books"
},
{
"name": _("Shopping"),
"short_name": _("Shopping"),
"description": _("View your shopping lists"),
"url": "./list/shopping-list/"
}
],
"share_target": {
"action": "/data/import/url",
"method": "GET",
"params": {
"title": "title",
"url": "url",
"text": "text"
}
}
}
return JsonResponse(manifest_info, json_dumps_params={'indent': 4})
def markdown_info(request):
return render(request, 'markdown_info.html', {})
@@ -443,3 +575,48 @@ def test(request):
def test2(request):
if not settings.DEBUG:
return HttpResponseRedirect(reverse('index'))
def get_orphan_files(delete_orphans=False):
# Get list of all image files in media folder
media_dir = settings.MEDIA_ROOT
def find_orphans():
image_files = []
for root, dirs, files in os.walk(media_dir):
for file in files:
if not file.lower().endswith(('.db')) and not root.lower().endswith(('@eadir')):
full_path = os.path.join(root, file)
relative_path = os.path.relpath(full_path, media_dir)
image_files.append((relative_path, full_path))
# Get list of all image fields in models
image_fields = []
for model in apps.get_models():
for field in model._meta.get_fields():
if isinstance(field, models.ImageField) or isinstance(field, models.FileField):
image_fields.append((model, field.name))
# get all images in the database
# TODO I don't know why, but this completely bypasses scope limitations
image_paths = []
for model, field in image_fields:
image_field_paths = model.objects.values_list(field, flat=True)
image_paths.extend(image_field_paths)
# Check each image file against model image fields
return [img for img in image_files if img[0] not in image_paths]
orphans = find_orphans()
if delete_orphans:
for f in [img[1] for img in orphans]:
try:
os.remove(f)
except FileNotFoundError:
print(f"File not found: {f}")
except Exception as e:
print(f"Error deleting file {f}: {e}")
orphans = find_orphans()
return [img[1] for img in orphans]

View File

@@ -1,5 +1,5 @@
There are several questions and issues that come up from time to time, here are some answers:
please note that the existence of some questions is due the application not being perfect in some parts.
please note that the existence of some questions is due the application not being perfect in some parts.
Many of those shortcomings are planned to be fixed in future release but simply could not be addressed yet due to time limits.
## Is there a Tandoor app?
@@ -15,7 +15,7 @@ Open Tandoor, click the `add Tandoor to the home screen` message that pops up at
### Desktop browsers
#### Google Chrome
#### Google Chrome
Open Tandoor, open the menu behind the three vertical dots at the top right, select `Install Tandoor Recipes...`
#### Microsoft Edge
@@ -32,6 +32,17 @@ If you just set up your Tandoor instance and you're having issues like;
then make sure you have set [all required headers](install/docker.md#required-headers) in your reverse proxy correctly.
If that doesn't fix it, you can also refer to the appropriate sub section in the [reverse proxy documentation](install/docker.md#reverse-proxy) and verify your general webserver configuration.
### Required Headers
Navigate to `/system` and review the headers listed in the DEBUG section. At a minimum, if you are using a reverse proxy the headers must match the below conditions.
| Header | Requirement |
| :--- | :---- |
| HTTP_HOST:mydomain.tld | The host domain must match the url that you are using to open Tandoor. |
| HTTP_X_FORWARDED_HOST:mydomain.tld | The host domain must match the url that you are using to open Tandoor. |
| HTTP_X_FORWARDED_PROTO:http(s) | The protocol must match the url you are using to open Tandoor. There must be exactly one protocol listed. |
| HTTP_X_SCRIPT_NAME:/subfolder | If you are hosting Tandoor at a subfolder instead of a subdomain this header must exist. |
## Why am I getting CSRF Errors?
If you are getting CSRF Errors this is most likely due to a reverse proxy not passing the correct headers.
@@ -41,19 +52,22 @@ If you are using a plain ngix you might need `proxy_set_header Host $http_host;`
Further discussions can be found in this [Issue #518](https://github.com/vabene1111/recipes/issues/518)
## Why are images not loading?
If images are not loading this might be related to the same issue as the CSRF errors (see above).
If images are not loading this might be related to the same issue as the CSRF errors (see above).
A discussion about that can be found at [Issue #452](https://github.com/vabene1111/recipes/issues/452)
The other common issue is that the recommended nginx container is removed from the deployment stack.
If removed, the nginx webserver needs to be replaced by something else that servers the /mediafiles/ directory or
The other common issue is that the recommended nginx container is removed from the deployment stack.
If removed, the nginx webserver needs to be replaced by something else that servers the /mediafiles/ directory or
`GUNICORN_MEDIA` needs to be enabled to allow media serving by the application container itself.
## Why am I getting an error stating database files are incompatible with server?
Your version of Postgres has been upgraded. See [Updating PostgreSQL](https://docs.tandoor.dev/system/updating/#postgresql)
## Why does the Text/Markdown preview look different than the final recipe?
Tandoor has always rendered the recipe instructions markdown on the server. This also allows tandoor to implement things like ingredient templating and scaling in text.
To make editing easier a markdown editor was added to the frontend with integrated preview as a temporary solution. Since the markdown editor uses a different
specification than the server the preview is different to the final result. It is planned to improve this in the future.
To make editing easier a markdown editor was added to the frontend with integrated preview as a temporary solution. Since the markdown editor uses a different
specification than the server the preview is different to the final result. It is planned to improve this in the future.
The markdown renderer follows this markdown specification https://daringfireball.net/projects/markdown/
@@ -66,18 +80,18 @@ To create a new user click on your name (top right corner) and select 'space set
It is not possible to create users through the admin because users must be assigned a default group and space.
To change a user's space you need to go to the admin and select User Infos.
To change a user's space you need to go to the admin and select User Infos.
If you use an external auth provider or proxy authentication make sure to specify a default group and space in the
If you use an external auth provider or proxy authentication make sure to specify a default group and space in the
environment configuration.
## What are spaces?
Spaces are is a type of feature used to separate one installation of Tandoor into several parts.
Spaces are is a type of feature used to separate one installation of Tandoor into several parts.
In technical terms it is a multi-tenant system.
You can compare a space to something like google drive or dropbox.
You can compare a space to something like google drive or dropbox.
There is only one installation of the Dropbox system, but it handles multiple users without them noticing each other.
For Tandoor that means all people that work together on one recipe collection can be in one space.
For Tandoor that means all people that work together on one recipe collection can be in one space.
If you want to host the collection of your friends, family, or neighbor you can create a separate space for them (through the admin interface).
Sharing between spaces is currently not possible but is planned for future releases.
@@ -90,7 +104,7 @@ To reset a lost password if access to the container is lost you need to:
3. run `python manage.py changepassword <username>` and follow the steps shown.
## How can I add an admin user?
To create a superuser you need to
To create a superuser you need to
1. execute into the container using `docker-compose exec web_recipes sh`
2. activate the virtual environment `source venv/bin/activate`
@@ -98,11 +112,26 @@ To create a superuser you need to
## Why cant I get support for my manual setup?
Even tough I would love to help everyone get tandoor up and running I have only so much time
that I can spend on this project besides work, family and other life things.
Due to the countless problems that can occur when manually installing I simply do not have
the time to help solving each one.
Even tough I would love to help everyone get tandoor up and running I have only so much time
that I can spend on this project besides work, family and other life things.
Due to the countless problems that can occur when manually installing I simply do not have
the time to help solving each one.
You can install Tandoor manually but please do not expect me or anyone to help you with that.
As a general advice: If you do it manually do NOT change anything at first and slowly work yourself
to your dream setup.
to your dream setup.
## How can I upgrade postgres (major versions)?
Postgres requires manual intervention when updating from one major version to another. The steps are roughly
1. use `pg_dumpall` to dump your database into SQL (for Docker `docker-compose exec -T <postgres_container_name> pg_dumpall -U <postgres_user_name> -f /path/to/dump.sql`)
2. stop the DB / down the container
3. move your postgres directory in order to keep it as a backup (e.g. `mv postgres postgres_old`)
4. update postgres to the new major version (for Docker just change the version number and pull)
5. start the db / up the container (do not start tandoor as it will automatically perform the database migrations which will conflict with loading the dump)
6. if not using docker, you might need to create the same postgres user you had in the old database
7. load the postgres dump (for Docker `'/usr/local/bin/docker-compose exec -T <postgres_container_name> psql -U <postgres_user_name> <postgres_database_name> < /path/to/dump.sql`)
If anything fails, go back to the old postgres version and data directory and try again.
There are many articles and tools online that might provide a good starting point to help you upgrade [1](https://thomasbandt.com/postgres-docker-major-version-upgrade), [2](https://github.com/tianon/docker-postgres-upgrade), [3](https://github.com/vabene1111/DockerPostgresBackups).

View File

@@ -6,6 +6,12 @@ It is possible to install this application using many different Docker configura
Please read the instructions on each example carefully and decide if this is the way for you.
## **DockSTARTer**
The main goal of [DockSTARTer](https://dockstarter.com/) is to make it quick and easy to get up and running with Docker.
You may choose to rely on DockSTARTer for various changes to your Docker system or use DockSTARTer as a stepping stone and learn to do more advanced configurations.
Follow the guide for installing DockSTARTer and then run `ds` then select 'Configuration' and 'Select Apps' to get Tandoor up and running quickly and easily.
## **Docker**
The docker image (`vabene1111/recipes`) simply exposes the application on the container's port `8080`.
@@ -110,7 +116,7 @@ in combination with [jrcs's letsencrypt companion](https://hub.docker.com/r/jrcs
Please refer to the appropriate documentation on how to setup the reverse proxy and networks.
!!! warning "Adjust client_max_body_size"
By using jwilder's Nginx-proxy, uploads will be restricted to 1 MB file size. This can be resolved by adjusting the ```client_max_body_size``` variable in the jwilder nginx configuration.
By using jwilder's Nginx-proxy, uploads will be restricted to 1 MB file size. This can be resolved by adjusting the ```client_max_body_size``` variable in the jwilder nginx configuration.
Remember to add the appropriate environment variables to the `.env` file:
@@ -343,10 +349,9 @@ ProxyPassReverse / http://localhost:8080/ # replace port
!!!info
Always wait at least 2-3 minutes after the very first start, since migrations will take some time!
!!!warning
If you want to use Tandoor on a Raspberry Pi running a 32-bit operating system you will need to use the following
docker image tags: `latest-raspi`, `beta-raspi` and the versioned `<x.y.z>-raspi`
We strongly recommend using the new 64-bit Raspian image as the 32-bit version is not tested.
!!!info
In the past there was a special `*-raspi` version of the image. This no longer exists. The normal Tags all support Arm/v7 architectures which should work on all Raspberry Pi's above Version 1 and the first generation Zero.
See [Wikipedia Raspberry Pi specifications](https://en.wikipedia.org/wiki/Raspberry_Pi#Specifications).
If you're having issues with installing Tandoor on your Raspberry Pi or similar device,
follow these instructions:
@@ -360,11 +365,11 @@ follow these instructions:
### Sub Path nginx config
If hosting under a sub-path you might want to change the default nginx config (which gets mounted through the named volume from the application container into the nginx container)
with the following config.
with the following config.
```nginx
location /my_app { # change to subfolder name
include /config/nginx/proxy.conf;
include /config/nginx/proxy.conf;
proxy_pass https://mywebapp.com/; # change to your host name:port
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -71,12 +71,13 @@ Basic guide to setup Docker and Portainer TrueNAS Core.
-Select "Get Started" to use the Enviroment Portainer is running in
![Screenshot of Enviroment Wizard](https://2914113074-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiZWHJxqQsgWYd9sI88sO%2Fuploads%2Fsig45vFliINvOKGKVStk%2F2.15-install-server-setup-wizard.png?alt=media&token=cd21d9e8-0632-40db-af9a-581365f98209)
### 3. Install Tandoor Recipies VIA Portainer Web Editor
### 3. Install Tandoor Recipes VIA Portainer Web Editor
-From the menu select Stacks, click Add stack, give the stack a descriptive name then select Web editor.
![Screenshot of Stack List](https://2914113074-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiZWHJxqQsgWYd9sI88sO%2Fuploads%2FnBx62EIPhmUy1L0S1iKI%2F2.15-docker_add_stack_web_editor.gif?alt=media&token=c45c0151-9c15-4d79-b229-1a90a7a86b84)
-Use the below code and input it into the Web Editor:
`version: "3"
```yaml
version: "3"
services:
db_recipes:
restart: always
@@ -87,13 +88,12 @@ services:
- stack.env
web_recipes:
# image: vabene1111/recipes:latest
image: vabene1111/recipes:beta
image: vabene1111/recipes:latest
env_file:
- stack.env
volumes:
- staticfiles:/opt/recipes/staticfiles
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
- nginx_config:/opt/recipes/nginx/conf.d
- ./mediafiles:/opt/recipes/mediafiles
depends_on:
@@ -116,7 +116,8 @@ services:
volumes:
nginx_config:
staticfiles:`
staticfiles:
```
-Download the .env template from [HERE](https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template) and load this file by pressing the "Load Variables from .env File" button:
![Screenshot of Add Stack screen](https://www.portainer.io/hubfs/image-png-Feb-21-2022-06-21-15-88-PM.png)

View File

@@ -8,7 +8,7 @@ downloaded and restored through the web interface.
When developing a new backup strategy, make sure to also test the restore process!
## Database
Please use any standard way of backing up your database. For most systems this can be achieved by using a dump
Please use any standard way of backing up your database. For most systems this can be achieved by using a dump
command that will create an SQL file with all the required data.
Please refer to your Database System documentation.
@@ -18,7 +18,7 @@ It is **neither** well tested nor documented so use at your own risk.
I would recommend using it only as a starting place for your own backup strategy.
## Mediafiles
The only Data this application stores apart from the database are the media files (e.g. images) used in your
The only Data this application stores apart from the database are the media files (e.g. images) used in your
recipes.
They can be found in the mediafiles mounted directory (depending on your installation).
@@ -56,3 +56,23 @@ You can now export recipes from Tandoor using the export function. This method r
Import:
Go to Import > from app > tandoor and select the zip file you want to import from.
## Backing up using the pgbackup container
You can add [pgbackup](https://hub.docker.com/r/prodrigestivill/postgres-backup-local) to manage the scheduling and automatic backup of your postgres database.
Modify the below to match your environment and add it to your `docker-compose.yml`
``` yaml
pgbackup:
container_name: pgbackup
environment:
BACKUP_KEEP_DAYS: "8"
BACKUP_KEEP_MONTHS: "6"
BACKUP_KEEP_WEEKS: "4"
POSTGRES_EXTRA_OPTS: -Z6 --schema=public --blobs
SCHEDULE: '@daily'
# Note: the tag must match the version of postgres you are using
image: prodrigestivill/postgres-backup-local:15
restart: unless-stopped
volumes:
- backups/postgres:/backups
```
You can manually initiate a backup by running `docker exec -it pgbackup ./backup.sh`

View File

@@ -0,0 +1,611 @@
This page describes all configuration options for the application
server. All settings must be configured in the environment of the
application server, usually by adding them to the `.env` file.
## Required Settings
The following settings need to be set appropriately for your installation.
They are included in the default `env.template`.
### Secret Key
Random secret key (at least 50 characters), use for example `base64 /dev/urandom | head -c50` to generate one.
It is used internally by django for various signing/cryptographic operations and **should be kept secret**.
See [Django Docs](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-SECRET_KEY)
```
SECRET_KEY=#$tp%v6*(*ba01wcz(ip(i5vfz8z$f%qdio&q@anr1#$=%(m4c
```
Alternatively you can point to a file containing just the secret key value. If using containers make sure the file is
persistent and available inside the container.
```
SECRET_KEY_FILE=/path/to/file.txt
// contents of file
#$tp%v6*(*ba01wcz(ip(i5vfz8z$f%qdio&q@anr1#$=%(m4c
```
### Database
Multiple parameters are required to configure the database.
| Var | Options | Description |
|-------------------|--------------------------------------------------------------------|-------------------------------------------------------------------------|
| DB_ENGINE | django.db.backends.postgresql (default) django.db.backends.sqlite3 | Type of database connection. Production should always use postgresql. |
| POSTGRES_HOST | any | Used to connect to database server. Use container name in docker setup. |
| POSTGRES_DB | any | Name of database. |
| POSTGRES_PORT | 1-65535 | Port of database, Postgresql default `5432` |
| POSTGRES_USER | any | Username for database connection. |
| POSTGRES_PASSWORD | any | Password for database connection. |
#### Password file
> default `None` - options: file path
Path to file containing the database password. Overrides `POSTGRES_PASSWORD`. Only applied when using Docker (or other
setups running `boot.sh`)
```
POSTGRES_PASSWORD_FILE=
```
#### Connection String
> default `None` - options: according to database specifications
Instead of configuring the connection using multiple individual environment parameters, you can use a connection string.
The connection string will override all other database settings.
```
DATABASE_URL = engine://username:password@host:port/dbname
```
#### Connection Options
> default `{}` - options: according to database specifications
Additional connection options can be set as shown in the example below.
```
DB_OPTIONS={"sslmode":"require"}
```
## Optional Settings
All optional settings are, as their name says, optional and can be ignored safely. If you want to know more about what
you can do with them take a look through this page. I recommend using the categories to guide yourself.
### Server configuration
Configuration options for serving related services.
#### Port
> default `8080` - options: `1-65535`
Port for gunicorn to bind to. Should not be changed if using docker stack with reverse proxy.
```
TANDOOR_PORT=8080
```
#### Allowed Hosts
> default `*` - options: `recipes.mydomain.com,cooking.mydomain.com,...` (comma seperated domain/ip list)
Security setting to prevent HTTP Host Header Attacks,
see [Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#allowed-hosts).
Many reverse proxies handle this and require the setting to be `*` (default).
```
ALLOWED_HOSTS=recipes.mydomain.com
```
#### URL Path
> default `None` - options: `/custom/url/base/path`
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
```
#### Static URL
> default `/static/` - options: `/any/url/path/`, `https://any.domain.name/and/url/path`
If staticfiles are stored or served from a different location uncomment and change accordingly.
This can either be a relative path from the applications base path or the url of an external host.
!!! info
- MUST END IN `/`
- This is not required if you are just using a subfolder
```
STATIC_URL=/static/
```
#### Media URL
> default `/static/` - options: `/any/url/path/`, `https://any.domain.name/and/url/path`
If mediafiles are stored at a different location uncomment and change accordingly.
This can either be a relative path from the applications base path or the url of an external host
!!! info
- MUST END IN `/`
- This is **not required** if you are just using a subfolder
- This is **not required** if using S3/object storage
```
MEDIA_URL=/media/
```
#### Gunicorn Workers
> default `3` - options `1-X`
Set the number of gunicorn workers to start when starting using `boot.sh` (all container installations).
The default is likely appropriate for most installations.
See [Gunicorn docs](https://docs.gunicorn.org/en/stable/design.html#how-many-workers) for recommended settings.
```
GUNICORN_WORKERS=3
```
#### Gunicorn Threads
> default `2` - options `1-X`
Set the number of gunicorn threads to start when starting using `boot.sh` (all container installations).
The default is likely appropriate for most installations.
See [Gunicorn docs](https://docs.gunicorn.org/en/stable/design.html#how-many-workers) for recommended settings.
```
GUNICORN_THREADS=2
```
#### Gunicorn Media
> default `0` - options `0`, `1`
Serve media files directly using gunicorn. Basically everyone recommends not doing this. Please use any of the examples
provided that include an additional nxginx container to handle media file serving.
If you know what you are doing turn this on (`1`) to serve media files using djangos serve() method.
```
GUNICORN_MEDIA=0
```
#### CSRF Trusted Origins
> default `[]` - options: [list,of,trusted,origins]
Allows setting origins to allow for unsafe requests.
See [Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#csrf-trusted-origins)
```
CSRF_TRUSTED_ORIGINS = []
```
#### Cors origins
> default `False` - options: `False`, `True`
By default, cross-origin resource sharing is disabled. Enabling this will allow access to your resources from other
domains.
Please read [the docs](https://github.com/adamchainz/django-cors-headers) carefully before enabling this.
```
CORS_ALLOW_ALL_ORIGINS = True
```
#### Session Cookies
Django session cookie settings. Can be changed to allow a single django application to authenticate several applications
when running under the same database.
```
SESSION_COOKIE_DOMAIN=.example.com
SESSION_COOKIE_NAME=sessionid # use this only to not interfere with non unified django applications under the same top level domain
```
### Features
Some features can be enabled/disabled on a server level because they might change the user experience significantly,
they might be unstable/beta or they have performance/security implications.
#### Captcha
If you allow signing up to your instance you might want to use a captcha to prevent spam.
Tandoor supports HCAPTCHA which is supposed to be a privacy-friendly captcha provider.
See [HCAPTCHA website](https://www.hcaptcha.com/) for more information and to acquire your sitekey and secret.
```
HCAPTCHA_SITEKEY=
HCAPTCHA_SECRET=
```
#### Metrics
Enable serving of prometheus metrics under the `/metrics` path
!!! danger
The view is not secured (as per the prometheus default way) so make sure to secure it
through your web server.
```
ENABLE_METRICS=0
```
#### Tree Sorting
> default `0` - options `0`, `1`
By default SORT_TREE_BY_NAME is disabled this will store all Keywords and Food in the order they are created.
Enabling this setting makes saving new keywords and foods very slow, which doesn't matter in most usecases.
However, when doing large imports of recipes that will create new objects, can increase total run time by 10-15x
Keywords and Food can be manually sorted by name in Admin
This value can also be temporarily changed in Admin, it will revert the next time the application is started
!!! info
Disabling tree sorting is a temporary fix, in the future we might find a better implementation to allow tree sorting
without the large performance impacts.
```
SORT_TREE_BY_NAME=0
```
#### PDF Export
> default `0` - options `0`, `1`
Exporting PDF's is a community contributed feature to export recipes as PDF files. This requires the server to download
a chromium binary and is generally implemented only rudimentary and somewhat slow depending on your server device.
See [Export feature docs](https://docs.tandoor.dev/features/import_export/#pdf) for additional information.
```
ENABLE_PDF_EXPORT=1
```
#### Legal URLS
Depending on your jurisdiction you might need to provide any of the following URLs for your instance.
```
TERMS_URL=
PRIVACY_URL=
IMPRINT_URL=
```
### Authentication
All configurable variables regarding authentication.
Please also visit the [dedicated docs page](https://docs.tandoor.dev/features/authentication/) for more information.
#### Default Permissions
Configures if a newly created user (from social auth or public signup) should automatically join into the given space and
default group.
This setting is targeted at private, single space instances that typically have a custom authentication system managing
access to the data.
!!! danger
With public signup enabled this will give everyone access to the data in the given space
!!! warning
This feature might be deprecated in favor of a space join and public viewing system in the future
> default `0` (disabled) - options `0`, `1-X` (space id)
When enabled will join user into space and apply group configured in `SOCIAL_DEFAULT_GROUP`.
```
SOCIAL_DEFAULT_ACCESS = 1
```
> default `guest` - options `guest`, `user`, `admin`
```
SOCIAL_DEFAULT_GROUP=guest
```
#### Enable Signup
> default `0` - options `0`, `1`
Allow everyone to create local accounts on your application instance (without an invite link)
You might want to setup HCAPTCHA to prevent bots from creating accounts/spam.
!!! info
Social accounts will always be able to sign up, if providers are configured
```
ENABLE_SIGNUP=0
```
#### Social Auth
Allows you to set up external OAuth providers.
```
SOCIAL_PROVIDERS = allauth.socialaccount.providers.github, allauth.socialaccount.providers.nextcloud,
```
#### Remote User Auth
> default `0` - options `0`, `1`
Allow authentication via the REMOTE-USER header (can be used for e.g. authelia).
!!! danger
Leave off if you don't know what you are doing! Enabling this without proper configuration will enable anybody
to login with any username!
```
REMOTE_USER_AUTH=0
```
#### LDAP
LDAP based authentication is disabled by default. You can enable it by setting `LDAP_AUTH` to `1` and configuring the
other
settings accordingly. Please remove/comment settings you do not need for your setup.
```
LDAP_AUTH=
AUTH_LDAP_SERVER_URI=
AUTH_LDAP_BIND_DN=
AUTH_LDAP_BIND_PASSWORD=
AUTH_LDAP_USER_SEARCH_BASE_DN=
AUTH_LDAP_TLS_CACERTFILE=
AUTH_LDAP_START_TLS=
```
### External Services
#### Email
Email Settings, see [Django docs](https://docs.djangoproject.com/en/3.2/ref/settings/#email-host) for additional
information.
Required for email confirmation and password reset (automatically activates if host is set).
```
EMAIL_HOST=
EMAIL_PORT=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
EMAIL_USE_TLS=0
EMAIL_USE_SSL=0
# email sender address (default 'webmaster@localhost')
DEFAULT_FROM_EMAIL=
```
Optional settings (only copy the ones you need)
```
# prefix used for account related emails (default "[Tandoor Recipes] ")
ACCOUNT_EMAIL_SUBJECT_PREFIX=
```
#### S3 Object storage
If you want to store your users media files using an external storage provider supporting the S3 API's (Like S3,
MinIO, ...)
configure the following settings accordingly.
As long as `S3_ACCESS_KEY` is not set, all object storage related settings are disabled.
See also [Django Storages Docs](https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html) for additional
information.
!!! info
Settings are only named S3 but apply to all compatible object storage providers.
Required settings
```
S3_ACCESS_KEY=
S3_SECRET_ACCESS_KEY=
S3_BUCKET_NAME=
```
Optional settings (only copy the ones you need)
```
S3_REGION_NAME= # default none, set your region might be required
S3_QUERYSTRING_AUTH=1 # default true, set to 0 to serve media from a public bucket without signed urls
S3_QUERYSTRING_EXPIRE=3600 # number of seconds querystring are valid for
S3_ENDPOINT_URL= # when using a custom endpoint like minio
S3_CUSTOM_DOMAIN= # when using a CDN/proxy to S3 (see https://github.com/TandoorRecipes/recipes/issues/1943)
```
#### FDC Api
The FDC Api is used to automatically load nutrition information from
the [FDC Nutrition Database](https://fdc.nal.usda.gov/fdc-app.html#/).
The default `DEMO_KEY` is limited to 30 requests / hour or 50 requests / day.
If you want to do many requests to the FDC API you need to get a (free) API
key [here](https://fdc.nal.usda.gov/api-key-signup.html).
```
FDC_API_KEY=DEMO_KEY
```
### Debugging/Development settings
!!! warning
These settings should not be left on in production as they might provide additional attack surfaces and
information to adversaries.
#### Debug
> default `0` - options: `0`, `1`
!!! info
Please enable this before posting logs anywhere to ask for help.
Setting to `1` enables several django debug features and additional
logs ([see docs](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-DEBUG)).
```
DEBUG=0
```
#### Debug Toolbar
> default `0` - options: `0`, `1`
Set to `1` to enable django debug toolbar middleware. Toolbar only shows if `DEBUG=1` is set and the requesting IP
is in `INTERNAL_IPS`.
See [Django Debug Toolbar Docs](https://django-debug-toolbar.readthedocs.io/en/latest/).
```
DEBUG_TOOLBAR=0
```
#### SQL Debug
> default `0` - options: `0`, `1`
Set to `1` to enable additional query output on the search page.
```
SQL_DEBUG=0
```
#### Gunicorn Log Level
> default `info` - options: [see Gunicorn Docs](https://docs.gunicorn.org/en/stable/settings.html#loglevel)
Increase or decrease the logging done by gunicorn (the python wsgi application).
```
GUNICORN_LOG_LEVEL="debug"
```
### Default User Preferences
Having default user preferences is nice so that users signing up to your instance already have the settings you deem
appropriate.
#### Fractions
> default `0` - options: `0`,`1`
The default value for the user preference 'fractions' (showing amounts as decimals or fractions).
```
FRACTION_PREF_DEFAULT=0
```
#### Comments
> default `1` - options: `0`,`1`
The default value for the user preference 'comments' (enable/disable commenting system)
```
COMMENT_PREF_DEFAULT=1
```
#### Sticky Navigation
> default `1` - options: `0`,`1`
The default value for the user preference 'sticky navigation' (always show navbar on top or hide when scrolling)
```
STICKY_NAV_PREF_DEFAULT=1
```
#### Max owned spaces
> default `100` - options: `0-X`
The default for the number of spaces a user can own. By setting to 0 space creation for users will be disabled.
Superusers can always bypass this limit.
```
MAX_OWNED_SPACES_PREF_DEFAULT=100
```
### Cosmetic / Preferences
#### Timezone
> default `Europe/Berlin` - options: [see timezone DB](https://timezonedb.com/time-zones)
Default timezone to use for database
connections ([see Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#time-zone)).
Usually everything is converted to the users timezone so this setting doesn't really need to be correct.
```
TZ=Europe/Berlin
```
#### Default Theme
> default `0` - options `1-X` (space ID)
Tandoors appearance can be changed on a user and space level but unauthenticated users always see the tandoor default style.
With this setting you can specify the ID of a space of which the appearance settings should be applied if a user is not logged in.
```
UNAUTHENTICATED_THEME_FROM_SPACE=
```
### Rate Limiting / Performance
#### Shopping auto sync
> default `5` - options: `1-XXX`
Users can set an amount of time after which the shopping list is automatically refreshed.
This is the minimum interval users can set. Setting this to a low value will allow users to automatically refresh very
frequently which
might cause high load on the server. (Technically they can obviously refresh as often as they want with their own
scripts)
```
SHOPPING_MIN_AUTOSYNC_INTERVAL=5
```
#### API Url Import throttle
> default `60/hour` - options: `x/hour`, `x/day`, `x/minute`, `x/second`
Limits how many recipes a user can import per hour.
A rate limit is recommended to prevent users from abusing your server for (DDoS) relay attacks and to prevent external
service
providers from blocking your server for too many request.
```
DRF_THROTTLE_RECIPE_URL_IMPORT=60/hour
```
#### Default Space Limits
You might want to limit how many resources a user might create. The following settings apply automatically to newly
created spaces. These defaults can be changed in the admin view after a space has been created.
If unset, all settings default to unlimited/enabled
```
SPACE_DEFAULT_MAX_RECIPES=0 # 0=unlimited recipes
SPACE_DEFAULT_MAX_USERS=0 # 0=unlimited users per space
SPACE_DEFAULT_MAX_FILES=0 # Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.
SPACE_DEFAULT_ALLOW_SHARING=1 # Allow users to share recipes with public links
```
#### Export file caching
> default `600` - options `1-X`
Recipe exports are cached for a certain time (in seconds) by default, adjust time if needed
```
EXPORT_FILE_CACHE_DURATION=600
```

View File

@@ -1,6 +1,6 @@
The Updating process depends on your chosen method of [installation](/install/docker)
While intermediate updates can be skipped when updating please make sure to
While intermediate updates can be skipped when updating please make sure to
**read the release notes** in case some special action is required to update.
## Docker
@@ -16,7 +16,79 @@ For all setups using Docker the updating process look something like this
For all setups using a manual installation updates usually involve downloading the latest source code from GitHub.
After that make sure to run:
1. `manage.py collectstatic`
2. `manage.py migrate`
1. `pip install -r requirements.txt`
2. `manage.py collectstatic`
3. `manage.py migrate`
4. `cd ./vue`
5. `yarn install`
6. `yarn build`
To apply all new migrations and collect new static files.
To install latest libraries, apply all new migrations and collect new static files.
## PostgreSQL
Postgres does not automatically upgrade database files when you change versions and requires manual intervention.
One option is to manually [backup/restore](https://docs.tandoor.dev/system/updating/#postgresql) the database.
A full list of options to upgrade a database provide in the [official PostgreSQL documentation](https://www.postgresql.org/docs/current/upgrading.html).
1. Collect information about your environment.
``` bash
grep -E 'POSTGRES|DATABASE' ~/.docker/compose/.env
docker ps -a --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}' | awk 'NR == 1 || /postgres/ || /recipes/'
```
2. Export the tandoor database
``` bash
docker exec -t {{database_container}} pg_dumpall -U {{djangouser}} > ~/tandoor.sql
```
3. Stop the postgres container
``` bash
docker stop {{database_container}} {{tandoor_container}}
```
4. Rename the tandoor volume
``` bash
sudo mv -R ~/.docker/compose/postgres ~/.docker/compose/postgres.old
```
5. Update image tag on postgres container.
``` yaml
db_recipes:
restart: always
image: postgres:16-alpine
volumes:
- ./postgresql:/var/lib/postgresql/data
env_file:
- ./.env
```
6. Pull and rebuild container.
``` bash
docker-compose pull && docker-compose up -d
```
7. Import the database export
``` bash
cat ~/tandoor.sql | sudo docker exec -i {{database_container}} psql postgres -U {{djangouser}}
```
8. Install postgres extensions
``` bash
docker exec -it {{database_container}} psql
```
then
``` psql
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
```
If anything fails, go back to the old postgres version and data directory and try again.
There are many articles and tools online that might provide a good starting point to help you upgrade [1](https://thomasbandt.com/postgres-docker-major-version-upgrade), [2](https://github.com/tianon/docker-postgres-upgrade), [3](https://github.com/vabene1111/DockerPostgresBackups).

View File

@@ -45,6 +45,7 @@ nav:
- Storages and Sync: features/external_recipes.md
- Import/Export: features/import_export.md
- System:
- Configuration: system/configuration.md
- Updating: system/updating.md
- Permission System: system/permissions.md
- Backup: system/backup.md

View File

@@ -44,7 +44,7 @@ INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(
',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
# allow djangos wsgi server to server mediafiles
GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', True)))
GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', False)))
if os.getenv('REVERSE_PROXY_AUTH') is not None:
print('DEPRECATION WARNING: Environment var "REVERSE_PROXY_AUTH" is deprecated. Please use "REMOTE_USER_AUTH".')
@@ -57,6 +57,8 @@ COMMENT_PREF_DEFAULT = bool(int(os.getenv('COMMENT_PREF_DEFAULT', True)))
FRACTION_PREF_DEFAULT = bool(int(os.getenv('FRACTION_PREF_DEFAULT', False)))
KJ_PREF_DEFAULT = bool(int(os.getenv('KJ_PREF_DEFAULT', False)))
STICKY_NAV_PREF_DEFAULT = bool(int(os.getenv('STICKY_NAV_PREF_DEFAULT', True)))
MAX_OWNED_SPACES_PREF_DEFAULT = int(os.getenv('MAX_OWNED_SPACES_PREF_DEFAULT', 100))
UNAUTHENTICATED_THEME_FROM_SPACE = int(os.getenv('UNAUTHENTICATED_THEME_FROM_SPACE', 0))
# minimum interval that users can set for automatic sync of shopping lists
SHOPPING_MIN_AUTOSYNC_INTERVAL = int(
@@ -69,7 +71,8 @@ if os.getenv('CSRF_TRUSTED_ORIGINS'):
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',')
if CORS_ORIGIN_ALLOW_ALL := os.getenv('CORS_ORIGIN_ALLOW_ALL') is not None:
print('DEPRECATION WARNING: Environment var "CORS_ORIGIN_ALLOW_ALL" is deprecated. Please use "CORS_ALLOW_ALL_ORIGINS."')
print(
'DEPRECATION WARNING: Environment var "CORS_ORIGIN_ALLOW_ALL" is deprecated. Please use "CORS_ALLOW_ALL_ORIGINS."')
CORS_ALLOW_ALL_ORIGINS = CORS_ORIGIN_ALLOW_ALL
else:
CORS_ALLOW_ALL_ORIGINS = bool(int(os.getenv("CORS_ALLOW_ALL_ORIGINS", True)))
@@ -89,13 +92,15 @@ DJANGO_TABLES2_PAGE_RANGE = 8
HCAPTCHA_SITEKEY = os.getenv('HCAPTCHA_SITEKEY', '')
HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
FDA_API_KEY = os.getenv('FDA_API_KEY', 'DEMO_KEY')
FDC_API_KEY = os.getenv('FDC_API_KEY', 'DEMO_KEY')
SHARING_ABUSE = bool(int(os.getenv('SHARING_ABUSE', False)))
SHARING_LIMIT = int(os.getenv('SHARING_LIMIT', 0))
ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm'
DRF_THROTTLE_RECIPE_URL_IMPORT = os.getenv('DRF_THROTTLE_RECIPE_URL_IMPORT', '60/hour')
TERMS_URL = os.getenv('TERMS_URL', '')
PRIVACY_URL = os.getenv('PRIVACY_URL', '')
IMPRINT_URL = os.getenv('IMPRINT_URL', '')
@@ -156,7 +161,8 @@ try:
INSTALLED_APPS.append(plugin_module)
plugin_config = {
'name': plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name,
'name': plugin_class.verbose_name if hasattr(plugin_class,
'verbose_name') else plugin_class.name,
'version': plugin_class.VERSION if hasattr(plugin_class, 'VERSION') else 'unknown',
'website': plugin_class.website if hasattr(plugin_class, 'website') else '',
'github': plugin_class.github if hasattr(plugin_class, 'github') else '',
@@ -164,7 +170,8 @@ try:
'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d),
'base_url': plugin_class.base_url,
'bundle_name': plugin_class.bundle_name if hasattr(plugin_class, 'bundle_name') else '',
'api_router_name': plugin_class.api_router_name if hasattr(plugin_class, 'api_router_name') else '',
'api_router_name': plugin_class.api_router_name if hasattr(plugin_class,
'api_router_name') else '',
'nav_main': plugin_class.nav_main if hasattr(plugin_class, 'nav_main') else '',
'nav_dropdown': plugin_class.nav_dropdown if hasattr(plugin_class, 'nav_dropdown') else '',
}
@@ -218,6 +225,7 @@ MIDDLEWARE = [
'django.middleware.locale.LocaleMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'cookbook.helper.scope_middleware.ScopeMiddleware',
'allauth.account.middleware.AccountMiddleware',
]
if DEBUG_TOOLBAR:
@@ -253,7 +261,8 @@ if LDAP_AUTH:
ldap.SCOPE_SUBTREE,
os.getenv('AUTH_LDAP_USER_SEARCH_FILTER_STR', '(uid=%(user)s)'),
)
AUTH_LDAP_USER_ATTR_MAP = ast.literal_eval(os.getenv('AUTH_LDAP_USER_ATTR_MAP')) if os.getenv('AUTH_LDAP_USER_ATTR_MAP') else {
AUTH_LDAP_USER_ATTR_MAP = ast.literal_eval(os.getenv('AUTH_LDAP_USER_ATTR_MAP')) if os.getenv(
'AUTH_LDAP_USER_ATTR_MAP') else {
'first_name': 'givenName',
'last_name': 'sn',
'email': 'mail',
@@ -350,7 +359,7 @@ WSGI_APPLICATION = 'recipes.wsgi.application'
# Load settings from env files
if os.getenv('DATABASE_URL'):
match = re.match(
r'(?P<schema>\w+):\/\/(?:(?P<user>[\w\d_-]+)(?::(?P<password>[^@]+))?@)?(?P<host>[^:/]+)(?:(?P<port>\d+))?(?:/(?P<database>[\w\d/._-]+))?',
r'(?P<schema>\w+):\/\/(?:(?P<user>[\w\d_-]+)(?::(?P<password>[^@]+))?@)?(?P<host>[^:/]+)(?::(?P<port>\d+))?(?:/(?P<database>[\w\d/._-]+))?',
os.getenv('DATABASE_URL')
)
settings = match.groupdict()
@@ -438,7 +447,7 @@ for p in PLUGINS:
if p['bundle_name'] != '':
WEBPACK_LOADER[p['bundle_name']] = {
'CACHE': not DEBUG,
'BUNDLE_DIR_NAME': f'vue/', # must end with slash
'BUNDLE_DIR_NAME': 'vue/', # must end with slash
'STATS_FILE': os.path.join(p["base_path"], 'vue', 'webpack-stats.json'),
'POLL_INTERVAL': 0.1,
'TIMEOUT': None,
@@ -450,7 +459,11 @@ for p in PLUGINS:
LANGUAGE_CODE = 'en'
TIME_ZONE = os.getenv('TIMEZONE') if os.getenv('TIMEZONE') else 'Europe/Berlin'
if os.getenv('TIMEZONE') is not None:
print('DEPRECATION WARNING: Environment var "TIMEZONE" is deprecated. Please use "TZ" instead.')
TIME_ZONE = os.getenv('TIMEZONE') if os.getenv('TIMEZONE') else 'Europe/Berlin'
else:
TIME_ZONE = os.getenv('TZ') if os.getenv('TZ') else 'Europe/Berlin'
USE_I18N = True

View File

@@ -1,5 +1,5 @@
Django==4.2.7
cryptography===41.0.4
cryptography===41.0.6
django-annoying==0.10.6
django-autocomplete-light==3.9.4
django-cleanup==8.0.0
@@ -9,7 +9,7 @@ django-tables2==2.5.3
djangorestframework==3.14.0
drf-writable-nested==0.7.0
django-oauth-toolkit==2.3.0
django-debug-toolbar==3.8.1
django-debug-toolbar==4.2.0
bleach==6.0.0
gunicorn==20.1.0
lxml==4.9.3
@@ -26,13 +26,13 @@ pyyaml==6.0.1
uritemplate==4.1.1
beautifulsoup4==4.12.2
microdata==0.8.0
Jinja2==3.1.2
Jinja2==3.1.3
django-webpack-loader==1.8.1
git+https://github.com/BITSOLVER/django-js-reverse@071e304fd600107bc64bbde6f2491f1fe049ec82
django-allauth==0.54.0
django-allauth==0.58.1
recipe-scrapers==14.52.0
django-scopes==2.0.0
pytest==7.3.1
pytest==7.4.3
pytest-django==4.6.0
django-treebeard==4.7
django-cors-headers==4.2.0

View File

@@ -13,12 +13,11 @@ tandoor_hash = ''
try:
print('getting tandoor version')
r = subprocess.check_output(['git', 'show', '-s'], cwd=BASE_DIR).decode()
tandoor_branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd=BASE_DIR).decode()
tandoor_branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd=BASE_DIR).decode().replace('\n', '')
tandoor_hash = r.split('\n')[0].split(' ')[1]
try:
tandoor_tag = subprocess.check_output(['git', 'describe', '--exact-match', tandoor_hash], cwd=BASE_DIR).decode().replace('\n', '')
except:
tandoor_tag = subprocess.check_output(['git', 'describe', '--exact-match', '--tags', tandoor_hash], cwd=BASE_DIR).decode().replace('\n', '')
except BaseException:
pass
version_info.append({
@@ -47,8 +46,9 @@ try:
commit_hash = r.split('\n')[0].split(' ')[1]
try:
print('running describe')
tag = subprocess.check_output(['git', 'describe', '--exact-match', commit_hash], cwd=os.path.join(BASE_DIR, 'recipes', 'plugins', d)).decode().replace('\n', '')
except:
tag = subprocess.check_output(['git', 'describe', '--exact-match', commit_hash],
cwd=os.path.join(BASE_DIR, 'recipes', 'plugins', d)).decode().replace('\n', '')
except BaseException:
tag = ''
version_info.append({
@@ -66,9 +66,11 @@ try:
traceback.print_exc()
except subprocess.CalledProcessError as e:
print("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output))
except:
except BaseException:
traceback.print_exc()
with open('cookbook/version_info.py', 'w+', encoding='UTF-8') as f:
print(f"writing version info {version_info}")
if not tandoor_tag:
tandoor_tag = tandoor_hash
f.write(f'TANDOOR_VERSION = "{tandoor_tag}"\nTANDOOR_REF = "{tandoor_hash}"\nVERSION_INFO = {version_info}')

View File

@@ -9,6 +9,11 @@
},
"dependencies": {
"@babel/eslint-parser": "^7.21.3",
"@codemirror/autocomplete": "^6.11.1",
"@codemirror/commands": "^6.3.2",
"@codemirror/lang-markdown": "^6.2.3",
"@codemirror/state": "^6.3.3",
"@codemirror/view": "^6.22.2",
"@popperjs/core": "^2.11.7",
"@vue/cli": "^5.0.8",
"@vue/composition-api": "1.7.1",

View File

@@ -669,8 +669,7 @@ export default {
if (url !== '') {
this.failed_imports.push(url)
}
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
throw "Load Recipe Error"
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_IMPORT, err)
})
},
/**
@@ -713,8 +712,7 @@ export default {
axios.post(resolveDjangoUrl('view_import'), formData, {headers: {'Content-Type': 'multipart/form-data'}}).then((response) => {
window.location.href = resolveDjangoUrl('view_import_response', response.data['import_id'])
}).catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE)
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_IMPORT, err)
})
},
/**

View File

@@ -363,20 +363,26 @@ export default {
}
},
mobileSimpleGrid() {
let grid = []
if (this.current_period !== null) {
for (const x of Array(7).keys()) {
let moment_date = moment(this.current_period.periodStart).add(x, "d")
grid.push({
date: moment_date,
create_default_date: moment_date.format("YYYY-MM-DD"), // improve meal plan edit modal to do formatting itself and accept dates
date_label: moment_date.format("dd") + " " + moment_date.format("ll"),
plan_entries: this.plan_items.filter((m) => moment_date.isBetween(moment(m.startDate), moment(m.endDate), 'day', '[]'))
})
}
let grid = [];
let currentDate = moment();
for (let x = 0; x < 7; x++) {
let moment_date = currentDate.clone().add(x, "d");
grid.push({
date: moment_date,
create_default_date: moment_date.format("YYYY-MM-DD"),
date_label: moment_date.format("dd") + " " + moment_date.format("ll"),
plan_entries: this.plan_items.filter(
(m) =>
moment_date.isBetween(
moment(m.startDate),
moment(m.endDate),
'day',
'[]'
)
),
});
}
return grid
return grid;
}
},
mounted() {
@@ -744,8 +750,12 @@ having to override as much.
.theme-default .cv-item.continued::before,
.theme-default .cv-item.toBeContinued::after {
/*
removed because it breaks a line and would increase item size https://github.com/TandoorRecipes/recipes/issues/2678
content: " \21e2 ";
color: #999;
*/
}
.theme-default .cv-item.toBeContinued {

View File

@@ -186,10 +186,10 @@ export default {
case "ingredient-editor": {
let url = resolveDjangoUrl("view_ingredient_editor")
if (this.this_model === this.Models.FOOD) {
window.location.href = url + '?food_id=' + e.source.id
window.open(url + '?food_id=' + e.source.id, "_blank");
}
if (this.this_model === this.Models.UNIT) {
window.location.href = url + '?unit_id=' + e.source.id
window.open(url + '?unit_id=' + e.source.id, "_blank");
}
break
}

View File

@@ -0,0 +1,227 @@
<template>
<div id="app">
<div>
<div class="row" v-if="recipe" style="max-height: 10vh">
<div class="col col-8">
<h2><a :href="resolveDjangoUrl('view_recipe', recipe.id)">{{ recipe.name }}</a></h2>
{{ recipe.description }}
<keywords-component :recipe="recipe"></keywords-component>
</div>
<div class="col col-4" v-if="recipe.image">
<img style="max-height: 10vh" class="img-thumbnail float-right" :src="recipe.image">
</div>
</div>
<div class="row mt-3">
<div class="col col-12">
<b-button variant="success" href="https://fdc.nal.usda.gov/index.html" target="_blank"><i class="fas fa-external-link-alt"></i> {{$t('FDC_Search')}}</b-button>
<table class="table table-sm table-bordered table-responsive mt-2 pb-5">
<thead>
<tr>
<td>{{ $t('Name') }}</td>
<td>FDC</td>
<td>{{ $t('Properties_Food_Amount') }}</td>
<td>{{ $t('Properties_Food_Unit') }}</td>
<td v-for="pt in property_types" v-bind:key="pt.id">
<b-button variant="primary" @click="editing_property_type = pt" class="btn-block">{{ pt.name }}
<span v-if="pt.unit !== ''">({{ pt.unit }}) </span> <br/>
<b-badge variant="light" ><i class="fas fa-sort-amount-down-alt"></i> {{ pt.order}}</b-badge>
<b-badge variant="success" v-if="pt.fdc_id > 0" class="mt-2" v-b-tooltip.hover :title="$t('property_type_fdc_hint')"><i class="fas fa-check"></i> FDC</b-badge>
<b-badge variant="warning" v-if="pt.fdc_id < 1" class="mt-2" v-b-tooltip.hover :title="$t('property_type_fdc_hint')"><i class="fas fa-times"></i> FDC</b-badge>
</b-button>
</td>
<td>
<b-button variant="success" @click="new_property_type = true"><i class="fas fa-plus"></i></b-button>
</td>
</tr>
</thead>
<tbody>
<tr v-for="f in this.foods" v-bind:key="f.id">
<td>
{{ f.name }}
</td>
<td style="width: 15em;">
<b-input-group>
<b-form-input v-model="f.fdc_id" type="number" @change="updateFood(f)" :disabled="f.loading"></b-form-input>
<b-input-group-append>
<b-button variant="success" @click="updateFoodFromFDC(f)" :disabled="f.loading"><i class="fas fa-sync-alt" :class="{'fa-spin': loading}"></i></b-button>
<b-button variant="info" :href="`https://fdc.nal.usda.gov/fdc-app.html#/food-details/${f.fdc_id}`" :disabled="f.fdc_id < 1" target="_blank"><i class="fas fa-external-link-alt"></i></b-button>
</b-input-group-append>
</b-input-group>
</td>
<td style="width: 5em; ">
<b-input v-model="f.properties_food_amount" type="number" @change="updateFood(f)" :disabled="f.loading"></b-input>
</td>
<td style="width: 11em;">
<generic-multiselect
@change="f.properties_food_unit = $event.val; updateFood(f)"
:initial_single_selection="f.properties_food_unit"
label="name" :model="Models.UNIT"
:multiple="false"
:disabled="f.loading"/>
</td>
<td v-for="p in f.properties" v-bind:key="`${f.id}_${p.property_type.id}`">
<b-input-group>
<b-form-input v-model="p.property_amount" type="number" :disabled="f.loading" v-b-tooltip.focus :title="p.property_type.name" @change="updateFood(f)"></b-form-input>
</b-input-group>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<generic-modal-form
:show="editing_property_type !== null"
:model="Models.PROPERTY_TYPE"
:action="Actions.UPDATE"
:item1="editing_property_type"
@finish-action="editing_property_type = null; loadData()">
</generic-modal-form>
<generic-modal-form
:show="new_property_type"
:model="Models.PROPERTY_TYPE"
:action="Actions.CREATE"
@finish-action="new_property_type = false; loadData()">
</generic-modal-form>
</div>
</div>
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import {ApiMixin, resolveDjangoUrl, StandardToasts} from "@/utils/utils";
import axios from "axios";
import BetaWarning from "@/components/BetaWarning.vue";
import {ApiApiFactory} from "@/utils/openapi/api";
import GenericMultiselect from "@/components/GenericMultiselect.vue";
import GenericModalForm from "@/components/Modals/GenericModalForm.vue";
import KeywordsComponent from "@/components/KeywordsComponent.vue";
Vue.use(BootstrapVue)
export default {
name: "PropertyEditorView",
mixins: [ApiMixin],
components: {KeywordsComponent, GenericModalForm, GenericMultiselect},
computed: {},
data() {
return {
recipe: null,
property_types: [],
editing_property_type: null,
new_property_type: false,
loading: false,
foods: [],
}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
this.loadData();
},
methods: {
resolveDjangoUrl,
loadData: function () {
let apiClient = new ApiApiFactory()
apiClient.listPropertyTypes().then(result => {
this.property_types = result.data
apiClient.retrieveRecipe(window.RECIPE_ID).then(result => {
this.recipe = result.data
this.foods = []
this.recipe.steps.forEach(s => {
s.ingredients.forEach(i => {
if (this.foods.filter(x => (x.id === i.food.id)).length === 0) {
this.foods.push(this.buildFood(i.food))
}
})
})
this.loading = false;
}).catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
}).catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
buildFood: function (food) {
/**
* Prepare food for display in grid by making sure the food properties are in the same order as property_types and that no types are missing
* */
let existing_properties = {}
food.properties.forEach(fp => {
existing_properties[fp.property_type.id] = fp
})
let food_properties = []
this.property_types.forEach(pt => {
let new_food_property = {
property_type: pt,
property_amount: 0,
}
if (pt.id in existing_properties) {
new_food_property = existing_properties[pt.id]
}
food_properties.push(new_food_property)
})
this.$set(food, 'loading', false)
food.properties = food_properties
return food
},
spliceInFood: function (food) {
/**
* replace food in foods list, for example after updates from the server
*/
this.foods = this.foods.map(f => (f.id === food.id) ? food : f)
},
updateFood: function (food) {
let apiClient = new ApiApiFactory()
apiClient.partialUpdateFood(food.id, food).then(result => {
this.spliceInFood(this.buildFood(result.data))
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
updateFoodFromFDC: function (food) {
food.loading = true;
let apiClient = new ApiApiFactory()
apiClient.fdcFood(food.id).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
this.spliceInFood(this.buildFood(result.data))
}).catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
food.loading = false;
})
}
},
}
</script>
<style>
</style>

View File

@@ -0,0 +1,22 @@
import Vue from 'vue'
import App from './PropertyEditorView.vue'
import i18n from '@/i18n'
import {createPinia, PiniaVuePlugin} from "pinia";
Vue.config.productionTip = false
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
let publicPath = localStorage.STATIC_URL + 'vue/'
if (process.env.NODE_ENV === 'development') {
publicPath = 'http://localhost:8080/'
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
pinia,
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -425,7 +425,6 @@
v-if="!ingredient.is_header">
<input
class="form-control"
style="height: 100%;"
v-model="ingredient.amount"
type="number"
step="any"
@@ -507,7 +506,6 @@
<input
class="form-control"
maxlength="256"
style="height: 100%;"
v-model="ingredient.note"
v-bind:placeholder="$t('Note')"
v-on:keydown.tab="
@@ -938,10 +936,7 @@ export default {
// set default visibility style for each component of the step
this.recipe.steps.forEach((s) => {
this.$set(s, "time_visible", s.time !== 0)
// ingredients_visible determines whether or not the ingredients UI is shown in the edit view
// show_ingredients_table determine whether the ingredients table is shown in the read view
// these are seperate as one might want to add ingredients but not want the step-level view
this.$set(s, "ingredients_visible", s.show_ingredients_table && (s.ingredients.length > 0 || this.recipe.steps.length === 1))
this.$set(s, "ingredients_visible", (s.ingredients.length > 0 || this.recipe.steps.length === 1))
this.$set(s, "instruction_visible", s.instruction !== "" || this.recipe.steps.length === 1)
this.$set(s, "step_recipe_visible", s.step_recipe !== null)
this.$set(s, "file_visible", s.file !== null)

View File

@@ -1,6 +1,6 @@
<template>
<div id="app" style="padding-bottom: 60px">
<RecipeSwitcher ref="ref_recipe_switcher"/>
<RecipeSwitcher ref="ref_recipe_switcher" />
<div class="row">
<div class="col-12 col-xl-10 col-lg-10 offset-xl-1 offset-lg-1">
<div class="row">
@@ -153,7 +153,7 @@
<div class="row" style="margin-top: 1vh">
<div class="col-12" style="text-align: right">
<b-button size="sm" variant="secondary" style="margin-right: 8px" @click="$root.$emit('bv::hide::popover')"
>{{ $t("Close") }}
>{{ $t("Close") }}
</b-button>
</div>
</div>
@@ -197,17 +197,17 @@
<span
class="text-sm-left text-warning"
v-if="ui.expert_mode && search.keywords_fields > 1 && hasDuplicateFilter(search.search_keywords, search.keywords_fields)"
>{{ $t("warning_duplicate_filter") }}</span
>{{ $t("warning_duplicate_filter") }}</span
>
<div class="row" v-if="ui.show_keywords">
<div class="col-12">
<b-input-group class="mt-2" v-for="(k, a) in keywordFields" :key="a">
<template #prepend v-if="ui.expert_mode">
<b-input-group-text style="width: 3em" @click="addField('keywords', k)">
<i class="fas fa-plus-circle text-primary" v-if="k == search.keywords_fields && k < 4"/>
<i class="fas fa-plus-circle text-primary" v-if="k == search.keywords_fields && k < 4" />
</b-input-group-text>
<b-input-group-text style="width: 3em" @click="removeField('keywords', k)">
<i class="fas fa-minus-circle text-primary" v-if="k == search.keywords_fields && k > 1"/>
<i class="fas fa-minus-circle text-primary" v-if="k == search.keywords_fields && k > 1" />
</b-input-group-text>
</template>
<generic-multiselect
@@ -258,17 +258,17 @@
<span
class="text-sm-left text-warning"
v-if="ui.expert_mode && search.foods_fields > 1 && hasDuplicateFilter(search.search_foods, search.foods_fields)"
>{{ $t("warning_duplicate_filter") }}</span
>{{ $t("warning_duplicate_filter") }}</span
>
<div class="row" v-if="ui.show_foods">
<div class="col-12">
<b-input-group class="mt-2" v-for="(f, i) in foodFields" :key="i">
<template #prepend v-if="ui.expert_mode">
<b-input-group-text style="width: 3em" @click="addField('foods', f)">
<i class="fas fa-plus-circle text-primary" v-if="f == search.foods_fields && f < 4"/>
<i class="fas fa-plus-circle text-primary" v-if="f == search.foods_fields && f < 4" />
</b-input-group-text>
<b-input-group-text style="width: 3em" @click="removeField('foods', f)">
<i class="fas fa-minus-circle text-primary" v-if="f == search.foods_fields && f > 1"/>
<i class="fas fa-minus-circle text-primary" v-if="f == search.foods_fields && f > 1" />
</b-input-group-text>
</template>
<generic-multiselect
@@ -314,17 +314,17 @@
<span
class="text-sm-left text-warning"
v-if="ui.expert_mode && search.books_fields > 1 && hasDuplicateFilter(search.search_books, search.books_fields)"
>{{ $t("warning_duplicate_filter") }}</span
>{{ $t("warning_duplicate_filter") }}</span
>
<div class="row" v-if="ui.show_books">
<div class="col-12">
<b-input-group class="mt-2" v-for="(b, i) in bookFields" :key="i">
<template #prepend v-if="ui.expert_mode">
<b-input-group-text style="width: 3em" @click="addField('books', b)">
<i class="fas fa-plus-circle text-primary" v-if="b == search.books_fields && b < 4"/>
<i class="fas fa-plus-circle text-primary" v-if="b == search.books_fields && b < 4" />
</b-input-group-text>
<b-input-group-text style="width: 3em" @click="removeField('books', b)">
<i class="fas fa-minus-circle text-primary" v-if="b == search.books_fields && b > 1"/>
<i class="fas fa-minus-circle text-primary" v-if="b == search.books_fields && b > 1" />
</b-input-group-text>
</template>
<generic-multiselect
@@ -558,16 +558,26 @@
<b-input-group-append v-if="ui.show_makenow">
<b-input-group-text>
{{ $t("make_now") }}
<b-form-checkbox v-model="search.makenow" name="check-button"
@change="refreshData(false)"
class="shadow-none" switch style="width: 4em"/>
<b-form-checkbox
v-model="search.makenow"
name="check-button"
@change="refreshData(false)"
class="shadow-none"
switch
style="width: 4em"
/>
</b-input-group-text>
<b-input-group-text>
<span>{{ $t("make_now_count") }}</span>
<b-form-input type="number" min="0" max="20" v-model="search.makenow_count"
@change="refreshData(false)"
size="sm" class="mt-1"></b-form-input>
<b-form-input
type="number"
min="0"
max="20"
v-model="search.makenow_count"
@change="refreshData(false)"
size="sm"
class="mt-1"
></b-form-input>
</b-input-group-text>
</b-input-group-append>
</b-input-group>
@@ -607,11 +617,11 @@
<!-- TODO find a way to localize this that works without explaining localization to each language translator -->
Show all recipes that are matched
<span v-if="search.search_input">
by <i>{{ search.search_input }}</i> <br/>
by <i>{{ search.search_input }}</i> <br />
</span>
<span v-else> without any search term <br/> </span>
<span v-else> without any search term <br /> </span>
<span v-if="search.search_internal"> and are <span class="text-success">internal</span> <br/></span>
<span v-if="search.search_internal"> and are <span class="text-success">internal</span> <br /></span>
<span v-for="k in search.search_keywords" v-bind:key="k.id">
<template v-if="k.items.length > 0">
@@ -620,7 +630,7 @@
contain
<b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">keywords</span>:
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
<br/>
<br />
</template>
</span>
@@ -631,7 +641,7 @@
contain
<b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">foods</span>:
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
<br/>
<br />
</template>
</span>
@@ -642,38 +652,38 @@
contain
<b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">books</span>:
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
<br/>
<br />
</template>
</span>
<span v-if="search.makenow"> and you can <span class="text-success">make right now</span> (based on the on hand flag) <br/></span>
<span v-if="search.makenow"> and you can <span class="text-success">make right now</span> (based on the on hand flag) <br /></span>
<span v-if="search.search_units.length > 0">
and contain <b v-if="search.search_units_or">any</b><b v-else>all</b> of the following <span class="text-success">units</span>:
<i>{{ search.search_units.flatMap((x) => x.name).join(", ") }}</i
><br/>
><br />
</span>
<span v-if="search.search_rating !== undefined">
and have a <span class="text-success">rating</span> <template v-if="search.search_rating_gte">greater than</template
><template v-else> less than</template> or equal to {{ search.search_rating }}<br/>
><template v-else> less than</template> or equal to {{ search.search_rating }}<br />
</span>
<span v-if="search.lastcooked !== undefined">
and have been <span class="text-success">last cooked</span> <template v-if="search.lastcooked_gte"> after</template
><template v-else> before</template> <i>{{ search.lastcooked }}</i
><br/>
><template v-else> before</template> <i>{{ search.lastcooked }}</i
><br />
</span>
<span v-if="search.timescooked !== undefined">
and have <span class="text-success">been cooked</span> <template v-if="search.timescooked_gte"> at least</template
><template v-else> less than</template> or equal to<i>{{ search.timescooked }}</i> times <br/>
><template v-else> less than</template> or equal to<i>{{ search.timescooked }}</i> times <br />
</span>
<span v-if="search.sort_order.length > 0">
<span class="text-success">order</span> by
<i>{{ search.sort_order.flatMap((x) => x.text).join(", ") }}</i>
<br/>
<br />
</span>
</div>
</div>
@@ -709,19 +719,19 @@
</b-dropdown>
<b-button variant="outline-primary" size="sm" class="shadow-none ml-1" @click="resetSearch()" v-if="searchFiltered()"
><i class="fas fa-file-alt"></i> {{ search.pagination_page }}/{{ Math.ceil(pagination_count / ui.page_size) }} {{ $t("Reset") }}
><i class="fas fa-file-alt"></i> {{ search.pagination_page }}/{{ Math.ceil(pagination_count / ui.page_size) }} {{ $t("Reset") }}
<i class="fas fa-times-circle"></i>
</b-button>
<b-button variant="outline-primary" size="sm" class="shadow-none ml-1" @click="openRandom()"
><i class="fas fa-dice-five"></i> {{ $t("Random") }}
><i class="fas fa-dice-five"></i> {{ $t("Random") }}
</b-button>
</div>
</div>
</div>
<template v-if="!searchFiltered() && ui.show_meal_plan && meal_plan_grid.length > 0">
<hr/>
<hr />
<div class="row">
<div class="col col-md-12">
<div
@@ -736,7 +746,7 @@
</div>
<div class="flex-grow-1 text-right">
<b-button class="hover-button btn-outline-primary btn-sm" @click="showMealPlanEditModal(null, day.create_default_date)"
><i class="fa fa-plus"></i
><i class="fa fa-plus"></i
></b-button>
</div>
</div>
@@ -744,8 +754,8 @@
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.id" class="hover-div p-0 pr-2">
<div class="d-flex flex-row align-items-center">
<div>
<img style="height: 50px; width: 50px; object-fit: cover" :src="plan.recipe.image" v-if="plan.recipe?.image"/>
<img style="height: 50px; width: 50px; object-fit: cover" :src="image_placeholder" v-else/>
<img style="height: 50px; width: 50px; object-fit: cover" :src="plan.recipe.image" v-if="plan.recipe?.image" />
<img style="height: 50px; width: 50px; object-fit: cover" :src="image_placeholder" v-else />
</div>
<div class="flex-grow-1 ml-2" style="text-overflow: ellipsis; overflow-wrap: anywhere">
<span class="two-row-text">
@@ -755,7 +765,7 @@
</div>
<div class="hover-button">
<b-button @click="showMealPlanEditModal(plan, null)" class="btn-outline-primary btn-sm"
><i class="fas fa-pencil-alt"></i
><i class="fas fa-pencil-alt"></i
></b-button>
</div>
</div>
@@ -765,7 +775,7 @@
</div>
</div>
</div>
<hr/>
<hr />
</template>
<div v-if="recipes.length > 0" class="mt-4">
@@ -853,24 +863,23 @@
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import VueCookies from "vue-cookies"
import { BootstrapVue } from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import Vue from "vue"
import VueCookies from "vue-cookies"
import moment from "moment"
import _debounce from "lodash/debounce"
import moment from "moment"
import Multiselect from "vue-multiselect"
import {ApiMixin, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils"
import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprecated?
import RecipeCard from "@/components/RecipeCard"
import GenericMultiselect from "@/components/GenericMultiselect"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import {ApiApiFactory} from "@/utils/openapi/api"
import {useMealPlanStore} from "@/stores/MealPlanStore"
import BottomNavigationBar from "@/components/BottomNavigationBar.vue"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import GenericMultiselect from "@/components/GenericMultiselect"
import MealPlanEditModal from "@/components/MealPlanEditModal.vue"
import RecipeCard from "@/components/RecipeCard"
import { useMealPlanStore } from "@/stores/MealPlanStore"
import { ApiApiFactory } from "@/utils/openapi/api"
import { ApiMixin, ResolveUrlMixin, StandardToasts, ToastMixin } from "@/utils/utils"
Vue.use(VueCookies)
Vue.use(BootstrapVue)
@@ -881,7 +890,7 @@ let UI_COOKIE_NAME = "ui_search_settings"
export default {
name: "RecipeSearchView",
mixins: [ResolveUrlMixin, ApiMixin, ToastMixin],
components: {GenericMultiselect, RecipeCard, RecipeSwitcher, Multiselect, BottomNavigationBar, MealPlanEditModal},
components: { GenericMultiselect, RecipeCard, RecipeSwitcher, Multiselect, BottomNavigationBar, MealPlanEditModal },
data() {
return {
// this.Models and this.Actions inherited from ApiMixin
@@ -898,22 +907,22 @@ export default {
search_input: "",
search_internal: false,
search_keywords: [
{items: [], operator: true, not: false},
{items: [], operator: false, not: false},
{items: [], operator: true, not: true},
{items: [], operator: false, not: true},
{ items: [], operator: true, not: false },
{ items: [], operator: false, not: false },
{ items: [], operator: true, not: true },
{ items: [], operator: false, not: true },
],
search_foods: [
{items: [], operator: true, not: false},
{items: [], operator: false, not: false},
{items: [], operator: true, not: true},
{items: [], operator: false, not: true},
{ items: [], operator: true, not: false },
{ items: [], operator: false, not: false },
{ items: [], operator: true, not: true },
{ items: [], operator: false, not: true },
],
search_books: [
{items: [], operator: true, not: false},
{items: [], operator: false, not: false},
{items: [], operator: true, not: true},
{items: [], operator: false, not: true},
{ items: [], operator: true, not: false },
{ items: [], operator: false, not: false },
{ items: [], operator: true, not: true },
{ items: [], operator: false, not: true },
],
search_units: [],
search_units_or: true,
@@ -986,7 +995,7 @@ export default {
date: moment_date,
create_default_date: moment_date.format("YYYY-MM-DD"), // improve meal plan edit modal to do formatting itself and accept dates
date_label: moment_date.format("dd") + " " + moment_date.format("ll"),
plan_entries: this.meal_plan_store.plan_list.filter((m) => moment_date.isBetween(moment(m.from_date), moment(m.to_date), 'day', '[]'))
plan_entries: this.meal_plan_store.plan_list.filter((m) => moment_date.isBetween(moment(m.from_date), moment(m.to_date), "day", "[]")),
})
}
}
@@ -1032,12 +1041,12 @@ export default {
}
return [
{id: 5, label: "⭐⭐⭐⭐⭐ " + label(5)},
{id: 4, label: "⭐⭐⭐⭐ " + label()},
{id: 3, label: "⭐⭐⭐ " + label()},
{id: 2, label: "⭐⭐ " + label()},
{id: 1, label: "⭐ " + label(1)},
{id: 0, label: this.$t("Unrated")},
{ id: 5, label: "⭐⭐⭐⭐⭐ " + label(5) },
{ id: 4, label: "⭐⭐⭐⭐ " + label() },
{ id: 3, label: "⭐⭐⭐ " + label() },
{ id: 2, label: "⭐⭐ " + label() },
{ id: 1, label: "⭐ " + label(1) },
{ id: 0, label: this.$t("Unrated") },
]
},
keywordFields: function () {
@@ -1063,7 +1072,7 @@ export default {
[this.$t("Name"), "name", "A-z", "Z-a"],
[this.$t("last_cooked"), "lastcooked", "↑", "↓"],
[this.$t("Rating"), "rating", "1-5", "5-1"],
[this.$t("times_cooked"), "favorite", "*-x", "x-*"],
[this.$t("times_cooked"), "favorite", "x-X", "X-x"],
[this.$t("date_created"), "created_at", "↑", "↓"],
[this.$t("date_viewed"), "lastviewed", "↑", "↓"],
]
@@ -1093,7 +1102,7 @@ export default {
},
},
mounted() {
this.recipes = Array(this.ui.page_size).fill({loading: true})
this.recipes = Array(this.ui.page_size).fill({ loading: true })
this.$nextTick(function () {
if (this.$cookies.isKey(UI_COOKIE_NAME)) {
@@ -1162,13 +1171,13 @@ export default {
"ui.expert_mode": function (newVal, oldVal) {
if (!newVal) {
this.search.search_keywords = this.search.search_keywords.map((x) => {
return {...x, not: false}
return { ...x, not: false }
})
this.search.search_foods = this.search.search_foods.map((x) => {
return {...x, not: false}
return { ...x, not: false }
})
this.search.search_books = this.search.search_books.map((x) => {
return {...x, not: false}
return { ...x, not: false }
})
}
},
@@ -1177,7 +1186,7 @@ export default {
// this.genericAPI inherited from ApiMixin
refreshData: _debounce(function (random) {
this.recipes_loading = true
this.recipes = Array(this.ui.page_size).fill({loading: true})
this.recipes = Array(this.ui.page_size).fill({ loading: true })
let params = this.buildParams(random)
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
window.scrollTo(0, 0)
@@ -1220,13 +1229,13 @@ export default {
},
resetSearch: function (filter = undefined) {
this.search.search_keywords = this.search.search_keywords.map((x) => {
return {...x, items: []}
return { ...x, items: [] }
})
this.search.search_foods = this.search.search_foods.map((x) => {
return {...x, items: []}
return { ...x, items: [] }
})
this.search.search_books = this.search.search_books.map((x) => {
return {...x, items: []}
return { ...x, items: [] }
})
this.search.search_input = filter?.query ?? ""
this.search.search_internal = filter?.internal ?? false
@@ -1289,7 +1298,7 @@ export default {
return
},
buildParams: function (random) {
let params = {options: {query: {}}, page: this.search.pagination_page, pageSize: this.ui.page_size}
let params = { options: { query: {} }, page: this.search.pagination_page, pageSize: this.ui.page_size }
if (this.search.search_filter) {
params.options.query.filter = this.search.search_filter.id
return params
@@ -1414,7 +1423,7 @@ export default {
;["page", "pageSize"].forEach((key) => {
delete search[key]
})
search = {...search, ...search.options.query}
search = { ...search, ...search.options.query }
console.log("after concat", search)
let params = {
name: filtername,

View File

@@ -28,7 +28,7 @@
</div>
</div>
<b-tabs content-class="mt-2" v-model="current_tab" class="mt-md-1" style="margin-top: 22px">
<b-tabs content-class="mt-2" v-model="current_tab" class="mt-md-1" style="margin-top: 22px;">
<!-- shopping list tab -->
<b-tab active>
<template #title>
@@ -37,12 +37,12 @@
<span
class="d-none d-md-inline-block">{{ $t('Shopping_list') + ` (${items.filter(x => x.checked === false).length})` }}</span>
</template>
<div class="container p-0 p-md-3" id="shoppinglist">
<div class="row">
<div class="container p-0 p-md-3 pb-5" id="shoppinglist">
<div class="row pb-5">
<div class="col col-md-12 p-0 p-lg-3">
<div role="tablist">
<!-- add to shopping form -->
<div class="container">
<div class="container d-lg-block d-print-none d-none">
<b-row class="justify-content-md-center align-items-center pl-1 pr-1"
v-if="entrymode">
<b-col cols="12" md="3" v-if="!ui.entry_mode_simple"
@@ -567,14 +567,25 @@
:modal_id="new_recipe.id" @finish="finishShopping" :list_recipe="new_recipe.list_recipe"/>
<bottom-navigation-bar active-view="view_shopping">
<template #custom_nav_content>
<div class="d-flex flex-row justify-content-around mb-3">
<b-input-group>
<b-form-input v-model="new_item.ingredient" :placeholder="$t('Food')"></b-form-input>
<b-input-group-append>
<b-button @click="addItem" variant="success">
<i class="fas fa-cart-plus "/>
</b-button>
</b-input-group-append>
</b-input-group>
</div>
</template>
<template #custom_create_functions>
<div class="dropdown-divider"></div>
<h6 class="dropdown-header">{{ $t('Shopping_list')}}</h6>
<a class="dropdown-item" @click="entrymode = !entrymode; " ><i class="fas fa-cart-plus"></i>
{{ $t("New_Entry") }}
</a>
<h6 class="dropdown-header">{{ $t('Shopping_list') }}</h6>
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')"
icon="far fa-file-pdf fa-fw"/>

View File

@@ -1,8 +1,8 @@
<template>
<div id="app">
<div class="row mt-2">
<div class="col col-12">
<b-row class="mt-2">
<b-col cols="12">
<div v-if="space !== undefined">
<h6><i class="fas fa-book"></i> {{ $t('Recipes') }}</h6>
<b-progress height="1.5rem" :max="space.max_recipes" variant="success" :striped="true">
@@ -32,13 +32,13 @@
</b-progress>
</div>
</div>
</div>
</b-col>
</b-row>
<div class="row mt-4">
<div class="col col-12">
<b-row class="mt-4">
<b-col cols="12">
<div v-if="user_spaces !== undefined">
<h4 class="mt-2"><i class="fas fa-users"></i> {{ $t('Users') }}</h4>
<h4><i class="fas fa-users"></i> {{ $t('Users') }}</h4>
<table class="table">
<thead>
<tr>
@@ -51,14 +51,14 @@
<td>{{ us.user.display_name }}</td>
<td>
<generic-multiselect
class="input-group-text m-0 p-0"
@change="us.groups = $event.val; updateUserSpace(us)"
label="name"
:initial_selection="us.groups"
:model="Models.GROUP"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:limit="10"
:multiple="true"
class="input-group-text m-0 p-0"
@change="us.groups = $event.val; updateUserSpace(us)"
label="name"
:initial_selection="us.groups"
:model="Models.GROUP"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:limit="10"
:multiple="true"
/>
</td>
<td>
@@ -67,12 +67,12 @@
</tr>
</table>
</div>
</div>
</div>
</b-col>
</b-row>
<div class="row mt-2">
<div class="col col-12">
<b-row class="mt-2">
<b-col cols="12">
<div v-if="invite_links !== undefined">
<h4 class="mt-2"><i class="fas fa-users"></i> {{ $t('Invites') }}</h4>
<table class="table">
@@ -90,14 +90,14 @@
<td>{{ il.email }}</td>
<td>
<generic-multiselect
class="input-group-text m-0 p-0"
@change="il.group = $event.val;"
label="name"
:initial_single_selection="il.group"
:model="Models.GROUP"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:limit="10"
:multiple="false"
class="input-group-text m-0 p-0"
@change="il.group = $event.val;"
label="name"
:initial_single_selection="il.group"
:model="Models.GROUP"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:limit="10"
:multiple="false"
/>
</td>
<td><input type="date" v-model="il.valid_until" class="form-control"></td>
@@ -131,46 +131,133 @@
</table>
<b-button variant="primary" @click="show_invite_create = true">{{ $t('Create') }}</b-button>
</div>
</div>
</div>
</b-col>
</b-row>
<div class="row mt-4" v-if="space !== undefined">
<div class="col col-12">
<h4 class="mt-2"><i class="fas fa-cogs"></i> {{ $t('Settings') }}</h4>
<b-row class="mt-4" v-if="space !== undefined">
<b-col cols="12">
<h4>{{ $t('Cosmetic') }}</h4>
<b-alert variant="warning" show><i class="fas fa-exclamation-triangle"></i> {{ $t('Space_Cosmetic_Settings') }}</b-alert>
<label>{{ $t('Message') }}</label>
<b-form-textarea v-model="space.message"></b-form-textarea>
<b-form-group :label="$t('Image')" :description="$t('CustomImageHelp')">
<generic-multiselect :initial_single_selection="space.image"
:model="Models.USERFILE"
:multiple="false"
@change="space.image = $event.val;"></generic-multiselect>
</b-form-group>
<label>{{ $t('Image') }}</label>
<generic-multiselect :initial_single_selection="space.image"
:model="Models.USERFILE"
:multiple="false"
@change="space.image = $event.val;"></generic-multiselect>
<br/>
<b-form-group :label="$t('Logo')" :description="$t('CustomNavLogoHelp')">
<generic-multiselect :initial_single_selection="space.nav_logo"
:model="Models.USERFILE"
:multiple="false"
@change="space.nav_logo = $event.val;"></generic-multiselect>
</b-form-group>
<b-form-checkbox v-model="space.show_facet_count"> Facet Count</b-form-checkbox>
<span class="text-muted small">{{ $t('facet_count_info') }}</span><br/>
<b-form-group :label="$t('Theme')">
<b-form-select v-model="space.space_theme">
<b-form-select-option value="BLANK">----</b-form-select-option>
<b-form-select-option value="TANDOOR">Tandoor</b-form-select-option>
<b-form-select-option value="TANDOOR_DARK">Tandoor Dark (Beta)</b-form-select-option>
<b-form-select-option value="BOOTSTRAP">Bootstrap</b-form-select-option>
<b-form-select-option value="DARKLY">Darkly</b-form-select-option>
<b-form-select-option value="FLATLY">Flatly</b-form-select-option>
<b-form-select-option value="SUPERHERO">Superhero</b-form-select-option>
</b-form-select>
</b-form-group>
<label>{{ $t('FoodInherit') }}</label>
<generic-multiselect :initial_selection="space.food_inherit"
:model="Models.FOOD_INHERIT_FIELDS"
@change="space.food_inherit = $event.val;">
</generic-multiselect>
<span class="text-muted small">{{ $t('food_inherit_info') }}</span><br/>
<a class="btn btn-success" @click="updateSpace()">{{ $t('Update') }}</a><br/>
<a class="btn btn-warning mt-1" @click="resetInheritance()">{{ $t('reset_food_inheritance') }}</a><br/>
<span class="text-muted small">{{ $t('reset_food_inheritance_info') }}</span>
</div>
</div>
<b-form-group :label="$t('CustomTheme')" :description="$t('CustomThemeHelp')">
<generic-multiselect :initial_single_selection="space.custom_space_theme"
:model="Models.USERFILE"
:multiple="false"
@change="space.custom_space_theme = $event.val;"></generic-multiselect>
<div class="row">
<div class="col-md-12">
</b-form-group>
<b-form-group :label="$t('Nav_Color')" :description="$t('Nav_Color_Help')">
<b-input-group>
<b-form-input type="color" v-model="space.nav_bg_color"></b-form-input>
<b-input-group-append>
<b-button @click="space.nav_bg_color = ''">{{ $t('Reset') }}</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<b-form-group :label="$t('Nav_Text_Mode')" :description="$t('Nav_Text_Mode_Help')">
<b-form-select v-model="space.nav_text_color">
<b-form-select-option value="BLANK">----</b-form-select-option>
<b-form-select-option value="LIGHT">Light</b-form-select-option>
<b-form-select-option value="DARK">Dark</b-form-select-option>
</b-form-select>
</b-form-group>
<h5>{{ $t('CustomLogos') }}</h5>
<p>{{$t('CustomLogoHelp')}} </p>
<b-form-group :label="$t('Logo')+' 32x32px'">
<generic-multiselect :initial_single_selection="space.logo_color_32"
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_32 = $event.val;"></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('Logo')+' 128x128px'">
<generic-multiselect :initial_single_selection="space.logo_color_128"
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_128 = $event.val;"></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('Logo')+' 144x144px'">
<generic-multiselect :initial_single_selection="space.logo_color_144"
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_144 = $event.val;"></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('Logo')+' 180x180px'">
<generic-multiselect :initial_single_selection="space.logo_color_180"
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_180 = $event.val;"></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('Logo')+' 192x192px'">
<generic-multiselect :initial_single_selection="space.logo_color_192"
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_192 = $event.val;"></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('Logo')+' 512x512px'">
<generic-multiselect :initial_single_selection="space.logo_color_512"
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_512 = $event.val;"></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('Logo')+' SVG'">
<generic-multiselect :initial_single_selection="space.logo_color_svg"
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_svg = $event.val;"></generic-multiselect>
</b-form-group>
<b-button variant="success" @click="updateSpace()">{{ $t('Update') }}</b-button>
</b-col>
</b-row>
<b-row class="mt-4" v-if="space !== undefined">
<b-col cols="12">
<h4><i class="fas fa-cogs"></i> {{ $t('Settings') }}</h4>
<b-form-group :label="$t('Message')">
<b-form-textarea v-model="space.message"></b-form-textarea>
</b-form-group>
<b-form-group :label="$t('FoodInherit')" :description="$t('food_inherit_info')">
<generic-multiselect :initial_selection="space.food_inherit"
:model="Models.FOOD_INHERIT_FIELDS"
@change="space.food_inherit = $event.val;">
</generic-multiselect>
</b-form-group>
<b-form-group :description="$t('reset_food_inheritance_info')">
<b-button-group class="mt-2">
<b-button variant="success" @click="updateSpace()">{{ $t('Update') }}</b-button>
<b-button variant="warning" @click="resetInheritance()">{{ $t('reset_food_inheritance') }}</b-button>
</b-button-group>
</b-form-group>
</b-col>
</b-row>
<b-row class="mt-4">
<b-col cols="12">
<h4>{{ $t('Open_Data_Import') }}</h4>
<open-data-import-component></open-data-import-component>
</div>
</b-col>
</div>
</b-row>
<div class="row mt-4">

View File

@@ -2,34 +2,8 @@
<div id="app">
<div>
<h2 v-if="recipe">{{ recipe.name}}</h2>
<markdown-editor-component></markdown-editor-component>
<table class="table table-sm table-bordered">
<thead>
<tr>
<td>{{ $t('Name') }}</td>
<td v-for="pt in property_types" v-bind:key="pt.id">{{ pt.name }}
<input type="text" v-model="pt.unit" @change="updatePropertyType(pt)">
<input v-model="pt.fdc_id" type="number" placeholder="FDC ID" @change="updatePropertyType(pt)"></td>
</tr>
</thead>
<tbody>
<tr v-for="f in this.foods" v-bind:key="f.food.id">
<td>
{{ f.food.name }}
{{ $t('Property') }} / <input type="number" v-model="f.food.properties_food_amount" @change="updateFood(f.food)">
<generic-multiselect
@change="f.food.properties_food_unit = $event.val; updateFood(f.food)"
:initial_selection="f.food.properties_food_unit"
label="name" :model="Models.UNIT"
:multiple="false"/>
<input v-model="f.food.fdc_id" placeholder="FDC ID">
<button>Load FDC</button>
</td>
<td v-for="p in f.properties" v-bind:key="`${f.id}_${p.property_type.id}`"><input type="number" v-model="p.property_amount"> {{ p.property_type.unit }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
@@ -45,6 +19,9 @@ import axios from "axios";
import BetaWarning from "@/components/BetaWarning.vue";
import {ApiApiFactory} from "@/utils/openapi/api";
import GenericMultiselect from "@/components/GenericMultiselect.vue";
import GenericModalForm from "@/components/Modals/GenericModalForm.vue";
import {Models} from "@/utils/models";
import MarkdownEditorComponent from "@/components/MarkdownEditorComponent.vue";
Vue.use(BootstrapVue)
@@ -53,65 +30,19 @@ Vue.use(BootstrapVue)
export default {
name: "TestView",
mixins: [ApiMixin],
components: {GenericMultiselect},
computed: {
foods: function () {
let foods = []
if (this.recipe !== null && this.property_types !== []) {
this.recipe.steps.forEach(s => {
s.ingredients.forEach(i => {
let food = {food: i.food, properties: {}}
this.property_types.forEach(pt => {
food.properties[pt.id] = {changed: false, property_amount: 0, property_type: pt}
})
i.food.properties.forEach(fp => {
food.properties[fp.property_type.id] = {changed: false, property_amount: fp.property_amount, property_type: fp.property_type}
})
foods.push(food)
})
})
}
return foods
}
},
components: {MarkdownEditorComponent},
computed: {},
data() {
return {
recipe: null,
property_types: []
}
return {}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
let apiClient = new ApiApiFactory()
apiClient.retrieveRecipe("112").then(result => {
this.recipe = result.data
})
apiClient.listPropertyTypes().then(result => {
this.property_types = result.data
})
},
methods: {
updateFood: function (food) {
let apiClient = new ApiApiFactory()
apiClient.partialUpdateFood(food.id, food).then(result => {
//TODO handle properly
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
updatePropertyType: function (pt) {
let apiClient = new ApiApiFactory()
apiClient.partialUpdatePropertyType(pt.id, pt).then(result => {
//TODO handle properly
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
refreshData: function () {
}
},
}

View File

@@ -227,11 +227,18 @@ export default {
async autoPlanThread(autoPlan, mealTypeIndex) {
let apiClient = new ApiApiFactory()
let keyword_ids = []
for (const index in autoPlan.keywords[mealTypeIndex]){
let keyword = autoPlan.keywords[mealTypeIndex][index]
keyword_ids.push(keyword.id)
}
let data = {
"start_date": moment(autoPlan.startDay).format("YYYY-MM-DD"),
"end_date": moment(autoPlan.endDay).format("YYYY-MM-DD"),
"meal_type_id": autoPlan.meal_types[mealTypeIndex].id,
"keywords": autoPlan.keywords[mealTypeIndex],
"keyword_ids": keyword_ids,
"servings": autoPlan.servings,
"shared": autoPlan.shared,
"addshopping": autoPlan.addshopping

View File

@@ -1,6 +1,12 @@
<template>
<!-- bottom button nav -->
<div class="fixed-bottom p-1 pt-2 pl-2 pr-2 border-top text-center d-lg-none d-print-none bottom-action-bar bg-white">
<slot name="custom_nav_content">
</slot>
<div class="d-flex flex-row justify-content-around">
<div class="flex-column" v-if="show_button_1">
<slot name="button_1">

View File

@@ -1,6 +1,6 @@
<template>
<div style="cursor:pointer">
<a v-if="!button" class="dropdown-item" @click="clipboard"><i :class="icon"></i> {{ label }}</a>
<a v-if="!button" class="dropdown-item" @click="clipboard" href="#"><i :class="icon"></i> {{ label }}</a>
<b-button v-if="button" @click="clipboard">{{ label }}</b-button>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<div style="cursor:pointer">
<a v-if="!button" class="dropdown-item" @click="downloadFile"><i :class="icon"></i> {{ label }}</a>
<a v-if="!button" class="dropdown-item" @click="downloadFile" href="#"><i :class="icon"></i> {{ label }}</a>
<b-button v-if="button" @click="downloadFile">{{ label }}</b-button>
</div>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<div style="cursor:pointer">
<a v-if="!button" class="dropdown-item" @click="downloadFile"><i :class="icon"></i> {{ label }}</a>
<b-button v-if="button" @click="downloadFile">{{ label }}</b-button>
<div style="cursor:pointer;">
<a v-if="!button" class="dropdown-item" @click="downloadFile" href="#"><i :class="icon"></i> {{ label }}</a>
<b-button class="dropdown-item" v-if="button" @click="downloadFile">{{ label }}</b-button>
</div>
</template>

View File

@@ -1,10 +1,9 @@
<template>
<div v-if="recipes !== {}">
<div id="switcher" class="align-center">
<i class="btn btn-primary fas fa-receipt fa-xl fa-fw shadow-none btn-circle" v-b-toggle.related-recipes/>
<i class="btn btn-primary fas fa-receipt fa-xl fa-fw shadow-none btn-circle" v-b-toggle.related-recipes />
</div>
<b-sidebar id="related-recipes" backdrop right bottom no-header shadow="sm" style="z-index: 10000"
@shown="updatePinnedRecipes()">
<b-sidebar id="related-recipes" backdrop right bottom no-header shadow="sm" style="z-index: 10000" @shown="updatePinnedRecipes()">
<template #default="{ hide }">
<div class="d-flex flex-column justify-content-end h-100 p-3 align-items-end">
<h5>{{ $t("Planned") }} <i class="fas fa-calendar fa-fw"></i></h5>
@@ -19,7 +18,7 @@
hide()
"
href="javascript:void(0);"
>{{ r.name }}</a
>{{ r.name }}</a
>
</div>
</div>
@@ -36,8 +35,7 @@
<div v-for="r in pinned_recipes" :key="`pin${r.id}`">
<b-row class="pb-1 pt-1">
<b-col cols="2">
<a href="javascript:void(0)" @click="unPinRecipe(r)" class="text-muted"><i
class="fas fa-times"></i></a>
<a href="javascript:void(0)" @click="unPinRecipe(r)" class="text-muted"><i class="fas fa-times"></i></a>
</b-col>
<b-col cols="10">
<a
@@ -47,7 +45,7 @@
"
href="javascript:void(0);"
class="align-self-end"
>{{ r.name }}
>{{ r.name }}
</a>
</b-col>
</b-row>
@@ -69,7 +67,7 @@
hide()
"
href="javascript:void(0);"
>{{ r.name }}</a
>{{ r.name }}</a
>
</div>
</div>
@@ -88,14 +86,14 @@
</template>
<script>
const {ApiApiFactory} = require("@/utils/openapi/api")
import {ResolveUrlMixin} from "@/utils/utils"
const { ApiApiFactory } = require("@/utils/openapi/api")
import { ResolveUrlMixin } from "@/utils/utils"
export default {
name: "RecipeSwitcher",
mixins: [ResolveUrlMixin],
props: {
recipe: {type: Number, default: undefined},
recipe: { type: Number, default: undefined },
},
data() {
return {
@@ -160,14 +158,16 @@ export default {
// get related recipes and save them for later
if (this.$parent.recipe) {
this.related_recipes = [this.$parent.recipe]
return apiClient.relatedRecipe(this.$parent.recipe.id, {
query: {
levels: 2,
format: "json"
}
}).then((result) => {
this.related_recipes = this.related_recipes.concat(result.data)
})
return apiClient
.relatedRecipe(this.$parent.recipe.id, {
query: {
levels: 2,
format: "json",
},
})
.then((result) => {
this.related_recipes = this.related_recipes.concat(result.data)
})
}
},
loadPinnedRecipes: function () {
@@ -179,16 +179,16 @@ export default {
// TODO move to utility function moment is in maintenance mode https://momentjs.com/docs/
var tzoffset = new Date().getTimezoneOffset() * 60000 //offset in milliseconds
let today = new Date(Date.now() - tzoffset).toISOString().split("T")[0]
return apiClient.listMealPlans({query: {from_date: today, to_date: today}}).then((result) => {
return apiClient.listMealPlans(today, today).then((result) => {
let promises = []
result.data.forEach((mealplan) => {
this.planned_recipes.push({...mealplan?.recipe, servings: mealplan?.servings})
this.planned_recipes.push({ ...mealplan?.recipe, servings: mealplan?.servings })
const serving_factor = (mealplan?.servings ?? mealplan?.recipe?.servings ?? 1) / (mealplan?.recipe?.servings ?? 1)
promises.push(
apiClient.relatedRecipe(mealplan?.recipe?.id, {query: {levels: 2}}).then((r) => {
apiClient.relatedRecipe(mealplan?.recipe?.id, { query: { levels: 2 } }).then((r) => {
// scale all recipes to mealplan servings
r.data = r.data.map((x) => {
return {...x, factor: serving_factor}
return { ...x, factor: serving_factor }
})
this.planned_recipes = [...this.planned_recipes, ...r.data]
})
@@ -220,7 +220,6 @@ export default {
z-index: 9000;
}
@media (max-width: 991.98px) {
#switcher .btn-circle {
position: fixed;

View File

@@ -33,11 +33,11 @@
<h5><i class="fas fa-database"></i> {{ $t('Properties') }}</h5>
<b-form-group :label="$t('Properties Food Amount')" description=""> <!-- TODO localize -->
<b-form-group :label="$t('Properties_Food_Amount')" description="">
<b-form-input v-model="food.properties_food_amount"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Properties Food Unit')" description=""> <!-- TODO localize -->
<b-form-group :label="$t('Properties_Food_Unit')" description="">
<generic-multiselect
@change="food.properties_food_unit = $event.val;"
:model="Models.UNIT"
@@ -204,6 +204,12 @@
}}
</b-form-checkbox>
</b-form-group>
<b-form-group :description="$t('substitute_children_help')">
<b-form-checkbox v-model="food.substitute_children">{{
$t('substitute_children')
}}
</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('InheritFields')" :description="$t('InheritFields_help')">
<generic-multiselect

View File

@@ -20,6 +20,7 @@
@input="selectionChanged"
@tag="addNew"
@open="selectOpened()"
:disabled="disabled"
>
</multiselect>
</template>
@@ -74,6 +75,7 @@ export default {
allow_create: { type: Boolean, default: false },
create_placeholder: { type: String, default: "You Forgot to Add a Tag Placeholder" },
clear: { type: Number },
disabled: {type: Boolean, default: false, },
},
watch: {
initial_selection: function (newVal, oldVal) {

View File

@@ -0,0 +1,106 @@
<template>
<div>
<h1>EDITOR</h1>
<div id="editor" style="" class="bg-info">
</div>
</div>
</template>
<script>
import {EditorState} from "@codemirror/state"
import {keymap, EditorView, MatchDecorator, Decoration, WidgetType, ViewPlugin} from "@codemirror/view"
import {defaultKeymap} from "@codemirror/commands"
import {markdown} from "@codemirror/lang-markdown"
import {autocompletion} from "@codemirror/autocomplete"
class PlaceholderWidget extends WidgetType { //TODO this is not working for some javascript magic reason
name = undefined
constructor(name) {
console.log(name)
super()
}
eq(other) {
return this.name == other.name
}
toDOM() {
let elt = document.createElement("span")
elt.style.cssText = `
border: 1px solid blue;
border-radius: 4px;
padding: 0 3px;
background: lightblue;`
elt.textContent = "Food"
return elt
}
ignoreEvent() {
return false
}
}
export default {
name: "MarkdownEditorComponent",
props: {},
computed: {},
mounted() {
const placeholderMatcher = new MatchDecorator({
regexp: /\{\{\singredients\[\d\]\s\}\}/g,
decoration: match => Decoration.replace({
widget: new PlaceholderWidget(match[0]),
})
})
const placeholders = ViewPlugin.fromClass(class {
placeholders
constructor(view) {
this.placeholders = placeholderMatcher.createDeco(view)
}
update(update) {
this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders)
}
}, {
decorations: instance => instance.placeholders,
provide: plugin => EditorView.atomicRanges.of(view => {
return view.plugin(plugin)?.placeholders || Decoration.none
})
})
let startState = EditorState.create({
doc: "Das ist eine Beschreibung \nPacke {{ ingredients[1] }} in das Fass mit {{ ingredients[3] }}\nTest Bla Bla",
extensions: [keymap.of(defaultKeymap), placeholders, markdown(), autocompletion({override: [this.foodTemplateAutoComplete]})]
})
let view = new EditorView({
state: startState,
extensions: [],
parent: document.getElementById("editor")
})
},
methods: {
foodTemplateAutoComplete: function (context) {
let word = context.matchBefore(/\w*/)
if (word.from == word.to && !context.explicit)
return null
return {
from: word.from,
options: [
{label: "Mehl", type: "text", apply: "{{ ingredients[1] }}", detail: "template"},
{label: "Butter", type: "text", apply: "{{ ingredients[2] }}", detail: "template"},
{label: "Salz", type: "text", apply: "{{ ingredients[3] }}", detail: "template"},
]
}
}
},
}
</script>

View File

@@ -27,7 +27,7 @@
<b-form-input type="date" v-model="entryEditing.from_date"></b-form-input>
<b-input-group-append>
<b-button variant="secondary" @click="entryEditing.from_date = changeDate(entryEditing.from_date, -1)"><i class="fas fa-minus"></i></b-button>
<b-button variant="primary" @click="entryEditing.from_date = changeDate(entryEditing.from_date, 1)"><i class="fas fa-plus"></i></b-button>
<b-button variant="primary" @click="entryEditing.from_date = changeDate(entryEditing.from_date, 1)"><i class="fas fa-plus"></i></b-button>
</b-input-group-append>
</b-input-group>
@@ -38,7 +38,7 @@
<b-form-input type="date" v-model="entryEditing.to_date"></b-form-input>
<b-input-group-append>
<b-button variant="secondary" @click="entryEditing.to_date = changeDate(entryEditing.to_date, -1)"><i class="fas fa-minus"></i></b-button>
<b-button variant="primary" @click="entryEditing.to_date = changeDate(entryEditing.to_date, 1)"><i class="fas fa-plus"></i></b-button>
<b-button variant="primary" @click="entryEditing.to_date = changeDate(entryEditing.to_date, 1)"><i class="fas fa-plus"></i></b-button>
</b-input-group-append>
</b-input-group>
<small tabindex="-1" class="form-text text-muted">{{ $t("EndDate") }}</small>
@@ -209,8 +209,14 @@ export default {
},
deep: true,
},
entryEditing: {
handler(newVal) {
'entryEditing.from_date': {
handler(newVal, oldVal) {
if (newVal !== undefined && oldVal !== undefined) {
if (newVal !== oldVal && newVal !== this.entryEditing.to_date) {
let change = Math.abs(moment(oldVal).diff(moment(this.entryEditing.to_date), 'days')) // even though negative numbers might be correct, they would be illogical as to needs to always be larger than from
this.entryEditing.to_date = moment(newVal).add(change, 'd').format("YYYY-MM-DD")
}
}
},
deep: true,
},
@@ -312,7 +318,7 @@ export default {
this.entryEditing.servings = 1
}
},
changeDate(date, change){
changeDate(date, change) {
return moment(date).add(change, 'd').format("YYYY-MM-DD")
}
},

View File

@@ -25,6 +25,12 @@ export default {
},
mounted() {
this.new_value = this.value
if (this.new_value === "") { // if the selection is empty but the options are of type number, set to 0 instead of ""
if (typeof this.options[0]['value'] === 'number') {
this.new_value = 0
}
}
},
watch: {
new_value: function () {

View File

@@ -14,7 +14,7 @@ export default {
props: {
field: { type: String, default: "You Forgot To Set Field Name" },
label: { type: String, default: "Text Field" },
value: { type: String, default: "" },
value: { type: Number, default: 0 },
placeholder: { type: Number, default: 0 },
help: { type: String, default: undefined },
subtitle: { type: String, default: undefined },

View File

@@ -24,7 +24,7 @@
<table class="table table-bordered table-sm">
<tr >
<tr>
<td style="border-top: none"></td>
<td class="text-right" style="border-top: none">{{ $t('per_serving') }}</td>
<td class="text-right" style="border-top: none">{{ $t('total') }}</td>
@@ -41,14 +41,18 @@
<td class="align-middle text-center" v-if="!show_recipe_properties">
<a href="#" @click="selected_property = p">
<i v-if="p.missing_value" class="text-warning fas fa-exclamation-triangle"></i>
<i v-if="!p.missing_value" class="text-muted fas fa-info-circle"></i>
<!-- <i v-if="p.missing_value" class="text-warning fas fa-exclamation-triangle"></i>-->
<!-- <i v-if="!p.missing_value" class="text-muted fas fa-info-circle"></i>-->
<i class="text-muted fas fa-info-circle"></i>
<!-- TODO find solution for missing values as 0 can either be missing or actually correct for any given property -->
</a>
</td>
</tr>
</table>
<div class="text-center">
<b-button variant="success" :href="resolveDjangoUrl('view_property_editor', recipe.id)"><i class="fas fa-table"></i> {{ $t('Property_Editor') }}</b-button>
</div>
</div>
@@ -79,7 +83,7 @@
</template>
<script>
import {ApiMixin, roundDecimals, StandardToasts} from "@/utils/utils";
import {ApiMixin, resolveDjangoUrl, roundDecimals, StandardToasts} from "@/utils/utils";
import GenericModalForm from "@/components/Modals/GenericModalForm.vue";
import {ApiApiFactory} from "@/utils/openapi/api";
@@ -153,11 +157,11 @@ export default {
}
}
function compare(a,b){
if(a.type.order > b.type.order){
function compare(a, b) {
if (a.type.order > b.type.order) {
return 1
}
if(a.type.order < b.type.order){
if (a.type.order < b.type.order) {
return -1
}
return 0
@@ -172,6 +176,7 @@ export default {
}
},
methods: {
resolveDjangoUrl,
roundDecimals,
openFoodEditModal: function (food) {
console.log(food)

View File

@@ -250,6 +250,10 @@ export default {
opacity: 1;
}
.content:hover .card-img-overlay {
opacity: 0;
}
.content-details {
position: absolute;
text-align: center;

View File

@@ -10,6 +10,9 @@
<a class="dropdown-item" :href="resolveDjangoUrl('edit_recipe', recipe.id)" v-if="!disabled_options.edit"><i
class="fas fa-pencil-alt fa-fw"></i> {{ $t("Edit") }}</a>
<a class="dropdown-item" :href="resolveDjangoUrl('view_property_editor', recipe.id)" v-if="!disabled_options.edit">
<i class="fas fa-table"></i> {{ $t("Property_Editor") }}</a>
<a class="dropdown-item" :href="resolveDjangoUrl('edit_convert_recipe', recipe.id)"
v-if="!recipe.internal && !disabled_options.convert"><i class="fas fa-exchange-alt fa-fw"></i> {{ $t("convert_internal") }}</a>
@@ -209,6 +212,7 @@ export default {
this.entryEditing = this.options.entryEditing
this.entryEditing.recipe = this.recipe
this.entryEditing.from_date = moment(new Date()).format("YYYY-MM-DD")
this.entryEditing.to_date = moment(new Date()).format("YYYY-MM-DD")
this.$nextTick(function () {
this.$bvModal.show(`modal-meal-plan_${this.modal_id}`)
})
@@ -259,9 +263,11 @@ export default {
},
}
})
if (recipe.nutrition !== null) {
delete recipe.nutrition.id
}
recipe.properties = recipe.properties.map(p => {
return { ...p, ...{ id: undefined, } }
})
apiClient
.createRecipe(recipe)
.then((new_recipe) => {

View File

@@ -277,9 +277,7 @@ export default {
}
},
handleResize: function () {
if (document.getElementById('nutrition_container') !== null) {
this.ingredient_height = document.getElementById('ingredient_container').clientHeight - document.getElementById('nutrition_container').clientHeight
} else {
if (document.getElementById('ingredient_container') !== null) {
this.ingredient_height = document.getElementById('ingredient_container').clientHeight
}
},

View File

@@ -49,34 +49,47 @@
</b-form-select>
</b-form-group>
<b-alert variant="warning" show><i class="fas fa-exclamation-triangle"></i> {{ $t('Space_Cosmetic_Settings') }}</b-alert>
<b-form-group :label="$t('Theme')">
<b-form-select v-model="user_preferences.theme" @change="updateSettings(true);">
<b-form-select-option value="TANDOOR">Tandoor</b-form-select-option>
<b-form-select-option value="TANDOOR_DARK">Tandoor Dark (Beta)</b-form-select-option>
<b-form-select-option value="BOOTSTRAP">Bootstrap</b-form-select-option>
<b-form-select-option value="DARKLY">Darkly</b-form-select-option>
<b-form-select-option value="FLATLY">Flatly</b-form-select-option>
<b-form-select-option value="SUPERHERO">Superhero</b-form-select-option>
<b-form-select-option value="TANDOOR_DARK">Tandoor Dark (INCOMPLETE)</b-form-select-option>
</b-form-select>
</b-form-group>
<b-form-group :description="$t('Sticky_Nav_Help')">
<b-form-checkbox v-model="user_preferences.sticky_navbar" @change="updateSettings(true);">
{{ $t('Sticky_Nav') }}
</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('Nav_Color')" :description="$t('Nav_Color_Help')">
<b-form-select v-model="user_preferences.nav_color" @change="updateSettings(true);">
<b-form-select-option value="PRIMARY">Primary</b-form-select-option>
<b-form-select-option value="SECONDARY">Secondary</b-form-select-option>
<b-form-select-option value="SUCCESS">Success</b-form-select-option>
<b-form-select-option value="INFO">Info</b-form-select-option>
<b-form-select-option value="WARNING">Warning</b-form-select-option>
<b-form-select-option value="DANGER">Danger</b-form-select-option>
<b-input-group>
<b-form-input type="color" v-model="user_preferences.nav_bg_color" @change="updateSettings(true);"></b-form-input>
<b-input-group-append>
<b-button @click="user_preferences.nav_bg_color = '#ddbf86'; updateSettings(true);">{{ $t('Reset') }}</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<b-form-group :label="$t('Nav_Text_Mode')" :description="$t('Nav_Text_Mode_Help')">
<b-form-select v-model="user_preferences.nav_text_color" @change="updateSettings(true);">
<b-form-select-option value="LIGHT">Light</b-form-select-option>
<b-form-select-option value="DARK">Dark</b-form-select-option>
</b-form-select>
</b-form-group>
<b-form-group :description="$t('Show_Logo_Help')">
<b-form-checkbox v-model="user_preferences.nav_show_logo" @change="updateSettings(true);">
{{ $t('Show_Logo') }}
</b-form-checkbox>
</b-form-group>
<b-form-group :description="$t('Sticky_Nav_Help')">
<b-form-checkbox v-model="user_preferences.nav_sticky" @change="updateSettings(true);">
{{ $t('Sticky_Nav') }}
</b-form-checkbox>
</b-form-group>

View File

@@ -16,14 +16,14 @@
"file_upload_disabled": "Nahrávání souborů není povoleno pro Váš prostor.",
"warning_space_delete": "Můžete smazat váš prostor včetně všech receptů, nákupních seznamů, jídelníčků a všeho ostatního, co jste vytvořili. Tuto akci nemůžete vzít zpět! Jste si jisti, že chcete pokračovat?",
"food_inherit_info": "Pole potravin, která budou standardně zděděna.",
"step_time_minutes": "Nastavte čas v minutách",
"step_time_minutes": "Délka kroku v minutách",
"confirm_delete": "Jste si jisti že chcete odstranit tento {objekt}?",
"import_running": "Probíhá import, čekejte prosím!",
"all_fields_optional": "Všechna pole jsou nepviná a mohou být ponechána prázdná.",
"convert_internal": "Převést na interní recept",
"show_only_internal": "Zobrazit pouze interní recepty",
"show_split_screen": "Rozdělené zobrazení",
"Log_Recipe_Cooking": "",
"Log_Recipe_Cooking": "Záznam vaření receptu",
"External_Recipe_Image": "Externí obrázek receptu",
"Add_to_Shopping": "Přidat k nákupu",
"Add_to_Plan": "Přidat do jídelníčku",
@@ -35,8 +35,8 @@
"Hide_as_header": "Skryj jako nadpis",
"Add_nutrition_recipe": "Přidat nutriční hodnoty",
"Remove_nutrition_recipe": "Smazat nutriční hodnoty",
"Copy_template_reference": "",
"Save_and_View": "Uložit & Zobrazit",
"Copy_template_reference": "Zkopírovat šablonu odkazu",
"Save_and_View": "Uložit a zobrazit",
"Manage_Books": "Spravovat kuchařky",
"Meal_Plan": "Jídelníček",
"Select_Book": "Vyber kuchařku",
@@ -44,19 +44,19 @@
"Recipe_Image": "Obrázek k receptu",
"Import_finished": "Import dokončen",
"View_Recipes": "Zobrazit recepty",
"Log_Cooking": "",
"Log_Cooking": "Zaznamenat vaření",
"New_Recipe": "Nový recept",
"Url_Import": "Import pomocí URL odkazu",
"Reset_Search": "Zrušit filtry vyhledávání",
"Recently_Viewed": "Naposledy prohlížené",
"Load_More": "Načíst další",
"New_Keyword": "Nové klíčové slovo",
"Delete_Keyword": "Smazat klíčové slovo",
"Edit_Keyword": "Upravit klíčové slovo",
"New_Keyword": "Nový štítek",
"Delete_Keyword": "Smazat štítek",
"Edit_Keyword": "Upravit štítek",
"Edit_Recipe": "Upravit recept",
"Move_Keyword": "Přesunout klíčové slovo",
"Merge_Keyword": "Sloučit klíčové slovo",
"Hide_Keywords": "Skrýt klíčové slovo",
"Move_Keyword": "Přesunout štítek",
"Merge_Keyword": "Sloučit štítek",
"Hide_Keywords": "Skrýt štítek",
"Hide_Recipes": "Skrýt recept",
"Move_Up": "Nahoru",
"Move_Down": "Dolů",
@@ -76,14 +76,14 @@
"Private_Recipe_Help": "Recept můžete zobrazit pouze vy a lidé, se kterými jej sdílíte.",
"reusable_help_text": "Má-li pozvánka platit pro více než jednoho uživatele.",
"Add_Step": "Přidat krok",
"Keywords": "Klíčová slova",
"Keywords": "Štítky",
"Books": "Kuchařky",
"Proteins": "Proteiny",
"Fats": "Tuky",
"Carbohydrates": "Sacharidy",
"Calories": "Kalorie",
"Energy": "Energie",
"Nutrition": "",
"Nutrition": "Výživové hodnoty",
"Date": "Datum",
"Share": "Sdílet",
"Automation": "Automatizace",
@@ -153,7 +153,7 @@
"Hide_Food": "Skrýt potravinu",
"Food_Alias": "Přezdívka potraviny",
"Unit_Alias": "Přezdívka jednotky",
"Keyword_Alias": "Přezdívka klíčového slova",
"Keyword_Alias": "Přezdívka štítku",
"Delete_Food": "Smazat potravinu",
"No_ID": "ID nenalezeno, odstranění není možné.",
"Meal_Plan_Days": "Budoucí jídelníčky",
@@ -162,7 +162,7 @@
"Food": "Potravina",
"Original_Text": "Původní text",
"Recipe_Book": "Kuchařka",
"del_confirmation_tree": "Jste si jistí, že chcete odstranit {source} se všemi pořazenými ?",
"del_confirmation_tree": "Jste si jistí, že chcete odstranit {source} i se všemi potomky?",
"delete_title": "Smazat {type}",
"create_title": "Nový {type}",
"edit_title": "Upravit {type}",
@@ -170,7 +170,7 @@
"Type": "Typ",
"Description": "Popis",
"Recipe": "Recept",
"tree_root": "",
"tree_root": "Kořen stromu",
"Icon": "Ikona",
"Unit": "Jednotka",
"Decimals": "Desetinná místa",
@@ -179,7 +179,7 @@
"New_Unit": "Nová jednotka",
"Create_New_Shopping Category": "Vytvořit novou nákupní kategorii",
"Create_New_Food": "Přidat novou potravinu",
"Create_New_Keyword": "Přidat nové klíčové slovo",
"Create_New_Keyword": "Přidat nový štítek",
"Create_New_Unit": "Přidat novou jednotku",
"Create_New_Meal_Type": "Přidat nový druh jídla",
"Create_New_Shopping_Category": "Přidat novou nákupní kategorii",
@@ -254,7 +254,7 @@
"SupermarketCategoriesOnly": "Pouze kategorie obchodu",
"MoveCategory": "Přesunout do: ",
"CountMore": "...+{count} víc",
"IgnoreThis": "Nikdy automaticky nepřídávat {food} na nákupní seznam",
"IgnoreThis": "Nikdy nepřidávat automaticky {food} na nákupní seznam",
"DelayFor": "Odložit na {hours} hodin",
"Warning": "Varování",
"NoCategory": "Není vybrána žádná kategorie.",
@@ -271,7 +271,7 @@
"default_delay": "Výchozí doba odložení v hodinách",
"plan_share_desc": "Nové položky v jídelníčku budou automaticky sdíleny s vybranými uživateli.",
"shopping_share_desc": "Uživatelé uvidí všechny položky na vašem nákupním seznamu. Abyste viděli položky na jejich seznamu, musí si přidat vás.",
"shopping_auto_sync_desc": "Nastavením 0 dojde k vypnutí automatické synchronizace. Při prohlížení nákupního seznamu je vždy po uplynutí nastaveného počtu vteřin aktualizován o změny, které mohli provést jiní uživatelé. To je užitečné, pokud nakupujete ve více lidech, ale může používat více dat.",
"shopping_auto_sync_desc": "Zadáním hodnoty 0 se automatická synchronizace vypne. Při prohlížení nákupního seznamu se seznam aktualizuje po stanovených sekundách, aby se synchronizovaly změny, které mohl provést někdo jiný. To je užitečné při nakupování s více lidmi, ale spotřebovávají se při tom mobilní data.",
"mealplan_autoadd_shopping_desc": "Automaticky podle jídelníčku přidat ingredience na nákupní seznam.",
"mealplan_autoexclude_onhand_desc": "Nepřidávat ingredience, které jsou k dispozici, na nákupní seznam, když je vytvořen podle jídelníčku (manuálně nebo automaticky).",
"mealplan_autoinclude_related_desc": "Když je nákupní seznam vytvořen podle jídelníčku, přidat i položky z přidružených receptů.",
@@ -280,7 +280,7 @@
"Coming_Soon": "Již brzy",
"Auto_Planner": "Automatický plánovač",
"New_Cookbook": "Nová kuchařka",
"Hide_Keyword": "Skrýt klíčová slova",
"Hide_Keyword": "Skrýt štítky",
"Hour": "Hodina",
"Hours": "Hodiny",
"Day": "Den",
@@ -328,7 +328,7 @@
"OnHand_help": "Potravina je v inventáři a nebude automaticky přidána na nákupní seznam. Status \"k dipozici\" je sdílen s nakupujícími uživateli.",
"ignore_shopping_help": "Nikdy nepřidávat potravinu na nákupní seznam (např. voda)",
"shopping_category_help": "Obchody mohou být seřazeny a třízeny pomocí nákupních kategorií podle rozvržení uliček a regálů.",
"food_recipe_help": "",
"food_recipe_help": "Zde uvedený recept bude připojen k jakémukoli jinému receptu, který používá tuto potravinu",
"Foods": "Potraviny",
"Account": "Účet",
"Cosmetic": "Zobrazení",
@@ -338,7 +338,7 @@
"simple_mode": "Jednoduchý režim",
"advanced": "Pokročilé",
"fields": "Pole",
"show_keywords": "Zobrazit klíčová slova",
"show_keywords": "Zobrazit štítky",
"show_foods": "Zobrazit potraviny",
"show_books": "Zobrazit kuchařky",
"show_rating": "Zobrazit hodnocení",
@@ -359,8 +359,8 @@
"times_cooked": "Kolkrát vařeno",
"date_created": "Datum vytvoření",
"show_sortby": "Zobrazit Seřazeno podle",
"search_rank": "",
"make_now": "",
"search_rank": "Skóre shody",
"make_now": "Udělat hned",
"recipe_filter": "Filtrovat recepty",
"book_filter_help": "Zahrnout i recepty z filtru stejně jako manuálně přidané.",
"review_shopping": "Zkontrolovat nákupní položky před uložením",
@@ -392,9 +392,9 @@
"search_create_help_text": "Vytvořit nový recept přímo v Tandoor.",
"warning_duplicate_filter": "Varování: Kvůli technickým omezení může použití několika filtrů se stejnou kombinací (a/nebo/ne) přinést neočekávaný výsledek.",
"reset_children": "Resetovat propsání podřízených",
"reset_children_help": "",
"reset_children_help": "Přepíše všechny potomky hodnotami dle nastavení propisovaných polí. Pokud není nastaveno pole Propisovaná pole podřízených, tak bude nastaveno pole Propsat hodnoty polí na aktuální hodnotu.",
"reset_food_inheritance": "Resetovat propisování",
"reset_food_inheritance_info": "",
"reset_food_inheritance_info": "Obnoví u všech potravin propisovaná pole na výchozí hodnotu a nastavit daná pole dle nadřazené položky.",
"substitute_help": "Při hledání podle ingrediencí, které jsou k dispozici, jsou zvažovány náhrady.",
"substitute_siblings_help": "Všechny potraviny, které sdílejí nadřazenou položku jsou považovány za náhrady.",
"substitute_children_help": "Všechny potraviny, které jsou podřízeny této, jsou považovány za náhražky.",
@@ -425,8 +425,8 @@
"Social_Authentication": "Přihlašování pomocí účtů sociálních sítí",
"Random Recipes": "Náhodné recepty",
"parameter_count": "Parametr {count}",
"select_keyword": "Vybrat klíčové slovo",
"add_keyword": "Přidat klíčové slovo",
"select_keyword": "Vybrat štítek",
"add_keyword": "Přidat štítek",
"select_file": "Vybrat soubor",
"select_recipe": "Vybrat recept",
"select_unit": "Vybrat jednotku",
@@ -439,7 +439,7 @@
"Username": "Uživatelské jméno",
"First_name": "Jméno",
"Last_name": "Příjmení",
"Keyword": "Klíčové slovo",
"Keyword": "Štítek",
"Advanced": "Rozšířené",
"Page": "Stránka",
"Single": "Jednoduchý",
@@ -479,10 +479,10 @@
"Create Recipe": "Vytvořit recept",
"Import Recipe": "Importovat recept",
"per_serving": "na porci",
"open_data_help_text": "Projekt Tandoor Open Data nabízí komunitou poskytnutá data pro Tandoor. Toto pole je automaticky vyplněno při importu a může být později upraveno.",
"Data_Import_Info": "Rozšiřte svůj prostor o seznamy potravin, jednotek a další spravované komunitou, a vylepšete tak svoji sbírku receptů.",
"open_data_help_text": "Projekt Tandoor Open Data nabízí komunitou poskytnutá otevřená data pro Tandoor. Toto pole se vyplní automaticky při importu a umožňuje budoucí aktualizace.",
"Data_Import_Info": "Rozšiřte svůj prostor o seznamy potravin, jednotek a dalších položek spravovaných komunitou, a vylepšete tak svoji sbírku receptů.",
"Update_Existing_Data": "Aktualizovat existující data",
"Use_Metric": "Používat metrické jednotky",
"Use_Metric": "Použít metrické jednotky",
"Learn_More": "Zjistit víc",
"converted_unit": "Převedená jendotka",
"converted_amount": "Převedené množství",
@@ -490,9 +490,67 @@
"base_amount": "Základní množství",
"Datatype": "Datový typ",
"Number of Objects": "Počet Objektů",
"Property": "Vlastnost",
"Property": "Nutriční vlastnost",
"Conversion": "Převod",
"Properties": "Vlastnosti",
"recipe_property_info": "Můžete také přidávat vlastnosti k Vašim potravinám. Hodnoty budou automaticky přepočteny na základě Vašeho receptu!",
"total": "celkem"
"Properties": "Nutriční vlastnosti",
"recipe_property_info": "Nutriční hodnoty se automaticky dopočtou podle receptu, pokud zadáte nutriční hodnoty přímo potravinám!",
"total": "celkem",
"CustomTheme": "Vlastní téma",
"CustomThemeHelp": "Přepsat styly vybraného motivu nahráním vlastního souboru CSS.",
"CustomLogoHelp": "Nahrajte čtvercové obrázky různých velikostí pro úpravu loga v záložce prohlížeče a v nainstalované webové aplikaci.",
"err_importing_recipe": "Během importu receptu došlo k chybě!",
"Open_Data_Slug": "Identifikátor pro otevřená data",
"Open_Data_Import": "Import otevřených dat",
"FDC_Search": "Vyhledávání v FDC",
"property_type_fdc_hint": "Data z databáze FDC mohou automaticky čerpat pouze typy vlastností se zadaným FDC ID",
"StartDate": "Počáteční datum",
"EndDate": "Konečné datum",
"Welcome": "Vítejte",
"Property_Editor": "Editovat nutriční vlastnosti",
"FDC_ID": "FDC ID",
"FDC_ID_help": "ID v databázi FDC",
"CustomImageHelp": "Nahrajte obrázek, který se zobrazí v přehledu prostoru.",
"CustomNavLogoHelp": "Nahrajte obrázek, který se má zobrazit jako logo v navigačním panelu.",
"CustomLogos": "Vlastní loga",
"OrderInformation": "Položky jsou seřazeny podle čísel od malých po velké.",
"kg": "kilogram [kg] (metrický systém, hmotnost)",
"g": "gram [g] (metrický systém, hmotnost)",
"ounce": "unce [oz] (imperiální systém, hmotnost)",
"pound": "libra (hmotnost)",
"Properties_Food_Unit": "Jednotka nutriční vlastnosti",
"Properties_Food_Amount": "Množství nutriční vlastnosti",
"tsp": "lžička [tsp] (US, objem)",
"imperial_tsp": "lžička imperiální [imp tbsp] (UK, objem)",
"Transpose_Words": "Transponovat slova",
"show_step_ingredients_setting": "Zobrazit ingredience u jednotlivých kroků receptu",
"Logo": "Logo",
"Show_Logo": "Zobrazit logo",
"show_step_ingredients_setting_help": "Zobrazí tabulku ingrediencí vedle kroků receptu. Nastavení se aplikuje při vytváření receptu a následně je možné volbu změnit při úpravě receptu.",
"show_step_ingredients": "Zobrazit ingredience u kroku",
"hide_step_ingredients": "Skrýt ingredience u kroku",
"Show_Logo_Help": "Zobrazit logo Tandoor nebo logo prostoru na navigačním panelu.",
"Nav_Text_Mode_Help": "Pro každé téma se chová jinak.",
"Space_Cosmetic_Settings": "Některá kosmetická nastavení mohou měnit správci prostoru a budou mít přednost před nastavením klienta pro daný prostor.",
"Nav_Text_Mode": "Textový režim navigace",
"show_ingredients_table": "Zobrazit tabulku složek vedle textu kroku",
"pint": "pinta [pt] (US, objem)",
"quart": "quart [qt] (US, objem)",
"imperial_fluid_ounce": "tekutá unce imperiální [imp fl oz] (UK, objem)",
"imperial_pint": "pinta imperiální [imp pt] (UK, objem)",
"imperial_quart": "quart imperiální [imp qt] (UK, objem)",
"gallon": "galon [gal] (US, objem)",
"tbsp": "lžíce [tbsp] (US, objem)",
"imperial_gallon": "galon imperiální [imp gal] (UK, objem)",
"imperial_tbsp": "lžíce imperiální [imp tbsp] (UK, objem)",
"Choose_Category": "Vyberte kategorii",
"Back": "Zpět",
"Food_Replace": "Nahrazení v potravině",
"Unit_Replace": "Nahrazení v jednotce",
"Name_Replace": "Nahrazení v názvu",
"ml": "mililitr [ml] (metrický systém, objem)",
"l": "litr [l] (metrický systém, objem)",
"fluid_ounce": "tekutá unce [fl oz] (US, objem)",
"make_now_count": "Nejvyšší počet chybějících ingrediencí",
"Alignment": "Zarovnání",
"Never_Unit": "Není jednotkou"
}

View File

@@ -1,5 +1,5 @@
{
"warning_feature_beta": "Denne funktion er i øjeblikket i BETA (test) stadie. Forvent fejl og fremtidige ændringer (hvor data kan mistes) ved brug af denne funktion.",
"warning_feature_beta": "Denne funktion er i øjeblikket i BETA (test)-stadie. Forvent fejl og fremtidige ændringer (hvor data kan mistes) ved brug af denne funktion.",
"err_fetching_resource": "Der opstod en fejl under indlæsning af denne ressource!",
"err_creating_resource": "Der opstod en fejl under oprettelsen af denne ressource!",
"err_updating_resource": "Der opstod en fejl under opdateringen af denne ressource!",
@@ -477,5 +477,62 @@
"Unpin": "Frigør",
"PinnedConfirmation": "{recipe} er fastgjort.",
"UnpinnedConfirmation": "{recipe} er frigjort.",
"Combine_All_Steps": "Kombiner alle trin til ét felt."
"Combine_All_Steps": "Kombiner alle trin til ét felt.",
"converted_unit": "Konverteret enhed",
"Property": "Egenskab",
"OrderInformation": "Objekter er rangeret fra små til store tal.",
"show_ingredients_table": "Vis ingredienser i en tabel ved siden af trinnets tekst",
"tsp": "teaspoon [tsp] (US, volumen)",
"imperial_fluid_ounce": "imperial fluid ounce [imp fl oz] (UK, volumen)",
"imperial_tsp": "imperial teaspoon [imp tsp] (UK, volumen)",
"open_data_help_text": "Tandoor Open Data projektet tilføjer netværksgenereret data til Tandoor. Dette felt bliver udfyldt automatisk under importering og muliggør fremtidige opdateringer.",
"converted_amount": "Konverteret mængde",
"StartDate": "Startdato",
"EndDate": "Slutdato",
"show_step_ingredients_setting": "Vis ingredienser ved siden af opskrifttrin",
"l": "liter [l] (metrisk, volumen)",
"g": "gram [g] (metrisk, vægt)",
"kg": "kilogram [kg] (metrisk, vægt)",
"ounce": "ounce [oz] (vægt)",
"pound": "pund (vægt)",
"ml": "milliliter [ml] (metrisk, volumen)",
"fluid_ounce": "flydende ounce [fl oz] (US, volumen)",
"pint": "pint [pt] (US, volumen)",
"Back": "Tilbage",
"quart": "quart [qt] (US, volumen)",
"recipe_property_info": "Du kan også tilføje næringsindhold til ingredienser for at udregne indholdet automatisk baseret på din opskrift!",
"per_serving": "per serveringer",
"Open_Data_Slug": "Open Data Slug",
"Open_Data_Import": "Open Data importering",
"Data_Import_Info": "Udbyg dit Space og gør din opskriftsamling bedre ved at importere en netværkskurateret liste af ingredienser, enheder og mere.",
"Update_Existing_Data": "Opdaterer eksisterende data",
"make_now_count": "Oftest manglende ingredienser",
"Welcome": "Velkommen",
"imperial_pint": "imperial pint [imp pt] (UK, volumen)",
"Alignment": "Justering",
"gallon": "gallon [gal] (US, volumen)",
"Never_Unit": "Aldrig enhed",
"FDC_ID": "FDC ID",
"FDC_ID_help": "FDC database ID",
"Use_Metric": "Benyt metriske enheder",
"Learn_More": "Lær mere",
"base_unit": "Basisenhed",
"base_amount": "Basismængde",
"Datatype": "Datatype",
"Number of Objects": "Antal objekter",
"Conversion": "Konversion",
"Properties": "Egenskaber",
"show_step_ingredients_setting_help": "Tilføj ingredienstabel ved siden af opskrifttrin. Tilføjes ved oprettelsen. Kan overskrives under rediger opskrift.",
"show_step_ingredients": "Vis trinnets ingredienser",
"hide_step_ingredients": "Skjul trinnets ingredienser",
"total": "total",
"tbsp": "tablespoon [tbsp] (US, volumen)",
"imperial_quart": "imperial quart [imp qt] (UK, volumen)",
"imperial_gallon": "imperial gal [imp gal] (UK, volumen)",
"imperial_tbsp": "imperial tablespoon [imp tbsp] (UK, volumen)",
"Choose_Category": "Vælg kategori",
"Transpose_Words": "Omstil ord",
"Name_Replace": "Erstat navn",
"Food_Replace": "Erstat ingrediens",
"Unit_Replace": "Erstat enhed"
}

View File

@@ -535,5 +535,25 @@
"Never_Unit": "Nie Einheit",
"Unit_Replace": "Einheit Ersetzen",
"quart": "\"Quart\" [qt] (US, Volumen)",
"imperial_quart": "Engl. \"Quart\" [imp qt] (UK, Volumen)"
"imperial_quart": "Engl. \"Quart\" [imp qt] (UK, Volumen)",
"err_importing_recipe": "Beim Importieren des Rezeptes ist ein Fehler aufgetreten!",
"property_type_fdc_hint": "Nur Nährwerte mit einer FDC ID können automatisch Daten aus der FDC Datenbank beziehen",
"Property_Editor": "Nährwerte bearbeiten",
"CustomTheme": "Benutzerdefiniertes Theme",
"CustomThemeHelp": "Überschreiben Sie die Stile des ausgewählten Themes, indem Sie eine eigene CSS-Datei hochladen.",
"CustomLogoHelp": "Laden Sie quadratische Bilder in verschiedenen Größen hoch, um das Logo im Browsertab und der installierten Webanwendung zu ändern.",
"Show_Logo_Help": "Zeigen Sie das Tandoor- oder Space-Logo in der Navigationsleiste an.",
"Space_Cosmetic_Settings": "Einige optische Einstellungen können von Administratoren des Bereichs geändert werden und setzen die Client-Einstellungen für diesen Bereich außer Kraft.",
"Properties_Food_Amount": "Nährwertangaben",
"Properties_Food_Unit": "Nährwert Einheit",
"FDC_Search": "FDC Suche",
"Logo": "Logo",
"Show_Logo": "Logo anzeigen",
"Nav_Text_Mode": "Navigation Textmodus",
"Nav_Text_Mode_Help": "Verhält sich bei jedem Theme anders.",
"FDC_ID": "FDC ID",
"FDC_ID_help": "FDC Datenbank ID",
"CustomImageHelp": "Laden Sie ein Bild hoch, das in der Space-Übersicht angezeigt werden soll.",
"CustomNavLogoHelp": "Laden Sie ein Bild hoch, das als Logo für die Navigationsleiste verwendet werden soll.",
"CustomLogos": "Individuelle Logos"
}

File diff suppressed because it is too large Load Diff

View File

@@ -487,5 +487,39 @@
"PinnedConfirmation": "{recipe} a été épinglée.",
"Back": "Retour",
"Open_Data_Import": "Import Open Data",
"Data_Import_Info": "Améliorez votre groupe en important des données partagées par la communauté afin d'améliorer vos collections de recettes : listes d'aliments, unités et plus encore."
"Data_Import_Info": "Améliorez votre groupe en important des données partagées par la communauté afin d'améliorer vos collections de recettes : listes d'aliments, unités et plus encore.",
"Nav_Color": "Couleur de la Navigation",
"Nav_Color_Help": "Changer la couleur de la navigation.",
"reset_food_inheritance_info": "Réinitialiser tous les champs d'héritage des aliments par les valeurs de leurs parents.",
"last_viewed": "Vu dernièrement",
"substitute_children_help": "Tout aliment étant enfant de cet aliment est considéré comme substitut.",
"show_step_ingredients": "Afficher les ingrédients de l'étape",
"FDC_ID": "ID FCD",
"FDC_ID_help": "ID de base de données FDC",
"reset_food_inheritance": "Réinitialiser l'héritage",
"kg": "kilogramme [kg] (métrique, poids)",
"ounce": "once [oz] (poids)",
"pound": "livre (poids)",
"base_amount": "Quantité de base",
"hide_step_ingredients": "Cacher les ingrédients de l'étape",
"show_ingredients_table": "Afficher une table des ingrédients à coté du texte de l'étape",
"Bookmarklet": "Signet",
"l": "litre [l] (métrique, volume)",
"Choose_Category": "Choisir une catégorie",
"err_importing_recipe": "Une erreur s'est produite lors de l'importation de cette recette !",
"Properties_Food_Amount": "Propriété Quantité de nourriture",
"Properties_Food_Unit": "Propriété Unité de nourriture",
"FDC_Search": "Recherche dans le FDC",
"property_type_fdc_hint": "Seules les propriétés avec un ID FDC peuvent être mises à jour automatiquement depuis la base FDC",
"Property_Editor": "Editeur de propriétés",
"warning_duplicate_filter": "Attention : en raison de limitations techniques, l'emploi de multiples filtres (and/or/not) peut mener à des résultats inattendus.",
"Social_Authentication": "Authentification Sociale",
"total": "total",
"g": "gramme [g] (métrique, poids)",
"ml": "millilitre [ml] (métrique, volume)",
"Never_Unit": "Ne pas mettre d'unité",
"Transpose_Words": "Transposer les mots",
"Name_Replace": "Remplacer le Nom",
"Food_Replace": "Remplacer l'aliment",
"Unit_Replace": "Remplacer l'Unité"
}

View File

@@ -263,9 +263,9 @@
"Current_Period": "תקופה נוכחית",
"Next_Day": "היום הבא",
"Previous_Day": "יום קודם",
"Inherit": "",
"InheritFields": "",
"FoodInherit": "",
"Inherit": "ירושה",
"InheritFields": "ירושת ערכי שדות",
"FoodInherit": "ערכי מזון",
"ShowUncategorizedFood": "הצג לא מוגדר",
"GroupBy": "אסוף לפי",
"Language": "שפה",
@@ -317,14 +317,14 @@
"CategoryName": "שם קטגוריה",
"SupermarketName": "שם סופרמרקט",
"CategoryInstruction": "גרור קטגוריות לשינוי הסדר שבו הן מופיעות ברשימת הקניות.",
"shopping_recent_days_desc": "",
"shopping_recent_days": "",
"download_pdf": "",
"download_csv": "",
"shopping_recent_days_desc": "מספר ימי קניות להציג.",
"shopping_recent_days": "מספר ימים",
"download_pdf": "הורד PDF",
"download_csv": "הורד CSV",
"csv_delim_help": "",
"csv_delim_label": "",
"SuccessClipboard": "",
"copy_to_clipboard": "",
"SuccessClipboard": "רשימת קניות הועתקה",
"copy_to_clipboard": "העתק",
"csv_prefix_help": "תחילית להוספה כאשר מעתיקים את הרשימה ללוח הכתיבה.",
"csv_prefix_label": "רשימת תחיליות",
"copy_markdown_table": "העתק כטבלת Markdown",
@@ -521,5 +521,15 @@
"Alignment": "יישור",
"StartDate": "תאריך התחלה",
"EndDate": "תאריך סיום",
"OrderInformation": "המוצרים מוצגים מהמספר הקטן לגדול."
"OrderInformation": "המוצרים מוצגים מהמספר הקטן לגדול.",
"FDC_ID_help": "מספר FDC",
"FDC_ID": "מספר FDC",
"show_step_ingredients_setting": "הצג חומרי גלם בתוך שלבי המרשם",
"err_importing_recipe": "שגיאה בעת יבוא המרשם!",
"FDC_Search": "חפש FDC",
"property_type_fdc_hint": "רק תכונות עם מספר FDC ימשכו מבסיס נתוני FDC",
"Property_Editor": "עורך ערכים",
"show_step_ingredients_setting_help": "הצג טבלת חומרי גלם לצדי שלבי המרשם. ניתן לשנות בזמן עריכת המרשם.",
"show_step_ingredients": "הראה חומרי גלם בשלבי המרשם",
"hide_step_ingredients": "הסתר חומרי גלם בשלבי המרשם"
}

View File

@@ -61,7 +61,7 @@
"Step_Name": "Lépés neve",
"Step_Type": "Lépés típusa",
"Make_Header": "Átalakítás címsorra",
"Make_Ingredient": "",
"Make_Ingredient": "Összetevő létrehozása",
"Enable_Amount": "Összeg bekapcsolása",
"Disable_Amount": "Összeg kikapcsolása",
"Ingredient Editor": "Hozzávalók szerkesztője",
@@ -172,7 +172,7 @@
"and_down": "& le",
"Instructions": "Elkészítés",
"Unrated": "Nem értékelt",
"Automate": "",
"Automate": "Automatizálás",
"Empty": "Üres",
"Key_Ctrl": "Ctrl",
"Key_Shift": "Shift",
@@ -187,7 +187,7 @@
"OnHand": "Jelenleg készleten",
"FoodOnHand": "Önnek {food} van készleten.",
"FoodNotOnHand": "Önnek {food} nincs készleten.",
"Undefined": "",
"Undefined": "Meghatározatlan",
"Create_Meal_Plan_Entry": "Menüterv bejegyzés létrehozása",
"Edit_Meal_Plan_Entry": "Menüterv bejegyzés szerkesztése",
"Title": "Cím",
@@ -233,7 +233,7 @@
"ShowUncategorizedFood": "",
"GroupBy": "Csoportosítva",
"SupermarketCategoriesOnly": "Csak a szupermarket kategóriák",
"MoveCategory": "",
"MoveCategory": "Áthelyezés ide: ",
"CountMore": "",
"IgnoreThis": "",
"DelayFor": "Késleltetés {hours} óráig",
@@ -241,7 +241,7 @@
"NoCategory": "Nincs kategória kiválasztva.",
"InheritWarning": "",
"ShowDelayed": "",
"Completed": "",
"Completed": "Kész",
"OfflineAlert": "Ön éppen offline állapotban van, a bevásárlólista nem biztos, hogy szinkronizálódik.",
"shopping_share": "Bevásárlólista megosztása",
"shopping_auto_sync": "Automatikus szinkronizáció",
@@ -265,7 +265,7 @@
"err_move_self": "",
"nothing": "",
"err_merge_self": "",
"show_sql": "",
"show_sql": "SQL megjelenítése",
"filter_to_supermarket_desc": "Alapértelmezés szerint a bevásárlólista szűrése csak a kiválasztott szupermarket kategóriáit tartalmazza.",
"CategoryName": "Kategória neve",
"SupermarketName": "Szupermarket neve",
@@ -281,10 +281,10 @@
"csv_prefix_help": "A lista vágólapra másolásakor hozzáadandó előtag.",
"csv_prefix_label": "",
"copy_markdown_table": "",
"in_shopping": "",
"in_shopping": "Bevásárlólistában",
"DelayUntil": "",
"Pin": "Kitűzés",
"mark_complete": "",
"mark_complete": "Késznek jelölés",
"QuickEntry": "Gyors bevitel",
"shopping_add_onhand_desc": "",
"shopping_add_onhand": "",
@@ -322,8 +322,8 @@
"desc": "Csökkenő",
"date_viewed": "Utoljára megtekintve",
"last_cooked": "Utoljára elkészítve",
"times_cooked": "",
"date_created": "",
"times_cooked": "szor elkészítve",
"date_created": "Létrehozás dátuma",
"show_sortby": "",
"search_rank": "Keresési rangsor",
"make_now": "",
@@ -333,7 +333,7 @@
"view_recipe": "Recept megtekintése",
"copy_to_new": "Másolás új receptbe",
"recipe_name": "Recept neve",
"paste_ingredients_placeholder": "",
"paste_ingredients_placeholder": "Az összetevők listájának beillesztése ide...",
"paste_ingredients": "Hozzávalók beillesztése",
"ingredient_list": "Hozzávalók listája",
"explain": "Magyarázat",
@@ -370,19 +370,19 @@
"no_pinned_recipes": "Nincsenek kitűzött receptjei!",
"Planned": "Tervezett",
"Pinned": "Kitűzve",
"Imported": "",
"Imported": "Importálva",
"Quick actions": "Gyors parancsok",
"Ratings": "Értékelések",
"Internal": "Belső",
"Units": "Mennyiségi egységek",
"Random Recipes": "",
"Random Recipes": "Véletlenszerű receptek",
"parameter_count": "Paraméter {count}",
"select_keyword": "Kulcsszó kiválasztása",
"add_keyword": "Kulcsszó hozzáadása",
"select_file": "",
"select_file": "Fájl kiválasztása",
"select_recipe": "Recept kiválasztása",
"select_unit": "",
"select_food": "",
"select_unit": "Egység kiválasztása",
"select_food": "Étel kiválasztása",
"remove_selection": "Kijelölés törlése",
"empty_list": "A lista üres.",
"Select": "Kiválasztás",
@@ -391,13 +391,13 @@
"Keyword": "Kulcsszó",
"Advanced": "Haladó",
"Page": "Oldal",
"Single": "",
"Single": "Egyetlen",
"Multiple": "Több",
"Reset": "Visszaállítás",
"Options": "Opciók",
"Create Food": "Alapanyag létrehozása",
"create_food_desc": "",
"additional_options": "",
"additional_options": "További lehetőségek",
"Importer_Help": "",
"Documentation": "Dokumentáció",
"Select_App_To_Import": "Kérjük, válasszon ki egy alkalmazást, amelyből importálni szeretne",
@@ -415,9 +415,9 @@
"Are_You_Sure": "Biztos benne?",
"Plural": "Többes szám",
"plural_short": "többes szám",
"Use_Plural_Unit_Always": "",
"Use_Plural_Unit_Always": "Mindig többes számot használjon az mértékegységhez",
"Use_Plural_Unit_Simple": "A mértékegység többes számának dinamikus beállítása",
"Use_Plural_Food_Always": "",
"Use_Plural_Food_Always": "Mindig többes számot használjon az ételhez",
"Use_Plural_Food_Simple": "Alapanyag többes számának dinamikus használata",
"plural_usage_info": "",
"Back": "Vissza",
@@ -516,5 +516,10 @@
"API": "API",
"Account": "Fiók",
"show_step_ingredients_setting": "Hozzávalók megjelenítése a recept lépései mellett",
"Disable": "Letiltás"
"Disable": "Letiltás",
"Name_Replace": "Név cseréje",
"make_now_count": "Leginkább hiányzó összetevők",
"Combine_All_Steps": "Egyesítse az összes lépést egyetlen mezőbe.",
"Food_Replace": "Étel cseréje",
"err_importing_recipe": "Hiba történt a recept importálásakor!"
}

View File

@@ -162,7 +162,7 @@
"Unit_Alias": "Alias Unità",
"Keyword_Alias": "Alias Parola Chiave",
"Table_of_Contents": "Indice dei contenuti",
"warning_feature_beta": "Questa funzione è attualmente in BETA (non è completa). Potrebbero verificarsi delle anomalie e modifiche che in futuro potrebbero bloccare la funzionalità stessa o rimuove i dati correlati a essa.",
"warning_feature_beta": "Questa funzione è attualmente in BETA (non è completa). Potrebbero verificarsi delle anomalie e modifiche che in futuro potrebbero bloccare la funzionalità stessa o rimuove i dati correlati ad essa.",
"Shopping_list": "Lista della spesa",
"Title": "Titolo",
"Create_New_Meal_Type": "Aggiungi nuovo tipo di pasto",
@@ -393,7 +393,7 @@
"view_recipe": "Mostra ricetta",
"copy_to_new": "Copia in una nuova ricetta",
"Pinned": "Fissato",
"App": "App",
"App": "Applicazione",
"filter": "Filtro",
"explain": "Maggior informazioni",
"Website": "Sito web",

View File

@@ -534,5 +534,25 @@
"Food_Replace": "Zastąp produkt",
"Unit_Replace": "Zastąp jednostkę",
"Alignment": "Wyrównanie",
"make_now_count": "Najbardziej brakujące składniki"
"make_now_count": "Najbardziej brakujące składniki",
"CustomTheme": "Własny motyw",
"CustomThemeHelp": "Zastąp style wybranego motywu, przesyłając własny plik CSS.",
"CustomLogoHelp": "Prześlij kwadratowe obrazy w różnych rozmiarach, aby zmienić logo w zakładce przeglądarki i zainstalowanej aplikacji internetowej.",
"Logo": "Logo",
"Show_Logo_Help": "Pokaż logo Tandoor lub przestrzeni na pasku nawigacyjnym.",
"Space_Cosmetic_Settings": "Administratorzy przestrzeni mogą zmienić niektóre ustawienia kosmetyczne, które zastąpią ustawienia klienta dla tej przestrzeni.",
"err_importing_recipe": "Wystąpił błąd podczas importowania przepisu!",
"Properties_Food_Amount": "Właściwości ilości żywności",
"Properties_Food_Unit": "Właściwości jednostek żywności",
"FDC_Search": "Wyszukiwanie w FDC",
"property_type_fdc_hint": "Tylko właściwe typy z identyfikatorem FDC mogą automatycznie pobierać dane z bazy danych FDC",
"Property_Editor": "Edytor właściwości",
"FDC_ID": "Identyfikator FDC",
"FDC_ID_help": "Identyfikator bazy FDC",
"CustomImageHelp": "Prześlij obraz, który będzie wyświetlany w przeglądzie przestrzeni.",
"CustomNavLogoHelp": "Prześlij obraz, który będzie używany jako logo paska nawigacyjnego.",
"CustomLogos": "Własne loga",
"Show_Logo": "Pokaż logo",
"Nav_Text_Mode": "Tryb nawigacji tekstowej",
"Nav_Text_Mode_Help": "Zachowuje się inaczej dla każdego motywu."
}

View File

@@ -345,5 +345,6 @@
"GroupBy": "Сгруппировать по",
"food_inherit_info": "Поля для продуктов питания, которые должны наследоваться по умолчанию.",
"warning_space_delete": "Вы можете удалить свое пространство, включая все рецепты, списки покупок, планы питания и все остальное, что вы создали. Этого нельзя отменить! Вы уверены, что хотите это сделать?",
"Description_Replace": "Изменить описание"
"Description_Replace": "Изменить описание",
"err_importing_recipe": "Произошла ошибка при импортировании рецепта!"
}

View File

@@ -1,14 +1,14 @@
import {defineStore} from 'pinia'
import {ApiApiFactory} from "@/utils/openapi/api";
const _STORE_ID = 'meal_plan_store'
const _LOCAL_STORAGE_KEY = 'MEAL_PLAN_CLIENT_SETTINGS'
import { ApiApiFactory } from "@/utils/openapi/api"
import { StandardToasts } from "@/utils/utils"
import { defineStore } from "pinia"
import Vue from "vue"
import {StandardToasts} from "@/utils/utils";
const _STORE_ID = "meal_plan_store"
const _LOCAL_STORAGE_KEY = "MEAL_PLAN_CLIENT_SETTINGS"
/*
* test store to play around with pinia and see if it can work for my usecases
* dont trust that all mealplans are in store as there is no cache validation logic, its just a shared data holder
* */
* test store to play around with pinia and see if it can work for my usecases
* dont trust that all mealplans are in store as there is no cache validation logic, its just a shared data holder
* */
export const useMealPlanStore = defineStore(_STORE_ID, {
state: () => ({
plans: {},
@@ -19,7 +19,7 @@ export const useMealPlanStore = defineStore(_STORE_ID, {
plan_list: function () {
let plan_list = []
for (let key in this.plans) {
plan_list.push(this.plans[key]);
plan_list.push(this.plans[key])
}
return plan_list
},
@@ -35,7 +35,7 @@ export const useMealPlanStore = defineStore(_STORE_ID, {
servings: 1,
shared: [],
title: "",
title_placeholder: 'Title', // meal plan edit modal should be improved to not need this
title_placeholder: "Title", // meal plan edit modal should be improved to not need this
}
},
client_settings: function () {
@@ -43,22 +43,15 @@ export const useMealPlanStore = defineStore(_STORE_ID, {
this.settings = this.loadClientSettings()
}
return this.settings
}
},
},
actions: {
refreshFromAPI(from_date, to_date) {
if (this.currently_updating !== [from_date, to_date]) {
this.currently_updating = [from_date, to_date] // certainly no perfect check but better than nothing
let options = {
query: {
from_date: from_date,
to_date: to_date,
},
}
let apiClient = new ApiApiFactory()
apiClient.listMealPlans(options).then(r => {
apiClient.listMealPlans(from_date, to_date).then((r) => {
r.data.forEach((p) => {
Vue.set(this.plans, p.id, p)
})
@@ -68,31 +61,40 @@ export const useMealPlanStore = defineStore(_STORE_ID, {
},
createObject(object) {
let apiClient = new ApiApiFactory()
return apiClient.createMealPlan(object).then(r => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
Vue.set(this.plans, r.data.id, r.data)
return r
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)
})
return apiClient
.createMealPlan(object)
.then((r) => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
Vue.set(this.plans, r.data.id, r.data)
return r
})
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)
})
},
updateObject(object) {
let apiClient = new ApiApiFactory()
return apiClient.updateMealPlan(object.id, object).then(r => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
Vue.set(this.plans, object.id, object)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
return apiClient
.updateMealPlan(object.id, object)
.then((r) => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
Vue.set(this.plans, object.id, object)
})
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
deleteObject(object) {
let apiClient = new ApiApiFactory()
return apiClient.destroyMealPlan(object.id).then(r => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE)
Vue.delete(this.plans, object.id)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
})
return apiClient
.destroyMealPlan(object.id)
.then((r) => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE)
Vue.delete(this.plans, object.id)
})
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
})
},
updateClientSettings(settings) {
this.settings = settings
@@ -110,6 +112,6 @@ export const useMealPlanStore = defineStore(_STORE_ID, {
} else {
return JSON.parse(s)
}
}
},
},
})
})

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