Compare commits

...

154 Commits
1.5.4 ... 1.5.6

Author SHA1 Message Date
vabene1111
d42d784aeb Merge branch 'develop' 2023-08-29 13:09:38 +02:00
vabene1111
ce84b3b385 updated translations 2023-08-29 13:09:32 +02:00
vabene1111
74fbcb03a1 Merge pull request #2592 from WoosterInitiative/develop
Update en.json
2023-08-29 13:05:54 +02:00
Karl
8675143cc1 Update en.json
Correct "loosing" to "losing."
2023-08-27 14:22:26 -07:00
Étienne
75e23106fc Translated using Weblate (French)
Currently translated at 88.6% (461 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2023-08-27 11:20:01 +00:00
Matias Laporte
2ad89b5b22 Translated using Weblate (Spanish)
Currently translated at 61.4% (301 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/es/
2023-08-27 11:20:01 +00:00
vabene1111
36074c9c35 added apple header 2023-08-27 08:42:11 +02:00
vabene1111
05560c5730 improved user agent for url image import 2023-08-26 07:54:19 +02:00
vabene1111
6ba4db6ff9 Merge pull request #2432 from smilerz/new_automations
add NEVER_UNIT automation
2023-08-26 07:41:27 +02:00
smilerz
6353885f9c update migrations after rebase 2023-08-25 08:10:21 -05:00
smilerz
833ebf8c0c Merge branch 'new_automations' of github.com:smilerz/recipes into new_automations 2023-08-25 08:04:30 -05:00
smilerz
0662255b27 update migrations 2023-08-25 08:03:07 -05:00
smilerz
fde4ea8c4c filtered automations to tokens present 2023-08-25 08:01:56 -05:00
smilerz
132815496c create Transpose Words automation 2023-08-25 08:01:26 -05:00
smilerz
a7a6abe3d2 add NEVER_UNIT automation 2023-08-25 07:57:56 -05:00
smilerz
2f617aa40f fix incorrect variable in apply_transpose_words_automations 2023-08-25 07:54:07 -05:00
smilerz
9b50ea4c22 make automation parameters case insensitive on search 2023-08-25 07:54:06 -05:00
smilerz
cde8dd8b53 fixed defect in NEVER_UNIT automation 2023-08-25 07:54:06 -05:00
smilerz
8411537f87 filtered automations to tokens present 2023-08-25 07:54:05 -05:00
smilerz
479cf1a042 create Transpose Words automation 2023-08-25 07:54:05 -05:00
smilerz
8fa00972bd add NEVER_UNIT automation 2023-08-25 07:53:53 -05:00
vabene1111
5d5eb45b5a also accept text as a parameter for import url 2023-08-25 12:15:58 +02:00
vabene1111
87beed48c9 testing share targets 2023-08-25 11:05:51 +02:00
vabene1111
cf7cc6c637 only url on share target 2023-08-25 10:56:15 +02:00
vabene1111
3d45a068e4 added share target to web manifest 2023-08-25 09:45:31 +02:00
vabene1111
01ce658883 fixed step factory 2023-08-25 09:12:58 +02:00
vabene1111
92d648c3a3 added ability to order property types 2023-08-24 12:50:17 +02:00
vabene1111
17fa3c8d7c fixed serving property calculation 2023-08-24 11:20:43 +02:00
vabene1111
c1ae4e3905 added migration for step ingredient showing 2023-08-24 11:20:31 +02:00
vabene1111
d819cbc20e Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-08-24 10:34:31 +02:00
vabene1111
f255397bbd added translation 2023-08-24 10:34:30 +02:00
vabene1111
2f0929e90e Merge pull request #2539 from srwareham/hide-step-ingredients
Added option: Hide step ingredients
2023-08-24 10:33:57 +02:00
srwareham
6785033a21 Add step-level configuration whether an ingredients table should be shown. User-level default added to settings 2023-08-23 21:46:09 -07:00
vabene1111
0345b7720c Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-08-23 13:17:48 +02:00
vabene1111
7163c33b2a fixed food edit merge/move/automate not working 2023-08-23 13:05:07 +02:00
vabene1111
934df3c5f7 Merge pull request from GHSA-66qh-qh47-9w6p
Changed remote auth var-name in env, info in docs and processing in settings
2023-08-23 11:24:29 +02:00
Theodoros Grammenos
2888b18819 Translated using Weblate (Greek)
Currently translated at 100.0% (520 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/el/
2023-08-22 21:19:55 +00:00
Theodoros Grammenos
c01081255b Translated using Weblate (Greek)
Currently translated at 62.5% (325 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/el/
2023-08-21 09:19:56 +00:00
Theodoros Grammenos
2e606dc166 Translated using Weblate (Greek)
Currently translated at 54.9% (288 of 524 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/el/
2023-08-21 09:19:55 +00:00
Theodoros Grammenos
835c5a1d3a Translated using Weblate (Greek)
Currently translated at 13.4% (70 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/el/
2023-08-19 21:36:10 +00:00
NeoID
8580aea43f Translated using Weblate (Norwegian Bokmål)
Currently translated at 71.4% (265 of 371 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/nb_NO/
2023-08-19 21:36:10 +00:00
Alexandre Braure
db4f2db236 Translated using Weblate (French)
Currently translated at 88.6% (461 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2023-08-16 21:19:58 +00:00
Bastian
7e9cef6075 Translated using Weblate (German)
Currently translated at 98.0% (510 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-08-16 21:19:58 +00:00
Alexandre Braure
75612781da Translated using Weblate (French)
Currently translated at 90.6% (444 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/fr/
2023-08-16 21:19:58 +00:00
Henning Bopp
f5fb4e563d Changed var-name in env, info in docs and processing in settings
Also added a deprecation warning and changed the structure of the authentication.md

Signed-off-by: Henning Bopp <henning.bopp@gmail.com>
2023-08-16 21:19:38 +02:00
vabene1111
1ecb57e795 removed dependency and upgraded bleach clean 2023-08-16 07:22:09 +02:00
vabene1111
c4a0df26fc Merge pull request #2446 from TandoorRecipes/dependabot/pip/bleach-6.0.0
Bump bleach from 5.0.1 to 6.0.0
2023-08-16 07:14:36 +02:00
vabene1111
8ff5142149 auto meal plan tweaks and improvements 2023-08-16 07:10:24 +02:00
vabene1111
716976453a fixed pycharm file 2023-08-16 06:20:43 +02:00
vabene1111
f07dec6062 Merge pull request #2468 from AquaticLava/Auto-Planner
Auto meal plan
2023-08-16 06:18:43 +02:00
vabene1111
ffc96890ac Delete recipes.iml 2023-08-16 06:18:02 +02:00
vabene1111
a8fd703d1d Merge pull request #2529 from TandoorRecipes/dependabot/npm_and_yarn/vue/typescript-5.1.6
Bump typescript from 4.9.5 to 5.1.6 in /vue
2023-08-16 06:06:28 +02:00
vabene1111
4592cc85a5 Merge pull request #2566 from TandoorRecipes/dependabot/npm_and_yarn/vue/eslint-8.46.0
Bump eslint from 7.32.0 to 8.46.0 in /vue
2023-08-16 06:06:17 +02:00
vabene1111
4a835c38d8 Merge pull request #2567 from TandoorRecipes/dependabot/pip/django-cleanup-8.0.0
Bump django-cleanup from 7.0.0 to 8.0.0
2023-08-16 06:06:03 +02:00
vabene1111
ef72a07acb Merge pull request #2568 from TandoorRecipes/dependabot/pip/lxml-4.9.3
Bump lxml from 4.9.2 to 4.9.3
2023-08-16 06:05:50 +02:00
vabene1111
246b9c4a02 Merge pull request #2569 from TandoorRecipes/dependabot/pip/django-auth-ldap-4.4.0
Bump django-auth-ldap from 4.2.0 to 4.4.0
2023-08-16 06:05:35 +02:00
Jochum van der Heide
c18a77bc9b Translated using Weblate (Dutch)
Currently translated at 99.8% (519 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nl/
2023-08-15 19:19:56 +00:00
Jochum van der Heide
3d7e2b1aa5 Translated using Weblate (Dutch)
Currently translated at 100.0% (490 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/nl/
2023-08-15 19:19:55 +00:00
vabene1111
28f18fbc42 Merge branch 'develop' 2023-08-14 06:26:25 +02:00
Miha Perpar
ba361a8a27 Translated using Weblate (Slovenian)
Currently translated at 59.0% (307 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/sl/
2023-08-13 08:19:59 +00:00
Miha Perpar
fc2ce6e488 Translated using Weblate (Slovenian)
Currently translated at 15.9% (81 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/sl/
2023-08-13 08:19:59 +00:00
Tomasz Klimczak
d7f77a572a Translated using Weblate (Polish)
Currently translated at 100.0% (520 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/
2023-08-13 08:19:58 +00:00
Fabian Flodman
64e28fd01a Translated using Weblate (German)
Currently translated at 97.3% (506 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-08-13 08:19:58 +00:00
Thomas
714d5e5184 Translated using Weblate (German)
Currently translated at 97.3% (506 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-08-13 08:19:58 +00:00
Fabian Flodman
640500c82d Translated using Weblate (German)
Currently translated at 100.0% (490 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2023-08-13 08:19:58 +00:00
smilerz
8bf661c1ab update migrations 2023-08-10 09:06:41 -05:00
smilerz
1d29e435d5 Merge branch 'new_automations' of github.com:smilerz/recipes into new_automations 2023-08-10 08:55:14 -05:00
smilerz
6eac48633b fix incorrect variable in apply_transpose_words_automations 2023-08-10 08:54:44 -05:00
smilerz
743fae1ba7 make automation parameters case insensitive on search 2023-08-10 08:54:44 -05:00
smilerz
b3565451ff fixed defect in NEVER_UNIT automation 2023-08-10 08:54:44 -05:00
smilerz
4a93681870 filtered automations to tokens present 2023-08-10 08:54:43 -05:00
smilerz
d83b0484d8 create Transpose Words automation 2023-08-10 08:54:43 -05:00
smilerz
c0d67dbc58 add NEVER_UNIT automation 2023-08-10 08:54:33 -05:00
smilerz
3a8ea4b4c9 fix incorrect variable in apply_transpose_words_automations 2023-08-10 08:33:02 -05:00
vabene1111
4b14a099df better logging 2023-08-05 12:00:03 +02:00
vabene1111
dae7cbfb85 version script updates and system page fix 2023-08-05 10:56:27 +02:00
vabene1111
0c62b80e3a Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2023-08-05 10:28:44 +02:00
vabene1111
678963e6dd more debug in version script 2023-08-05 10:28:39 +02:00
dependabot[bot]
6d84c718fd Bump django-cleanup from 7.0.0 to 8.0.0
Bumps [django-cleanup](https://github.com/un1t/django-cleanup) from 7.0.0 to 8.0.0.
- [Changelog](https://github.com/un1t/django-cleanup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/un1t/django-cleanup/compare/7.0.0...8.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-05 07:46:02 +00:00
vabene1111
b8e1ed8967 Merge pull request #2570 from TandoorRecipes/dependabot/pip/cryptography-41.0.3
Bump cryptography from 41.0.2 to 41.0.3
2023-08-05 09:45:13 +02:00
Chen
d87633433a Translated using Weblate (Hebrew)
Currently translated at 90.5% (471 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/he/
2023-08-03 22:19:55 +00:00
Chen
fe33adbba0 Translated using Weblate (Hebrew)
Currently translated at 25.7% (134 of 520 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/he/
2023-08-02 15:51:50 +00:00
Chen
baa84cf481 Added translation using Weblate (Hebrew) 2023-08-02 15:26:57 +00:00
AquaticLava
ecd828008e added auto shopping functionality. fixed bug when there are no matching recipes 2023-08-01 21:52:59 -06:00
dependabot[bot]
2b8c607b78 Bump cryptography from 41.0.2 to 41.0.3
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.2 to 41.0.3.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.2...41.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-02 02:23:22 +00:00
AquaticLava
df684f591a added share functionality. changed random recipe selection to prevent repeating duplicate choices. 2023-08-01 17:02:05 -06:00
dependabot[bot]
cb5b51bde3 Bump django-auth-ldap from 4.2.0 to 4.4.0
Bumps [django-auth-ldap](https://github.com/django-auth-ldap/django-auth-ldap) from 4.2.0 to 4.4.0.
- [Release notes](https://github.com/django-auth-ldap/django-auth-ldap/releases)
- [Changelog](https://github.com/django-auth-ldap/django-auth-ldap/blob/master/docs/changes.rst)
- [Commits](https://github.com/django-auth-ldap/django-auth-ldap/compare/4.2.0...4.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 00:28:49 +00:00
dependabot[bot]
7f27419215 Bump lxml from 4.9.2 to 4.9.3
Bumps [lxml](https://github.com/lxml/lxml) from 4.9.2 to 4.9.3.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-4.9.2...lxml-4.9.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 00:28:28 +00:00
dependabot[bot]
312cd077d0 Bump eslint from 7.32.0 to 8.46.0 in /vue
Bumps [eslint](https://github.com/eslint/eslint) from 7.32.0 to 8.46.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.32.0...v8.46.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 00:15:02 +00:00
Mára Štěpánek
eac059ca85 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/
2023-07-31 14:19:56 +00:00
vabene1111
782dd4cb17 build stuff 2023-07-29 11:24:11 +02:00
vabene1111
f7b60f2c52 version script improvements 2023-07-29 10:55:18 +02:00
vabene1111
ca28e52698 keep git installed 2023-07-29 10:06:51 +02:00
vabene1111
0c2c12d536 improved version script 2023-07-29 08:43:17 +02:00
vabene1111
113c40c243 changed version command order 2023-07-29 08:38:13 +02:00
vabene1111
0688f46d8b new version script 2023-07-29 08:32:10 +02:00
vabene1111
2fdcdba889 base pasth pdf viewer 2023-07-29 07:48:27 +02:00
vabene1111
6a39148e5f fixed try catch and added git to permanent dependency 2023-07-28 15:59:48 +02:00
vabene1111
22dfb40fd5 improved system info even more 2023-07-27 20:48:51 +02:00
vabene1111
2b5a86ce53 improved system page 2023-07-27 20:40:25 +02:00
vabene1111
e77016ea9b playing around 2023-07-27 18:49:39 +02:00
vabene1111
9988a61da7 added version number to system screen 2023-07-27 18:39:21 +02:00
vabene1111
f34fb8eec3 Merge pull request #2563 from smilerz/test_fixes
fixed rating sort order and updated tests
2023-07-26 06:21:54 +02:00
smilerz
7853357065 fix error when filtering on rating in saved filters 2023-07-25 17:48:19 -05:00
smilerz
6f1befc43c fixed rating sort order and updated tests 2023-07-25 11:37:48 -05:00
vabene1111
c18386b9b5 fixed copied ingredients being linked together 2023-07-22 12:59:31 +02:00
vabene1111
d5ba2e6716 improved multi url import 2023-07-22 11:18:06 +02:00
vabene1111
b30f8c245e added option to set URL on food 2023-07-22 09:12:45 +02:00
vabene1111
74c86f1b6b Merge pull request #2541 from titilambert/patch-1
Expose food description in food form
2023-07-22 08:28:39 +02:00
smilerz
cf9d599536 fixed sort by rating so that unrated are always last 2023-07-20 15:39:35 -05:00
vabene1111
14a67fd6c2 improved spinner rendering 2023-07-20 16:24:25 +02:00
smilerz
19f1225249 make automation parameters case insensitive on search 2023-07-19 16:43:39 -05:00
smilerz
7f33f82b60 fixed defect in NEVER_UNIT automation 2023-07-19 16:42:37 -05:00
smilerz
6880c0a967 filtered automations to tokens present 2023-07-19 16:42:37 -05:00
smilerz
814f4157db create Transpose Words automation 2023-07-19 16:42:36 -05:00
smilerz
0f5e53526e add NEVER_UNIT automation 2023-07-19 16:42:04 -05:00
vabene1111
413da01c5c Merge pull request #2554 from TandoorRecipes/dependabot/npm_and_yarn/vue/word-wrap-1.2.4
Bump word-wrap from 1.2.3 to 1.2.4 in /vue
2023-07-19 09:05:55 +02:00
dependabot[bot]
a73d231bd4 Bump word-wrap from 1.2.3 to 1.2.4 in /vue
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-19 07:05:07 +00:00
vabene1111
4f2392faac updated pyyaml to be compatible with cython 3 2023-07-19 09:04:01 +02:00
vabene1111
2321dcec6c Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-07-18 16:40:42 +02:00
vabene1111
c2cf7ba758 fixed test 2023-07-18 16:40:38 +02:00
vabene1111
239dd4aa60 Merge pull request #2481 from TandoorRecipes/dependabot/pip/pytube-15.0.0
Bump pytube from 12.1.0 to 15.0.0
2023-07-18 15:35:04 +02:00
vabene1111
a653b2e777 Merge pull request #2525 from TandoorRecipes/dependabot/pip/whitenoise-6.5.0
Bump whitenoise from 6.2.0 to 6.5.0
2023-07-18 15:34:52 +02:00
vabene1111
d8faee7e93 Merge pull request #2545 from TandoorRecipes/dependabot/pip/cryptography-41.0.2
Bump cryptography from 41.0.0 to 41.0.2
2023-07-18 15:32:29 +02:00
vabene1111
69417425e9 Merge branch 'develop' into dependabot/pip/cryptography-41.0.2 2023-07-18 15:32:23 +02:00
vabene1111
e8574a49a7 Merge pull request #2544 from TandoorRecipes/dependabot/npm_and_yarn/vue/semver-5.7.2
Bump semver from 5.7.1 to 5.7.2 in /vue
2023-07-18 15:31:59 +02:00
vabene1111
fe624cd218 Merge pull request #2536 from TandoorRecipes/dependabot/pip/django-4.1.10
Bump django from 4.1.9 to 4.1.10
2023-07-18 15:31:34 +02:00
vabene1111
1f10a66c74 added base unit to unit editor 2023-07-18 13:54:35 +02:00
vabene1111
a8f1cd26cd change guest recipe permission 2023-07-18 10:54:20 +02:00
vabene1111
a497a6b7f5 space api read for all users in space 2023-07-15 13:57:25 +02:00
dependabot[bot]
9dc144f2b5 Bump cryptography from 41.0.0 to 41.0.2
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.0 to 41.0.2.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.0...41.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-15 01:23:46 +00:00
Eirik Skarding
7d50f3cf21 Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.9% (344 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nb_NO/
2023-07-12 21:19:57 +00:00
dependabot[bot]
315af4911c Bump semver from 5.7.1 to 5.7.2 in /vue
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-11 17:57:36 +00:00
vabene1111
35704c69c7 added option to pass recipe to recipe view 2023-07-11 17:50:48 +02:00
vabene1111
a24628c771 fixed userspace tetsts 2023-07-11 17:25:43 +02:00
vabene1111
e9748a160a addded paginated user space endpoint 2023-07-11 17:01:56 +02:00
Thibault Cohen
7bc78e104f Expose food description in food form 2023-07-10 21:26:26 -04:00
Mára Štěpánek
6f0dccfec9 Translated using Weblate (Czech)
Currently translated at 97.5% (487 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2023-07-06 21:19:59 +00:00
Rubens
76d6981dab Translated using Weblate (Catalan)
Currently translated at 85.1% (417 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/ca/
2023-07-06 21:19:59 +00:00
dependabot[bot]
5df37c52dd Bump django from 4.1.9 to 4.1.10
Bumps [django](https://github.com/django/django) from 4.1.9 to 4.1.10.
- [Commits](https://github.com/django/django/compare/4.1.9...4.1.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-06 00:10:14 +00:00
dependabot[bot]
c2def3eb9d Bump typescript from 4.9.5 to 5.1.6 in /vue
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.1.6.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-01 00:09:18 +00:00
dependabot[bot]
ad7ebf1cd5 Bump whitenoise from 6.2.0 to 6.5.0
Bumps [whitenoise](https://github.com/evansd/whitenoise) from 6.2.0 to 6.5.0.
- [Changelog](https://github.com/evansd/whitenoise/blob/main/docs/changelog.rst)
- [Commits](https://github.com/evansd/whitenoise/compare/6.2.0...6.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-01 00:03:23 +00:00
AquaticLava
ac17b84a7a updated auto meal plan to start at the current day, and exclude a meal plan if it has no keywords. Added debug buttons to help with testing. 2023-06-21 19:35:48 -06:00
AquaticLava
9756b7b653 regenerated open api file 2023-06-21 19:32:54 -06:00
AquaticLava
ee38d93e3b Created auto meal plan api endpoint. 2023-06-21 19:31:49 -06:00
AquaticLava
ee5c7d0ef4 Merge branch 'TandoorRecipes:develop' into Auto-Planner 2023-06-21 19:16:49 -06:00
dependabot[bot]
991a51d55e Bump pytube from 12.1.0 to 15.0.0
Bumps [pytube](https://github.com/pytube/pytube) from 12.1.0 to 15.0.0.
- [Release notes](https://github.com/pytube/pytube/releases)
- [Commits](https://github.com/pytube/pytube/compare/v12.1.0...v15.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-01 00:57:43 +00:00
AquaticLava
6c9227faac fixed formatting and minor bug causeing the start of the period to always be the current day. 2023-05-18 11:14:59 -06:00
AquaticLava
693b43af2e Merge remote-tracking branch 'origin/develop' into Auto-Planner
# Conflicts:
#	vue/src/apps/MealPlanView/MealPlanView.vue
2023-05-17 21:22:26 -06:00
dependabot[bot]
4fb5ce550e Bump bleach from 5.0.1 to 6.0.0
Bumps [bleach](https://github.com/mozilla/bleach) from 5.0.1 to 6.0.0.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/main/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v5.0.1...v6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 00:58:35 +00:00
AquaticLava
4a390b5824 removed logging 2023-01-08 12:01:59 -07:00
AquaticLava
785dc15cd9 Merge branch 'TandoorRecipes:develop' into Auto-Planner 2023-01-05 16:27:27 -07:00
AquaticLava
31f3425354 Menu for auto planner, menu sets auto planner settings. delete method no longer deletes all records for testing the auto planner. 2023-01-05 16:25:42 -07:00
AquaticLava
689eb426ea method for asynchronous generation of meals. start of menu for auto planner. delete method deletes all records for testing the auto planner. 2022-09-04 16:31:28 -06:00
113 changed files with 3528 additions and 3784 deletions

View File

@@ -3,7 +3,6 @@ npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE

View File

@@ -100,10 +100,12 @@ GUNICORN_MEDIA=0
# prefix used for account related emails (default "[Tandoor Recipes] ")
# ACCOUNT_EMAIL_SUBJECT_PREFIX=
# allow authentication via reverse proxy (e.g. authelia), leave off if you dont know what you are doing
# see docs for more information https://docs.tandoor.dev/features/authentication/
# 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)
REVERSE_PROXY_AUTH=0
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

View File

@@ -34,16 +34,6 @@ jobs:
echo VERSION=develop >> $GITHUB_OUTPUT
fi
# Update Version number
- name: Update version file
uses: DamianReeves/write-file-action@v1.2
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}-open-data'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
# clone open data plugin
- name: clone open data plugin repo
uses: actions/checkout@master

View File

@@ -34,16 +34,6 @@ jobs:
echo VERSION=develop >> $GITHUB_OUTPUT
fi
# Update Version number
- name: Update version file
uses: DamianReeves/write-file-action@v1.2
with:
path: recipes/version.py
contents: |
VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}'
BUILD_REF = '${{ github.sha }}'
write-mode: overwrite
# Build Vue frontend
- uses: actions/setup-node@v3
with:

2
.idea/vcs.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,7 +1,7 @@
FROM python:3.10-alpine3.18
#Install all dependencies.
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev openldap git
#Print all logs without buffering it.
ENV PYTHONUNBUFFERED 1
@@ -19,7 +19,7 @@ RUN \
if [ `apk --print-arch` = "armv7" ]; then \
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
fi
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev git && \
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev && \
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
@@ -30,5 +30,11 @@ RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-de
#Copy project and execute it.
COPY . ./
# collect information from git repositories
RUN /opt/recipes/venv/bin/python version.py
# delete git repositories to reduce image size
RUN find . -type d -name ".git" | xargs rm -rf
RUN chmod +x boot.sh
ENTRYPOINT ["/opt/recipes/boot.sh"]

View File

@@ -314,6 +314,7 @@ admin.site.register(InviteLink, InviteLinkAdmin)
class CookLogAdmin(admin.ModelAdmin):
list_display = ('recipe', 'created_by', 'created_at', 'rating', 'servings')
search_fields = ('recipe__name', 'space__name',)
admin.site.register(CookLog, CookLogAdmin)

View File

@@ -46,6 +46,7 @@ class UserPreferenceForm(forms.ModelForm):
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 = {
@@ -60,7 +61,8 @@ class UserPreferenceForm(forms.ModelForm):
'ingredient_decimals': _('Ingredient decimal places'),
'shopping_auto_sync': _('Shopping list auto sync period'),
'comments': _('Comments'),
'left_handed': _('Left-handed mode')
'left_handed': _('Left-handed mode'),
'show_step_ingredients': _('Show step ingredients table')
}
help_texts = {
@@ -82,7 +84,8 @@ class UserPreferenceForm(forms.ModelForm):
'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.')
'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 = {

View File

@@ -3,8 +3,10 @@ import string
import unicodedata
from django.core.cache import caches
from django.db.models import Q
from django.db.models.functions import Lower
from cookbook.models import Unit, Food, Automation, Ingredient
from cookbook.models import Automation, Food, Ingredient, Unit
class IngredientParser:
@@ -12,6 +14,8 @@ class IngredientParser:
ignore_rules = False
food_aliases = {}
unit_aliases = {}
never_unit = {}
transpose_words = {}
def __init__(self, request, cache_mode, ignore_automations=False):
"""
@@ -29,7 +33,7 @@ class IngredientParser:
caches['default'].touch(FOOD_CACHE_KEY, 30)
else:
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').order_by('order').all():
self.food_aliases[a.param_1] = a.param_2
self.food_aliases[a.param_1.lower()] = a.param_2
caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
UNIT_CACHE_KEY = f'automation_unit_alias_{self.request.space.pk}'
@@ -38,11 +42,33 @@ class IngredientParser:
caches['default'].touch(UNIT_CACHE_KEY, 30)
else:
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all():
self.unit_aliases[a.param_1] = a.param_2
self.unit_aliases[a.param_1.lower()] = a.param_2
caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
NEVER_UNIT_CACHE_KEY = f'automation_never_unit_{self.request.space.pk}'
if c := caches['default'].get(NEVER_UNIT_CACHE_KEY, None):
self.never_unit = c
caches['default'].touch(NEVER_UNIT_CACHE_KEY, 30)
else:
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.NEVER_UNIT).only('param_1', 'param_2').order_by('order').all():
self.never_unit[a.param_1.lower()] = a.param_2
caches['default'].set(NEVER_UNIT_CACHE_KEY, self.never_unit, 30)
TRANSPOSE_WORDS_CACHE_KEY = f'automation_transpose_words_{self.request.space.pk}'
if c := caches['default'].get(TRANSPOSE_WORDS_CACHE_KEY, None):
self.transpose_words = c
caches['default'].touch(TRANSPOSE_WORDS_CACHE_KEY, 30)
else:
i = 0
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.TRANSPOSE_WORDS).only('param_1', 'param_2').order_by('order').all():
self.transpose_words[i] = [a.param_1.lower(), a.param_2.lower()]
i += 1
caches['default'].set(TRANSPOSE_WORDS_CACHE_KEY, self.transpose_words, 30)
else:
self.food_aliases = {}
self.unit_aliases = {}
self.never_unit = {}
self.transpose_words = {}
def apply_food_automation(self, food):
"""
@@ -55,11 +81,11 @@ class IngredientParser:
else:
if self.food_aliases:
try:
return self.food_aliases[food]
return self.food_aliases[food.lower()]
except KeyError:
return food
else:
if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1=food, disabled=False).order_by('order').first():
if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1__iexact=food, disabled=False).order_by('order').first():
return automation.param_2
return food
@@ -72,13 +98,13 @@ class IngredientParser:
if self.ignore_rules:
return unit
else:
if self.unit_aliases:
if self.transpose_words:
try:
return self.unit_aliases[unit]
return self.unit_aliases[unit.lower()]
except KeyError:
return unit
else:
if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1=unit, disabled=False).order_by('order').first():
if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1__iexact=unit, disabled=False).order_by('order').first():
return automation.param_2
return unit
@@ -133,10 +159,10 @@ class IngredientParser:
end = 0
while (end < len(x) and (x[end] in string.digits
or (
(x[end] == '.' or x[end] == ',' or x[end] == '/')
and end + 1 < len(x)
and x[end + 1] in string.digits
))):
(x[end] == '.' or x[end] == ',' or x[end] == '/')
and end + 1 < len(x)
and x[end + 1] in string.digits
))):
end += 1
if end > 0:
if "/" in x[:end]:
@@ -160,7 +186,8 @@ class IngredientParser:
if unit is not None and unit.strip() == '':
unit = None
if unit is not None and (unit.startswith('(') or unit.startswith('-')): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
if unit is not None and (unit.startswith('(') or unit.startswith(
'-')): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
unit = None
note = x
return amount, unit, note
@@ -205,6 +232,67 @@ class IngredientParser:
food, note = self.parse_food_with_comma(tokens)
return food, note
def apply_never_unit_automations(self, tokens):
"""
Moves a string that should never be treated as a unit to next token and optionally replaced with default unit
e.g. NEVER_UNIT: param1: egg, param2: None would modify ['1', 'egg', 'white'] to ['1', '', 'egg', 'white']
or NEVER_UNIT: param1: egg, param2: pcs would modify ['1', 'egg', 'yolk'] to ['1', 'pcs', 'egg', 'yolk']
:param1 string: string that should never be considered a unit, will be moved to token[2]
:param2 (optional) unit as string: will insert unit string into token[1]
:return: unit as string (possibly changed by automation)
"""
if self.ignore_rules:
return tokens
new_unit = None
alt_unit = self.apply_unit_automation(tokens[1])
never_unit = False
if self.never_unit:
try:
new_unit = self.never_unit[tokens[1].lower()]
never_unit = True
except KeyError:
return tokens
else:
if automation := Automation.objects.annotate(param_1_lower=Lower('param_1')).filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1_lower__in=[
tokens[1].lower(), alt_unit.lower()], disabled=False).order_by('order').first():
new_unit = automation.param_2
never_unit = True
if never_unit:
tokens.insert(1, new_unit)
return tokens
def apply_transpose_words_automations(self, ingredient):
"""
If two words (param_1 & param_2) are detected in sequence, swap their position in the ingredient string
:param 1: first word to detect
:param 2: second word to detect
return: new ingredient string
"""
if self.ignore_rules:
return ingredient
else:
tokens = [x.lower() for x in ingredient.replace(',', ' ').split()]
if self.transpose_words:
filtered_rules = {}
for key, value in self.transpose_words.items():
if value[0] in tokens and value[1] in tokens:
filtered_rules[key] = value
for k, v in filtered_rules.items():
ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient, flags=re.IGNORECASE)
else:
for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False) \
.annotate(param_1_lower=Lower('param_1'), param_2_lower=Lower('param_2')) \
.filter(Q(Q(param_1_lower__in=tokens) | Q(param_2_lower__in=tokens))).order_by('order'):
ingredient = re.sub(rf"\b({rule.param_1})\W*({rule.param_1})\b", r"\2 \1", ingredient, flags=re.IGNORECASE)
return ingredient
def parse(self, ingredient):
"""
Main parsing function, takes an ingredient string (e.g. '1 l Water') and extracts amount, unit, food, ...
@@ -230,8 +318,8 @@ class IngredientParser:
# if the string contains parenthesis early on remove it and place it at the end
# because its likely some kind of note
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', ingredient):
match = re.search('\((.[^\(])+\)', ingredient)
if re.match('(.){1,6}\\s\\((.[^\\(\\)])+\\)\\s', ingredient):
match = re.search('\\((.[^\\(])+\\)', ingredient)
ingredient = ingredient[:match.start()] + ingredient[match.end():] + ' ' + ingredient[match.start():match.end()]
# leading spaces before commas result in extra tokens, clean them out
@@ -239,12 +327,14 @@ class IngredientParser:
# handle "(from) - (to)" amounts by using the minimum amount and adding the range to the description
# "10.5 - 200 g XYZ" => "100 g XYZ (10.5 - 200)"
ingredient = re.sub("^(\d+|\d+[\\.,]\d+) - (\d+|\d+[\\.,]\d+) (.*)", "\\1 \\3 (\\1 - \\2)", ingredient)
ingredient = re.sub("^(\\d+|\\d+[\\.,]\\d+) - (\\d+|\\d+[\\.,]\\d+) (.*)", "\\1 \\3 (\\1 - \\2)", ingredient)
# if amount and unit are connected add space in between
if re.match('([0-9])+([A-z])+\s', ingredient):
if re.match('([0-9])+([A-z])+\\s', ingredient):
ingredient = re.sub(r'(?<=([a-z])|\d)(?=(?(1)\d|[a-z]))', ' ', ingredient)
ingredient = self.apply_transpose_words_automations(ingredient)
tokens = ingredient.split() # split at each space into tokens
if len(tokens) == 1:
# there only is one argument, that must be the food
@@ -257,6 +347,7 @@ class IngredientParser:
# three arguments if it already has a unit there can't be
# a fraction for the amount
if len(tokens) > 2:
tokens = self.apply_never_unit_automations(tokens)
try:
if unit is not None:
# a unit is already found, no need to try the second argument for a fraction

View File

@@ -322,7 +322,7 @@ class CustomRecipePermission(permissions.BasePermission):
def has_permission(self, request, view): # user is either at least a guest or a share link is given and the request is safe
share = request.query_params.get('share', None)
return has_group_permission(request.user, ['guest']) or (share and request.method in SAFE_METHODS and 'pk' in view.kwargs)
return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS) or has_group_permission(request.user, ['user'])) or (share and request.method in SAFE_METHODS and 'pk' in view.kwargs)
def has_object_permission(self, request, view, obj):
share = request.query_params.get('share', None)
@@ -332,7 +332,7 @@ class CustomRecipePermission(permissions.BasePermission):
if obj.private:
return ((obj.created_by == request.user) or (request.user in obj.shared.all())) and obj.space == request.space
else:
return has_group_permission(request.user, ['guest']) and obj.space == request.space
return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS) or has_group_permission(request.user, ['user'])) and obj.space == request.space
class CustomUserPermission(permissions.BasePermission):

View File

@@ -34,7 +34,7 @@ class FoodPropertyHelper:
caches['default'].set(CacheHelper(self.space).PROPERTY_TYPE_CACHE_KEY, property_types, 60 * 60) # cache is cleared on property type save signal so long duration is fine
for fpt in property_types:
computed_properties[fpt.id] = {'id': fpt.id, 'name': fpt.name, 'icon': fpt.icon, 'description': fpt.description, 'unit': fpt.unit, 'food_values': {}, 'total_value': 0, 'missing_value': False}
computed_properties[fpt.id] = {'id': fpt.id, 'name': fpt.name, 'icon': fpt.icon, 'description': fpt.description, 'unit': fpt.unit, 'order': fpt.order, 'food_values': {}, 'total_value': 0, 'missing_value': False}
uch = UnitConversionHelper(self.space)

View File

@@ -32,6 +32,9 @@ class RecipeSearch():
if custom_filter:
self._params = {**json.loads(custom_filter.search)}
self._original_params = {**(params or {})}
# json.loads casts rating as an integer, expecting string
if isinstance(self._params.get('rating', None), int):
self._params['rating'] = str(self._params['rating'])
else:
self._params = {**(params or {})}
else:
@@ -85,9 +88,9 @@ class RecipeSearch():
self._viewedon = self._params.get('viewedon', None)
self._makenow = self._params.get('makenow', None)
# this supports hidden feature to find recipes missing X ingredients
if type(self._makenow) == bool and self._makenow == True:
if isinstance(self._makenow, bool) and self._makenow == True:
self._makenow = 0
elif type(self._makenow) == str and self._makenow in ["yes", "true"]:
elif isinstance(self._makenow, str) and self._makenow in ["yes", "true"]:
self._makenow = 0
else:
try:
@@ -150,7 +153,7 @@ class RecipeSearch():
self.unit_filters(units=self._units)
self._makenow_filter(missing=self._makenow)
self.string_filters(string=self._string)
return self._queryset.filter(space=self._request.space).distinct().order_by(*self.orderby)
return self._queryset.filter(space=self._request.space).order_by(*self.orderby)
def _sort_includes(self, *args):
for x in args:
@@ -434,22 +437,21 @@ class RecipeSearch():
def rating_filter(self, rating=None):
if rating or self._sort_includes('rating'):
lessthan = self._sort_includes('-rating') or '-' in (rating or [])
if lessthan:
lessthan = '-' in (rating or [])
reverse = 'rating' in (self._sort_order or []) and '-rating' not in (self._sort_order or [])
if lessthan or reverse:
default = 100
else:
default = 0
# TODO make ratings a settings user-only vs all-users
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(
cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
self._queryset = self._queryset.annotate(rating=Round(Avg(Case(When(cooklog__created_by=self._request.user, then='cooklog__rating'), default=default))))
if rating is None:
return
if rating == '0':
self._queryset = self._queryset.filter(rating=0)
elif lessthan:
self._queryset = self._queryset.filter(
rating__lte=int(rating[1:])).exclude(rating=0)
self._queryset = self._queryset.filter(rating__lte=int(rating[1:])).exclude(rating=0)
else:
self._queryset = self._queryset.filter(rating__gte=int(rating))
@@ -560,7 +562,7 @@ class RecipeSearch():
self._filters += [Q(pk__in=self._fuzzy_match.values('pk'))]
def _makenow_filter(self, missing=None):
if missing is None or (type(missing) == bool and missing == False):
if missing is None or (isinstance(missing, bool) and missing == False):
return
shopping_users = [
*self._request.user.get_shopping_share(), self._request.user]

View File

@@ -15,7 +15,6 @@ from recipe_scrapers._utils import get_host_name, get_minutes
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.models import Automation, Keyword, PropertyType
# from unicodedata import decomposition
@@ -51,7 +50,8 @@ def get_from_scraper(scrape, request):
recipe_json['internal'] = True
try:
servings = scrape.schema.data.get('recipeYield') or 1 # dont use scrape.yields() as this will always return "x servings" or "x items", should be improved in scrapers directly
# dont use scrape.yields() as this will always return "x servings" or "x items", should be improved in scrapers directly
servings = scrape.schema.data.get('recipeYield') or 1
except Exception:
servings = 1
@@ -147,7 +147,7 @@ def get_from_scraper(scrape, request):
recipe_json['steps'] = []
try:
for i in parse_instructions(scrape.instructions()):
recipe_json['steps'].append({'instruction': i, 'ingredients': [], })
recipe_json['steps'].append({'instruction': i, 'ingredients': [], 'show_ingredients_table': request.user.userpreference.show_step_ingredients,})
except Exception:
pass
if len(recipe_json['steps']) == 0:
@@ -156,7 +156,14 @@ def get_from_scraper(scrape, request):
parsed_description = parse_description(description)
# TODO notify user about limit if reached
# limits exist to limit the attack surface for dos style attacks
automations = Automation.objects.filter(type=Automation.DESCRIPTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').all().order_by('order')[:512]
automations = Automation.objects.filter(
type=Automation.DESCRIPTION_REPLACE,
space=request.space,
disabled=False).only(
'param_1',
'param_2',
'param_3').all().order_by('order')[
:512]
for a in automations:
if re.match(a.param_1, (recipe_json['source_url'])[:512]):
parsed_description = re.sub(a.param_2, a.param_3, parsed_description, count=1)
@@ -206,7 +213,14 @@ def get_from_scraper(scrape, request):
pass
if 'source_url' in recipe_json and recipe_json['source_url']:
automations = Automation.objects.filter(type=Automation.INSTRUCTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').order_by('order').all()[:512]
automations = Automation.objects.filter(
type=Automation.INSTRUCTION_REPLACE,
space=request.space,
disabled=False).only(
'param_1',
'param_2',
'param_3').order_by('order').all()[
:512]
for a in automations:
if re.match(a.param_1, (recipe_json['source_url'])[:512]):
for s in recipe_json['steps']:
@@ -272,7 +286,7 @@ def get_from_youtube_scraper(url, request):
def parse_name(name):
if type(name) == list:
if isinstance(name, list):
try:
name = name[0]
except Exception:
@@ -316,16 +330,16 @@ def parse_instructions(instructions):
"""
instruction_list = []
if type(instructions) == list:
if isinstance(instructions, list):
for i in instructions:
if type(i) == str:
if isinstance(i, str):
instruction_list.append(clean_instruction_string(i))
else:
if 'text' in i:
instruction_list.append(clean_instruction_string(i['text']))
elif 'itemListElement' in i:
for ile in i['itemListElement']:
if type(ile) == str:
if isinstance(ile, str):
instruction_list.append(clean_instruction_string(ile))
elif 'text' in ile:
instruction_list.append(clean_instruction_string(ile['text']))
@@ -341,13 +355,13 @@ def parse_image(image):
# check if list of images is returned, take first if so
if not image:
return None
if type(image) == list:
if isinstance(image, list):
for pic in image:
if (type(pic) == str) and (pic[:4] == 'http'):
if (isinstance(pic, str)) and (pic[:4] == 'http'):
image = pic
elif 'url' in pic:
image = pic['url']
elif type(image) == dict:
elif isinstance(image, dict):
if 'url' in image:
image = image['url']
@@ -358,12 +372,12 @@ def parse_image(image):
def parse_servings(servings):
if type(servings) == str:
if isinstance(servings, str):
try:
servings = int(re.search(r'\d+', servings).group())
except AttributeError:
servings = 1
elif type(servings) == list:
elif isinstance(servings, list):
try:
servings = int(re.findall(r'\b\d+\b', servings[0])[0])
except KeyError:
@@ -372,12 +386,12 @@ def parse_servings(servings):
def parse_servings_text(servings):
if type(servings) == str:
if isinstance(servings, str):
try:
servings = re.sub("\d+", '', servings).strip()
servings = re.sub("\\d+", '', servings).strip()
except Exception:
servings = ''
if type(servings) == list:
if isinstance(servings, list):
try:
servings = parse_servings_text(servings[1])
except Exception:
@@ -394,7 +408,7 @@ def parse_time(recipe_time):
recipe_time = round(iso_parse_duration(recipe_time).seconds / 60)
except ISO8601Error:
try:
if (type(recipe_time) == list and len(recipe_time) > 0):
if (isinstance(recipe_time, list) and len(recipe_time) > 0):
recipe_time = recipe_time[0]
recipe_time = round(parse_duration(recipe_time).seconds / 60)
except AttributeError:
@@ -413,7 +427,7 @@ def parse_keywords(keyword_json, space):
caches['default'].touch(KEYWORD_CACHE_KEY, 30)
else:
for a in Automation.objects.filter(space=space, disabled=False, type=Automation.KEYWORD_ALIAS).only('param_1', 'param_2').order_by('order').all():
keyword_aliases[a.param_1] = a.param_2
keyword_aliases[a.param_1.lower()] = a.param_2
caches['default'].set(KEYWORD_CACHE_KEY, keyword_aliases, 30)
# keywords as list
@@ -424,7 +438,7 @@ def parse_keywords(keyword_json, space):
if len(kw) != 0:
if keyword_aliases:
try:
kw = keyword_aliases[kw]
kw = keyword_aliases[kw.lower()]
except KeyError:
pass
if k := Keyword.objects.filter(name=kw, space=space).first():
@@ -438,15 +452,15 @@ def parse_keywords(keyword_json, space):
def listify_keywords(keyword_list):
# keywords as string
try:
if type(keyword_list[0]) == dict:
if isinstance(keyword_list[0], dict):
return keyword_list
except (KeyError, IndexError):
pass
if type(keyword_list) == str:
if isinstance(keyword_list, str):
keyword_list = keyword_list.split(',')
# keywords as string in list
if (type(keyword_list) == list and len(keyword_list) == 1 and ',' in keyword_list[0]):
if (isinstance(keyword_list, list) and len(keyword_list) == 1 and ',' in keyword_list[0]):
keyword_list = keyword_list[0].split(',')
return [x.strip() for x in keyword_list]
@@ -500,13 +514,13 @@ def get_images_from_soup(soup, url):
def clean_dict(input_dict, key):
if type(input_dict) == dict:
if isinstance(input_dict, dict):
for x in list(input_dict):
if x == key:
del input_dict[x]
elif type(input_dict[x]) == dict:
elif isinstance(input_dict[x], dict):
input_dict[x] = clean_dict(input_dict[x], key)
elif type(input_dict[x]) == list:
elif isinstance(input_dict[x], list):
temp_list = []
for e in input_dict[x]:
temp_list.append(clean_dict(e, key))

View File

@@ -2,7 +2,6 @@ from gettext import gettext as _
import bleach
import markdown as md
from bleach_allowlist import markdown_attrs, markdown_tags
from jinja2 import Template, TemplateSyntaxError, UndefinedError
from markdown.extensions.tables import TableExtension
@@ -53,9 +52,17 @@ class IngredientObject(object):
def render_instructions(step): # TODO deduplicate markdown cleanup code
instructions = step.instruction
tags = markdown_tags + [
'pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead', 'img'
]
tags = {
"h1", "h2", "h3", "h4", "h5", "h6",
"b", "i", "strong", "em", "tt",
"p", "br",
"span", "div", "blockquote", "code", "pre", "hr",
"ul", "ol", "li", "dd", "dt",
"img",
"a",
"sub", "sup",
'pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead'
}
parsed_md = md.markdown(
instructions,
extensions=[
@@ -63,7 +70,11 @@ def render_instructions(step): # TODO deduplicate markdown cleanup code
UrlizeExtension(), MarkdownFormatExtension()
]
)
markdown_attrs['*'] = markdown_attrs['*'] + ['class', 'width', 'height']
markdown_attrs = {
"*": ["id", "class", 'width', 'height'],
"img": ["src", "alt", "title"],
"a": ["href", "alt", "title"],
}
instructions = bleach.clean(parsed_md, tags, markdown_attrs)

View File

@@ -36,7 +36,7 @@ class ChefTap(Integration):
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space, )
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space,)
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,)
if source_url != '':
step.instruction += '\n' + source_url

View File

@@ -55,7 +55,7 @@ class Chowdown(Integration):
recipe.keywords.add(keyword)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions), space=self.request.space,
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions), space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
ingredient_parser = IngredientParser(self.request, True)

View File

@@ -47,7 +47,7 @@ class CookBookApp(Integration):
pass
# assuming import files only contain single step
step = Step.objects.create(instruction=recipe_json['steps'][0]['instruction'], space=self.request.space, )
step = Step.objects.create(instruction=recipe_json['steps'][0]['instruction'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
if 'nutrition' in recipe_json:
step.instruction = step.instruction + '\n\n' + recipe_json['nutrition']

View File

@@ -50,7 +50,7 @@ class Cookmate(Integration):
for step in recipe_text.getchildren():
if step.text:
step = Step.objects.create(
instruction=step.text.strip(), space=self.request.space,
instruction=step.text.strip(), space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
recipe.steps.add(step)

View File

@@ -51,7 +51,7 @@ class CopyMeThat(Integration):
except AttributeError:
pass
step = Step.objects.create(instruction='', space=self.request.space, )
step = Step.objects.create(instruction='', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
ingredient_parser = IngredientParser(self.request, True)

View File

@@ -28,7 +28,7 @@ class Domestica(Integration):
recipe.save()
step = Step.objects.create(
instruction=file['directions'], space=self.request.space,
instruction=file['directions'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
if file['source'] != '':

View File

@@ -25,7 +25,7 @@ class Mealie(Integration):
created_by=self.request.user, internal=True, space=self.request.space)
for s in recipe_json['recipe_instructions']:
step = Step.objects.create(instruction=s['text'], space=self.request.space, )
step = Step.objects.create(instruction=s['text'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
recipe.steps.add(step)
step = recipe.steps.first()

View File

@@ -39,7 +39,7 @@ class MealMaster(Integration):
recipe.keywords.add(keyword)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
instruction='\n'.join(directions) + '\n\n', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
ingredient_parser = IngredientParser(self.request, True)

View File

@@ -67,7 +67,7 @@ class MelaRecipes(Integration):
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
))
recipe.steps.add(step)

View File

@@ -54,11 +54,11 @@ class NextcloudCookbook(Integration):
instruction_text = ''
if 'text' in s:
step = Step.objects.create(
instruction=s['text'], name=s['name'], space=self.request.space,
instruction=s['text'], name=s['name'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
else:
step = Step.objects.create(
instruction=s, space=self.request.space,
instruction=s, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
if not ingredients_added:
if len(recipe_json['description'].strip()) > 500:

View File

@@ -51,7 +51,7 @@ class OpenEats(Integration):
recipe.image = f'recipes/openeats-import/{file["photo"]}'
recipe.save()
step = Step.objects.create(instruction=instructions, space=self.request.space,)
step = Step.objects.create(instruction=instructions, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file['ingredients']:

View File

@@ -58,7 +58,7 @@ class Paprika(Integration):
pass
step = Step.objects.create(
instruction=instructions, space=self.request.space,
instruction=instructions, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
if 'description' in recipe_json and len(recipe_json['description'].strip()) > 500:

View File

@@ -35,7 +35,7 @@ class Pepperplate(Integration):
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
instruction='\n'.join(directions) + '\n\n', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
ingredient_parser = IngredientParser(self.request, True)

View File

@@ -46,7 +46,7 @@ class Plantoeat(Integration):
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
instruction='\n'.join(directions) + '\n\n', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
if tags:

View File

@@ -46,7 +46,7 @@ class RecetteTek(Integration):
if not instructions:
instructions = ''
step = Step.objects.create(instruction=instructions, space=self.request.space,)
step = Step.objects.create(instruction=instructions, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,)
# Append the original import url to the step (if it exists)
try:

View File

@@ -41,7 +41,7 @@ class RecipeKeeper(Integration):
except AttributeError:
pass
step = Step.objects.create(instruction='', space=self.request.space, )
step = Step.objects.create(instruction='', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"):

View File

@@ -49,7 +49,7 @@ class RecipeSage(Integration):
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
))
recipe.steps.add(step)

View File

@@ -37,7 +37,7 @@ class Rezeptsuitede(Integration):
try:
if prep.find('step').text:
step = Step.objects.create(
instruction=prep.find('step').text.strip(), space=self.request.space,
instruction=prep.find('step').text.strip(), space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
recipe.steps.add(step)
except Exception:

View File

@@ -38,7 +38,7 @@ class RezKonv(Integration):
recipe.keywords.add(keyword)
step = Step.objects.create(
instruction=' \n'.join(directions) + '\n\n', space=self.request.space,
instruction=' \n'.join(directions) + '\n\n', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
ingredient_parser = IngredientParser(self.request, True)

View File

@@ -43,7 +43,7 @@ class Saffron(Integration):
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space, )
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space, )
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients, )
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:

View File

@@ -13,8 +13,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-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"PO-Revision-Date: 2023-07-06 21:19+0000\n"
"Last-Translator: Rubens <rubenixnagios@gmail.com>\n"
"Language-Team: Catalan <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/ca/>\n"
"Language: ca\n"
@@ -421,7 +421,7 @@ msgstr "Compartir Llista de la Compra"
#: .\cookbook\forms.py:525
msgid "Autosync"
msgstr "Autosync"
msgstr "Autosinc"
#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
@@ -477,7 +477,7 @@ msgstr "Mostra el recompte de receptes als filtres de cerca"
#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""
msgstr "Empra el plural d'aquestes unitats i menjars dins de l'espai."
#: .\cookbook\helper\AllAuthCustomAdapter.py:39
msgid ""

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-03-25 11:32+0000\n"
"Last-Translator: Matěj Kubla <matykubla@gmail.com>\n"
"PO-Revision-Date: 2023-07-31 14:19+0000\n"
"Last-Translator: Mára Štěpánek <stepanekm7@gmail.com>\n"
"Language-Team: Czech <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/cs/>\n"
"Language: cs\n"
@@ -36,7 +36,7 @@ msgid ""
"try them out!"
msgstr ""
"Barva horního navigačního menu. Některé barvy neladí se všemi tématy a je "
"třeba je vyzkoušet."
"třeba je vyzkoušet!"
#: .\cookbook\forms.py:45
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
@@ -50,7 +50,7 @@ msgid ""
"to fractions automatically)"
msgstr ""
"Povolit podporu zlomků u množství ingrediencí (desetinná čísla budou "
"automaticky převedena na zlomky)."
"automaticky převedena na zlomky)"
#: .\cookbook\forms.py:47
msgid ""

View File

@@ -15,8 +15,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-06-21 14:19+0000\n"
"Last-Translator: Tobias Huppertz <tobias.huppertz@mail.de>\n"
"PO-Revision-Date: 2023-08-13 08:19+0000\n"
"Last-Translator: Fabian Flodman <fabian@flodman.de>\n"
"Language-Team: German <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/de/>\n"
"Language: de\n"
@@ -1436,11 +1436,11 @@ msgid ""
" "
msgstr ""
"\n"
" <b>Password und Token</b> werden im <b>Klartext</b> in der Datenbank "
" <b>Passwort und Token</b> werden im <b>Klartext</b> in der Datenbank "
"gespeichert.\n"
" Dies ist notwendig da Passwort oder Token benötigt werden, um API-"
"Anfragen zu stellen, bringt jedoch auch ein Sicherheitsrisiko mit sich. <br/"
">\n"
"Anfragen zu stellen, bringt jedoch auch ein Sicherheitsrisiko mit sich. <br/>"
"\n"
" Um das Risiko zu minimieren sollten, wenn möglich, Tokens oder "
"Accounts mit limitiertem Zugriff verwendet werden.\n"
" "
@@ -2600,7 +2600,7 @@ msgstr "Ungültiges URL Schema."
#: .\cookbook\views\api.py:1233
msgid "No usable data could be found."
msgstr "Es konnten keine nutzbaren Daten gefunden werden."
msgstr "Es konnten keine passenden Daten gefunden werden."
#: .\cookbook\views\api.py:1326 .\cookbook\views\import_export.py:117
msgid "Importing is not implemented for this provider"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-12 19:20+0200\n"
"PO-Revision-Date: 2023-06-25 14:19+0000\n"
"Last-Translator: sweeney <sweeneytodd91@protonmail.com>\n"
"PO-Revision-Date: 2023-08-21 09:19+0000\n"
"Last-Translator: Theodoros Grammenos <teogramm@outlook.com>\n"
"Language-Team: Greek <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/el/>\n"
"Language: el\n"
@@ -22,7 +22,7 @@ msgstr ""
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
#: .\cookbook\templates\stats.html:28
msgid "Ingredients"
msgstr "Συστατικά"
msgstr "Υλικά"
#: .\cookbook\forms.py:53
msgid "Default unit"
@@ -66,7 +66,7 @@ msgstr "Κοινοποίηση προγράμματος"
#: .\cookbook\forms.py:63
msgid "Ingredient decimal places"
msgstr ""
msgstr "Δεκαδικά ψηφία υλικών"
#: .\cookbook\forms.py:64
msgid "Shopping list auto sync period"

File diff suppressed because it is too large Load Diff

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-05-26 16:19+0000\n"
"Last-Translator: Luis Cacho <luiscachog@gmail.com>\n"
"PO-Revision-Date: 2023-08-27 11:19+0000\n"
"Last-Translator: Matias Laporte <laportematias+weblate@gmail.com>\n"
"Language-Team: Spanish <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/es/>\n"
"Language: es\n"
@@ -543,19 +543,19 @@ msgstr ""
#: .\cookbook\helper\recipe_url_import.py:268
msgid "knead"
msgstr ""
msgstr "amasar"
#: .\cookbook\helper\recipe_url_import.py:269
msgid "thicken"
msgstr ""
msgstr "espesar"
#: .\cookbook\helper\recipe_url_import.py:270
msgid "warm up"
msgstr ""
msgstr "precalentar"
#: .\cookbook\helper\recipe_url_import.py:271
msgid "ferment"
msgstr ""
msgstr "fermentar"
#: .\cookbook\helper\recipe_url_import.py:272
msgid "sous-vide"
@@ -573,11 +573,11 @@ msgstr ""
#: .\cookbook\integration\copymethat.py:44
#: .\cookbook\integration\melarecipes.py:37
msgid "Favorite"
msgstr ""
msgstr "Favorito"
#: .\cookbook\integration\copymethat.py:50
msgid "I made this"
msgstr ""
msgstr "Lo he preparado"
#: .\cookbook\integration\integration.py:218
msgid ""
@@ -604,7 +604,7 @@ msgstr "Se importaron %s recetas."
#: .\cookbook\integration\openeats.py:26
msgid "Recipe source:"
msgstr "Recipe source:"
msgstr "Fuente de la receta:"
#: .\cookbook\integration\paprika.py:49
msgid "Notes"
@@ -645,19 +645,21 @@ msgstr "Sección"
#: .\cookbook\management\commands\rebuildindex.py:14
msgid "Rebuilds full text search index on Recipe"
msgstr ""
msgstr "Reconstruye el índice de búsqueda por texto completo de la receta"
#: .\cookbook\management\commands\rebuildindex.py:18
msgid "Only Postgresql databases use full text search, no index to rebuild"
msgstr ""
"Solo las bases de datos Postgresql utilizan la búsqueda por texto completo, "
"no hay índice para reconstruir"
#: .\cookbook\management\commands\rebuildindex.py:29
msgid "Recipe index rebuild complete."
msgstr ""
msgstr "Se reconstruyó el índice de la receta."
#: .\cookbook\management\commands\rebuildindex.py:31
msgid "Recipe index rebuild failed."
msgstr ""
msgstr "No fue posible reconstruir el índice de la receta."
#: .\cookbook\migrations\0047_auto_20200602_1133.py:14
msgid "Breakfast"
@@ -699,23 +701,23 @@ msgstr "Libros"
#: .\cookbook\models.py:580
msgid " is part of a recipe step and cannot be deleted"
msgstr ""
msgstr " es parte del paso de una receta y no puede ser eliminado"
#: .\cookbook\models.py:1181 .\cookbook\templates\search_info.html:28
msgid "Simple"
msgstr ""
msgstr "Simple"
#: .\cookbook\models.py:1182 .\cookbook\templates\search_info.html:33
msgid "Phrase"
msgstr ""
msgstr "Frase"
#: .\cookbook\models.py:1183 .\cookbook\templates\search_info.html:38
msgid "Web"
msgstr ""
msgstr "Web"
#: .\cookbook\models.py:1184 .\cookbook\templates\search_info.html:47
msgid "Raw"
msgstr ""
msgstr "Crudo"
#: .\cookbook\models.py:1231
msgid "Food Alias"
@@ -762,49 +764,53 @@ msgstr "Palabra clave"
#: .\cookbook\serializer.py:198
msgid "File uploads are not enabled for this Space."
msgstr ""
msgstr "Las cargas de archivo no están habilitadas para esta Instancia."
#: .\cookbook\serializer.py:209
msgid "You have reached your file upload limit."
msgstr ""
msgstr "Has alcanzado el límite de cargas de archivo."
#: .\cookbook\serializer.py:291
msgid "Cannot modify Space owner permission."
msgstr ""
msgstr "No puedes modificar los permisos del propietario de la Instancia."
#: .\cookbook\serializer.py:1093
msgid "Hello"
msgstr ""
msgstr "Hola"
#: .\cookbook\serializer.py:1093
msgid "You have been invited by "
msgstr ""
msgstr "Has sido invitado por: "
#: .\cookbook\serializer.py:1094
msgid " to join their Tandoor Recipes space "
msgstr ""
msgstr " para unirte a su instancia de Tandoor Recipes "
#: .\cookbook\serializer.py:1095
msgid "Click the following link to activate your account: "
msgstr ""
msgstr "Haz click en el siguiente enlace para activar tu cuenta: "
#: .\cookbook\serializer.py:1096
msgid ""
"If the link does not work use the following code to manually join the space: "
msgstr ""
"Si el enlace no funciona, utiliza el siguiente código para unirte "
"manualmente a la instancia: "
#: .\cookbook\serializer.py:1097
msgid "The invitation is valid until "
msgstr ""
msgstr "La invitación es válida hasta "
#: .\cookbook\serializer.py:1098
msgid ""
"Tandoor Recipes is an Open Source recipe manager. Check it out on GitHub "
msgstr ""
"Tandoor Recipes es un administrador de recetas Open Source. Dale una ojeada "
"en GitHub "
#: .\cookbook\serializer.py:1101
msgid "Tandoor Recipes Invite"
msgstr ""
msgstr "Invitación para Tandoor Recipes"
#: .\cookbook\serializer.py:1242
msgid "Existing shopping list to update"

View File

@@ -14,10 +14,10 @@ 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-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"Language-Team: French <http://translate.tandoor.dev/projects/tandoor/recipes-"
"backend/fr/>\n"
"PO-Revision-Date: 2023-08-16 21:19+0000\n"
"Last-Translator: Alexandre Braure <alex@tkclab.ca>\n"
"Language-Team: French <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -549,7 +549,7 @@ msgstr "Il est nécessaire de fournir soit le queryset, soit la clé de hachage"
#, fuzzy
#| msgid "Use fractions"
msgid "reverse rotation"
msgstr "Utiliser les fractions"
msgstr "sens inverse"
#: .\cookbook\helper\recipe_url_import.py:267
msgid "careful rotation"
@@ -620,10 +620,8 @@ msgid "Imported %s recipes."
msgstr "%s recettes importées."
#: .\cookbook\integration\openeats.py:26
#, fuzzy
#| msgid "Recipe Home"
msgid "Recipe source:"
msgstr "Page daccueil"
msgstr "Source de la recette :"
#: .\cookbook\integration\paprika.py:49
msgid "Notes"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-11 15:09+0200\n"
"PO-Revision-Date: 2023-04-17 20:55+0000\n"
"Last-Translator: Espen Sellevåg <buskmenn.drammer03@icloud.com>\n"
"PO-Revision-Date: 2023-08-19 21:36+0000\n"
"Last-Translator: NeoID <neoid@animenord.com>\n"
"Language-Team: Norwegian Bokmål <http://translate.tandoor.dev/projects/"
"tandoor/recipes-backend/nb_NO/>\n"
"Language: nb_NO\n"
@@ -31,6 +31,8 @@ msgid ""
"Color of the top navigation bar. Not all colors work with all themes, just "
"try them out!"
msgstr ""
"Farge på toppnavigasjonslinjen. Ikke alle farger fungerer med alle temaer, "
"så bare prøv dem ut!"
#: .\cookbook\forms.py:46
msgid "Default Unit to be used when inserting a new ingredient into a recipe."
@@ -79,13 +81,15 @@ msgstr ""
#: .\cookbook\forms.py:56
msgid "Makes the navbar stick to the top of the page."
msgstr ""
msgstr "Fest navigasjonslinjen til toppen av siden."
#: .\cookbook\forms.py:72
msgid ""
"Both fields are optional. If none are given the username will be displayed "
"instead"
msgstr ""
"Begge feltene er valgfrie. Hvis ingen blir oppgitt, vil brukernavnet vises i "
"stedet"
#: .\cookbook\forms.py:93 .\cookbook\forms.py:315
#: .\cookbook\templates\forms\edit_internal_recipe.html:45
@@ -97,15 +101,15 @@ msgstr "Navn"
#: .\cookbook\templates\forms\edit_internal_recipe.html:81
#: .\cookbook\templates\stats.html:24 .\cookbook\templates\url_import.html:202
msgid "Keywords"
msgstr ""
msgstr "Nøkkelord"
#: .\cookbook\forms.py:95
msgid "Preparation time in minutes"
msgstr ""
msgstr "Forberedelsestid i minutter"
#: .\cookbook\forms.py:96
msgid "Waiting time (cooking/baking) in minutes"
msgstr ""
msgstr "Ventetid (til matlaging/baking) i minutter"
#: .\cookbook\forms.py:97 .\cookbook\forms.py:317
msgid "Path"
@@ -124,6 +128,8 @@ msgid ""
"To prevent duplicates recipes with the same name as existing ones are "
"ignored. Check this box to import everything."
msgstr ""
"For å unngå duplikater, blir oppskrifter med samme navn som eksisterende "
"ignorert. Merk av denne boksen for å importere alt."
#: .\cookbook\forms.py:149
msgid "New Unit"
@@ -131,7 +137,7 @@ msgstr "Ny enhet"
#: .\cookbook\forms.py:150
msgid "New unit that other gets replaced by."
msgstr ""
msgstr "Ny enhet som erstatter den gamle."
#: .\cookbook\forms.py:155
msgid "Old Unit"
@@ -143,19 +149,19 @@ msgstr "Enhet som skal erstattes."
#: .\cookbook\forms.py:172
msgid "New Food"
msgstr ""
msgstr "Ny matvare"
#: .\cookbook\forms.py:173
msgid "New food that other gets replaced by."
msgstr ""
msgstr "Ny matvare som erstatter den gamle."
#: .\cookbook\forms.py:178
msgid "Old Food"
msgstr ""
msgstr "Gammel matvare"
#: .\cookbook\forms.py:179
msgid "Food that should be replaced."
msgstr ""
msgstr "Matvare som bør erstattes."
#: .\cookbook\forms.py:197
msgid "Add your comment: "
@@ -163,17 +169,19 @@ msgstr "Legg til din kommentar: "
#: .\cookbook\forms.py:238
msgid "Leave empty for dropbox and enter app password for nextcloud."
msgstr ""
msgstr "La det stå tomt for Dropbox og skriv inn app-passordet for Nextcloud."
#: .\cookbook\forms.py:245
msgid "Leave empty for nextcloud and enter api token for dropbox."
msgstr ""
msgstr "La det stå tomt for Nextcloud og skriv inn API-tokenet for Dropbox."
#: .\cookbook\forms.py:253
msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
msgstr ""
"La det stå tomt for Dropbox, og skriv bare inn grunn-URLen for Nextcloud "
"(<code>/remote.php/webdav/</code> blir lagt til automatisk)"
#: .\cookbook\forms.py:291
msgid "Search String"
@@ -185,11 +193,12 @@ msgstr "Fil-ID"
#: .\cookbook\forms.py:354
msgid "You must provide at least a recipe or a title."
msgstr ""
msgstr "Du må oppgi minst en oppskrift eller en tittel."
#: .\cookbook\forms.py:367
msgid "You can list default users to share recipes with in the settings."
msgstr ""
"Du kan liste opp standardbrukere for å dele oppskrifter innen innstillingene."
#: .\cookbook\forms.py:368
#: .\cookbook\templates\forms\edit_internal_recipe.html:377
@@ -197,10 +206,14 @@ msgid ""
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
"\">docs here</a>"
msgstr ""
"Du kan bruke Markdown for å formatere dette feltet. Se <a href=\"/docs/"
"markdown/\">dokumentasjonen her</a>"
#: .\cookbook\forms.py:393
msgid "A username is not required, if left blank the new user can choose one."
msgstr ""
"Et brukernavn er ikke påkrevd. Hvis det blir stående tomt, kan den nye "
"brukeren velge ett selv."
#: .\cookbook\helper\permission_helper.py:123
#: .\cookbook\helper\permission_helper.py:129
@@ -222,26 +235,30 @@ msgstr "Du er ikke innlogget og kan derfor ikke vise siden!"
#: .\cookbook\helper\permission_helper.py:167
#: .\cookbook\helper\permission_helper.py:182
msgid "You cannot interact with this object as it is not owned by you!"
msgstr ""
msgstr "Du kan ikke samhandle med dette objektet, da det ikke tilhører deg!"
#: .\cookbook\helper\recipe_url_import.py:40 .\cookbook\views\api.py:549
msgid "The requested site provided malformed data and cannot be read."
msgstr ""
"Nettstedet du har forespurt, har levert feilformatert data som ikke kan "
"leses."
#: .\cookbook\helper\recipe_url_import.py:54
msgid ""
"The requested site does not provide any recognized data format to import the "
"recipe from."
msgstr ""
"Det forespurte nettstedet gir ingen gjenkjennelig dataformat som kan "
"importeres oppskriften fra."
#: .\cookbook\helper\recipe_url_import.py:160
msgid "Imported from"
msgstr ""
msgstr "Importert fra"
#: .\cookbook\helper\template_helper.py:60
#: .\cookbook\helper\template_helper.py:62
msgid "Could not parse template code."
msgstr ""
msgstr "Kunne ikke analysere mal-koden."
#: .\cookbook\integration\integration.py:102
#: .\cookbook\templates\import.html:14 .\cookbook\templates\import.html:20
@@ -250,50 +267,52 @@ msgstr ""
#: .\cookbook\templates\url_import.html:233 .\cookbook\views\delete.py:60
#: .\cookbook\views\edit.py:190
msgid "Import"
msgstr ""
msgstr "Importér"
#: .\cookbook\integration\integration.py:131
msgid ""
"Importer expected a .zip file. Did you choose the correct importer type for "
"your data ?"
msgstr ""
"Importøren forventet en .zip-fil. Har du valgt riktig type importør for "
"dataene dine?"
#: .\cookbook\integration\integration.py:134
msgid "The following recipes were ignored because they already existed:"
msgstr ""
msgstr "Følgende oppskrifter ble ignorert fordi de allerede eksisterte:"
#: .\cookbook\integration\integration.py:137
#, python-format
msgid "Imported %s recipes."
msgstr ""
msgstr "Importerte %s oppskrifter."
#: .\cookbook\integration\paprika.py:44
msgid "Notes"
msgstr ""
msgstr "Notater"
#: .\cookbook\integration\paprika.py:47
msgid "Nutritional Information"
msgstr ""
msgstr "Næringsinformasjon"
#: .\cookbook\integration\paprika.py:50
msgid "Source"
msgstr ""
msgstr "Kilde"
#: .\cookbook\integration\safron.py:23
#: .\cookbook\templates\forms\edit_internal_recipe.html:75
#: .\cookbook\templates\include\log_cooking.html:16
#: .\cookbook\templates\url_import.html:84
msgid "Servings"
msgstr ""
msgstr "Porsjoner"
#: .\cookbook\integration\safron.py:25
msgid "Waiting time"
msgstr ""
msgstr "Ventetid"
#: .\cookbook\integration\safron.py:27
#: .\cookbook\templates\forms\edit_internal_recipe.html:69
msgid "Preparation Time"
msgstr ""
msgstr "Forberedelsestid"
#: .\cookbook\integration\safron.py:29 .\cookbook\templates\base.html:71
#: .\cookbook\templates\forms\ingredients.html:7
@@ -329,7 +348,7 @@ msgstr "Søk"
#: .\cookbook\templates\meal_plan.html:5 .\cookbook\views\delete.py:152
#: .\cookbook\views\edit.py:224 .\cookbook\views\new.py:188
msgid "Meal-Plan"
msgstr ""
msgstr "Måltidsplan"
#: .\cookbook\models.py:112 .\cookbook\templates\base.html:82
msgid "Books"
@@ -337,11 +356,11 @@ msgstr "Bøker"
#: .\cookbook\models.py:119
msgid "Small"
msgstr ""
msgstr "Liten"
#: .\cookbook\models.py:119
msgid "Large"
msgstr ""
msgstr "Stor"
#: .\cookbook\models.py:327
#: .\cookbook\templates\forms\edit_internal_recipe.html:198
@@ -1109,22 +1128,24 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:125
msgid "Images & Links"
msgstr ""
msgstr "Bilder og lenker"
#: .\cookbook\templates\markdown_info.html:126
msgid ""
"Links can be formatted with Markdown. This application also allows to paste "
"links directly into markdown fields without any formatting."
msgstr ""
"Lenker kan formateres med Markdown. Denne applikasjonen lar deg også lime "
"inn lenker direkte i Markdown-felt uten noen formatering."
#: .\cookbook\templates\markdown_info.html:132
#: .\cookbook\templates\markdown_info.html:145
msgid "This will become an image"
msgstr ""
msgstr "Dette vil bli til et bilde"
#: .\cookbook\templates\markdown_info.html:152
msgid "Tables"
msgstr ""
msgstr "Tabeller"
#: .\cookbook\templates\markdown_info.html:153
msgid ""
@@ -1132,124 +1153,130 @@ msgid ""
"editor like <a href=\"https://www.tablesgenerator.com/markdown_tables\" rel="
"\"noreferrer noopener\" target=\"_blank\">this one.</a>"
msgstr ""
"Markdown-tabeller er vanskelige å lage for hånd. Det anbefales å bruke en "
"tabellredigerer som <a href=\"https://www.tablesgenerator.com/"
"markdown_tables\" rel=\"noreferrer noopener\" target=\"_blank\">denne.</a>"
#: .\cookbook\templates\markdown_info.html:155
#: .\cookbook\templates\markdown_info.html:157
#: .\cookbook\templates\markdown_info.html:171
#: .\cookbook\templates\markdown_info.html:177
msgid "Table"
msgstr ""
msgstr "Tabell"
#: .\cookbook\templates\markdown_info.html:155
#: .\cookbook\templates\markdown_info.html:172
msgid "Header"
msgstr ""
msgstr "Overskrift"
#: .\cookbook\templates\markdown_info.html:157
#: .\cookbook\templates\markdown_info.html:178
msgid "Cell"
msgstr ""
msgstr "Celle"
#: .\cookbook\templates\meal_plan.html:101
msgid "New Entry"
msgstr ""
msgstr "Ny oppføring"
#: .\cookbook\templates\meal_plan.html:113
#: .\cookbook\templates\shopping_list.html:52
msgid "Search Recipe"
msgstr ""
msgstr "Søk oppskrift"
#: .\cookbook\templates\meal_plan.html:139
msgid "Title"
msgstr ""
msgstr "Tittel"
#: .\cookbook\templates\meal_plan.html:141
msgid "Note (optional)"
msgstr ""
msgstr "Merknad (valgfritt)"
#: .\cookbook\templates\meal_plan.html:143
msgid ""
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
"\" target=\"_blank\" rel=\"noopener noreferrer\">docs here</a>"
msgstr ""
"Du kan bruke Markdown for å formatere dette feltet. Se <a href=\"/docs/"
"markdown/\" target=\"_blank\" rel=\"noopener noreferrer\">dokumentasjonen "
"her</a>"
#: .\cookbook\templates\meal_plan.html:147
#: .\cookbook\templates\meal_plan.html:251
msgid "Serving Count"
msgstr ""
msgstr "Antall porsjoner"
#: .\cookbook\templates\meal_plan.html:153
msgid "Create only note"
msgstr ""
msgstr "Opprett kun en merknad"
#: .\cookbook\templates\meal_plan.html:168
#: .\cookbook\templates\shopping_list.html:7
#: .\cookbook\templates\shopping_list.html:29
#: .\cookbook\templates\shopping_list.html:705
msgid "Shopping List"
msgstr ""
msgstr "Handleliste"
#: .\cookbook\templates\meal_plan.html:172
msgid "Shopping list currently empty"
msgstr ""
msgstr "Handlelisten er for øyeblikket tom"
#: .\cookbook\templates\meal_plan.html:175
msgid "Open Shopping List"
msgstr ""
msgstr "Åpne handlelisten"
#: .\cookbook\templates\meal_plan.html:189
msgid "Plan"
msgstr ""
msgstr "Plan"
#: .\cookbook\templates\meal_plan.html:196
msgid "Number of Days"
msgstr ""
msgstr "Antall dager"
#: .\cookbook\templates\meal_plan.html:206
msgid "Weekday offset"
msgstr ""
msgstr "Ukedagsforskyvning"
#: .\cookbook\templates\meal_plan.html:209
msgid ""
"Number of days starting from the first day of the week to offset the default "
"view."
msgstr ""
msgstr "Antall dager fra den første dagen i uken for å endre standardvisningen."
#: .\cookbook\templates\meal_plan.html:217
#: .\cookbook\templates\meal_plan.html:294
msgid "Edit plan types"
msgstr ""
msgstr "Rediger plantyper"
#: .\cookbook\templates\meal_plan.html:219
msgid "Show help"
msgstr ""
msgstr "Vis hjelp"
#: .\cookbook\templates\meal_plan.html:220
msgid "Week iCal export"
msgstr ""
msgstr "Uke iCal-eksport"
#: .\cookbook\templates\meal_plan.html:264
#: .\cookbook\templates\meal_plan_entry.html:18
msgid "Created by"
msgstr ""
msgstr "Opprettet av"
#: .\cookbook\templates\meal_plan.html:270
#: .\cookbook\templates\meal_plan_entry.html:20
#: .\cookbook\templates\shopping_list.html:250
msgid "Shared with"
msgstr ""
msgstr "Delt med"
#: .\cookbook\templates\meal_plan.html:280
msgid "Add to Shopping"
msgstr ""
msgstr "Legg til i handlelisten"
#: .\cookbook\templates\meal_plan.html:323
msgid "New meal type"
msgstr ""
msgstr "Ny måltidstype"
#: .\cookbook\templates\meal_plan.html:338
msgid "Meal Plan Help"
msgstr ""
msgstr "Hjelp for måltidsplanen"
#: .\cookbook\templates\meal_plan.html:344
msgid ""
@@ -1289,7 +1316,7 @@ msgstr ""
#: .\cookbook\templates\meal_plan_entry.html:6
msgid "Meal Plan View"
msgstr ""
msgstr "Visning av måltidsplanen"
#: .\cookbook\templates\meal_plan_entry.html:50
msgid "Never cooked before."
@@ -1297,7 +1324,7 @@ msgstr ""
#: .\cookbook\templates\meal_plan_entry.html:76
msgid "Other meals on this day"
msgstr ""
msgstr "Andre måltider denne dagen"
#: .\cookbook\templates\no_groups_info.html:5
#: .\cookbook\templates\no_groups_info.html:12

View File

@@ -13,10 +13,10 @@ 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-02-27 13:55+0000\n"
"Last-Translator: Jesse <jesse.kamps@pm.me>\n"
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/recipes-"
"backend/nl/>\n"
"PO-Revision-Date: 2023-08-15 19:19+0000\n"
"Last-Translator: Jochum van der Heide <jochum@famvanderheide.com>\n"
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/nl/>\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -522,34 +522,32 @@ msgid "One of queryset or hash_key must be provided"
msgstr "Er moet een queryset of hash_key opgegeven worden"
#: .\cookbook\helper\recipe_url_import.py:266
#, fuzzy
#| msgid "Use fractions"
msgid "reverse rotation"
msgstr "Gebruik fracties"
msgstr "omgekeerde rotatie"
#: .\cookbook\helper\recipe_url_import.py:267
msgid "careful rotation"
msgstr ""
msgstr "voorzichtige rotatie"
#: .\cookbook\helper\recipe_url_import.py:268
msgid "knead"
msgstr ""
msgstr "kneden"
#: .\cookbook\helper\recipe_url_import.py:269
msgid "thicken"
msgstr ""
msgstr "verdikken"
#: .\cookbook\helper\recipe_url_import.py:270
msgid "warm up"
msgstr ""
msgstr "opwarmen"
#: .\cookbook\helper\recipe_url_import.py:271
msgid "ferment"
msgstr ""
msgstr "gisten"
#: .\cookbook\helper\recipe_url_import.py:272
msgid "sous-vide"
msgstr ""
msgstr "sous-vide"
#: .\cookbook\helper\shopping_helper.py:157
msgid "You must supply a servings size"
@@ -594,10 +592,8 @@ msgid "Imported %s recipes."
msgstr "%s recepten geïmporteerd."
#: .\cookbook\integration\openeats.py:26
#, fuzzy
#| msgid "Recipe Home"
msgid "Recipe source:"
msgstr "Recept thuis"
msgstr "Bron van het recept:"
#: .\cookbook\integration\paprika.py:49
msgid "Notes"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
"PO-Revision-Date: 2023-04-12 11:55+0000\n"
"Last-Translator: noxonad <noxonad@proton.me>\n"
"PO-Revision-Date: 2023-08-13 08:19+0000\n"
"Last-Translator: Miha Perpar <miha.perpar2@gmail.com>\n"
"Language-Team: Slovenian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/sl/>\n"
"Language: sl\n"
@@ -964,7 +964,7 @@ msgstr ""
#: .\cookbook\templates\base.html:275
msgid "GitHub"
msgstr ""
msgstr "GitHub"
#: .\cookbook\templates\base.html:277
msgid "Translate Tandoor"
@@ -1961,7 +1961,7 @@ msgstr ""
#: .\cookbook\templates\space.html:106
msgid "user"
msgstr ""
msgstr "uporabnik"
#: .\cookbook\templates\space.html:107
msgid "guest"

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.10 on 2023-07-22 06:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0195_invitelink_internal_note_userspace_internal_note_and_more'),
]
operations = [
migrations.AddField(
model_name='food',
name='url',
field=models.CharField(blank=True, default='', max_length=1024, null=True),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.1.10 on 2023-08-24 08:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0196_food_url'),
]
operations = [
migrations.AddField(
model_name='step',
name='show_ingredients_table',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='userpreference',
name='show_step_ingredients',
field=models.BooleanField(default=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.10 on 2023-08-24 09:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0197_step_show_ingredients_table_and_more'),
]
operations = [
migrations.AddField(
model_name='propertytype',
name='order',
field=models.IntegerField(default=0),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.1.10 on 2023-08-25 13:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0198_propertytype_order'),
]
operations = [
migrations.AlterField(
model_name='automation',
name='type',
field=models.CharField(
choices=[
('FOOD_ALIAS',
'Food Alias'),
('UNIT_ALIAS',
'Unit Alias'),
('KEYWORD_ALIAS',
'Keyword Alias'),
('DESCRIPTION_REPLACE',
'Description Replace'),
('INSTRUCTION_REPLACE',
'Instruction Replace'),
('NEVER_UNIT',
'Never Unit'),
('TRANSPOSE_WORDS',
'Transpose Words')],
max_length=128),
),
]

View File

@@ -5,7 +5,6 @@ import uuid
from datetime import date, timedelta
import oauth2_provider.models
from PIL import Image
from annoying.fields import AutoOneToOneField
from django.contrib import auth
from django.contrib.auth.models import Group, User
@@ -14,13 +13,14 @@ from django.contrib.postgres.search import SearchVectorField
from django.core.files.uploadedfile import InMemoryUploadedFile, UploadedFile
from django.core.validators import MinLengthValidator
from django.db import IntegrityError, models
from django.db.models import Index, ProtectedError, Q, Avg, Max
from django.db.models import Avg, Index, Max, ProtectedError, Q
from django.db.models.fields.related import ManyToManyField
from django.db.models.functions import Substr
from django.utils import timezone
from django.utils.translation import gettext as _
from django_prometheus.models import ExportModelOperationsMixin
from django_scopes import ScopedManager, scopes_disabled
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,
@@ -394,6 +394,7 @@ class UserPreference(models.Model, PermissionModelMixin):
shopping_add_onhand = models.BooleanField(default=False)
filter_to_supermarket = models.BooleanField(default=False)
left_handed = models.BooleanField(default=False)
show_step_ingredients = models.BooleanField(default=True)
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
shopping_recent_days = models.PositiveIntegerField(default=7)
csv_delim = models.CharField(max_length=2, default=",")
@@ -579,6 +580,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
plural_name = models.CharField(max_length=128, null=True, blank=True, default=None)
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
url = models.CharField(max_length=1024, blank=True, null=True, default='')
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) # inherited field
ignore_shopping = models.BooleanField(default=False) # inherited field
onhand_users = models.ManyToManyField(User, blank=True)
@@ -737,6 +739,7 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
order = models.IntegerField(default=0)
file = models.ForeignKey('UserFile', on_delete=models.PROTECT, null=True, blank=True)
show_as_header = models.BooleanField(default=True)
show_ingredients_table = models.BooleanField(default=True)
search_vector = SearchVectorField(null=True)
step_recipe = models.ForeignKey('Recipe', default=None, blank=True, null=True, on_delete=models.PROTECT)
@@ -765,8 +768,10 @@ class PropertyType(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
unit = models.CharField(max_length=64, blank=True, null=True)
icon = models.CharField(max_length=16, blank=True, null=True)
order = models.IntegerField(default=0)
description = models.CharField(max_length=512, blank=True, null=True)
category = models.CharField(max_length=64, choices=((NUTRITION, _('Nutrition')), (ALLERGEN, _('Allergen')), (PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
category = models.CharField(max_length=64, choices=((NUTRITION, _('Nutrition')), (ALLERGEN, _('Allergen')),
(PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
# TODO show if empty property?
@@ -783,6 +788,7 @@ class PropertyType(models.Model, PermissionModelMixin):
models.UniqueConstraint(fields=['space', 'name'], name='property_type_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='property_type_unique_open_data_slug_per_space')
]
ordering = ('order',)
class Property(models.Model, PermissionModelMixin):
@@ -1314,10 +1320,13 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
KEYWORD_ALIAS = 'KEYWORD_ALIAS'
DESCRIPTION_REPLACE = 'DESCRIPTION_REPLACE'
INSTRUCTION_REPLACE = 'INSTRUCTION_REPLACE'
NEVER_UNIT = 'NEVER_UNIT'
TRANSPOSE_WORDS = 'TRANSPOSE_WORDS'
type = models.CharField(max_length=128,
choices=((FOOD_ALIAS, _('Food Alias')), (UNIT_ALIAS, _('Unit Alias')), (KEYWORD_ALIAS, _('Keyword Alias')),
(DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')),))
(DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')),
(NEVER_UNIT, _('Never Unit')), (TRANSPOSE_WORDS, _('Transpose Words')),))
name = models.CharField(max_length=128, default='')
description = models.TextField(blank=True, null=True)

View File

@@ -1,3 +1,4 @@
import random
import traceback
import uuid
from datetime import datetime, timedelta
@@ -375,7 +376,7 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
'food_inherit_default', 'default_delay',
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
'csv_delim', 'csv_prefix',
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'food_children_exist'
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'show_step_ingredients', 'food_children_exist'
)
@@ -478,7 +479,7 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin)
class Meta:
model = Unit
fields = ('id', 'name', 'plural_name', 'description', 'numrecipe', 'image', 'open_data_slug')
fields = ('id', 'name', 'plural_name', 'description', 'base_unit', 'numrecipe', 'image', 'open_data_slug')
read_only_fields = ('id', 'numrecipe', 'image')
@@ -527,7 +528,7 @@ class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer,
class Meta:
model = PropertyType
fields = ('id', 'name', 'icon', 'unit', 'description', 'open_data_slug')
fields = ('id', 'name', 'icon', 'unit', 'description', 'order', 'open_data_slug')
class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
@@ -684,7 +685,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
class Meta:
model = Food
fields = (
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe',
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url',
'properties', 'properties_food_amount', 'properties_food_unit',
'food_onhand', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
@@ -770,7 +771,8 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
model = Step
fields = (
'id', 'name', 'instruction', 'ingredients', 'ingredients_markdown',
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe', 'step_recipe_data', 'numrecipe'
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe',
'step_recipe_data', 'numrecipe', 'show_ingredients_table'
)
@@ -999,6 +1001,16 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
read_only_fields = ('created_by',)
class AutoMealPlanSerializer(serializers.Serializer):
start_date = serializers.DateField()
end_date = serializers.DateField()
meal_type_id = serializers.IntegerField()
keywords = KeywordSerializer(many=True)
servings = CustomDecimalField()
shared = UserSerializer(many=True, required=False, allow_null=True)
addshopping = serializers.BooleanField()
class ShoppingListRecipeSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField('get_name') # should this be done at the front end?
recipe_name = serializers.ReadOnlyField(source='recipe.name')
@@ -1350,7 +1362,7 @@ class StepExportSerializer(WritableNestedModelSerializer):
class Meta:
model = Step
fields = ('name', 'instruction', 'ingredients', 'time', 'order', 'show_as_header')
fields = ('name', 'instruction', 'ingredients', 'time', 'order', 'show_as_header', 'show_ingredients_table')
class RecipeExportSerializer(WritableNestedModelSerializer):

View File

@@ -7,8 +7,7 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="2000px"
height="2000px"
viewBox="0 0 2000 2000"
version="1.1"
id="SVGRoot"

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -29,6 +29,7 @@
<meta name="msapplication-TileColor" content="#161616">
<meta name="theme-color" content="#ffffff">
<meta name="apple-mobile-web-app-capable" content="yes"/>
<!-- Bootstrap 4 -->
<link id="id_main_css" href="{% theme_url request %}" rel="stylesheet">

View File

@@ -1,42 +1,52 @@
{
"name": "Tandoor Recipes",
"short_name": "Tandoor",
"description": "Application to manage, tag and search recipes.",
"icons": [
{
"src": "/static/assets/logo_color144.png",
"type": "image/png",
"sizes": "144x144"
},
{
"src": "/static/assets/logo_color512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "./search",
"background_color": "#ffcb76",
"display": "standalone",
"scope": ".",
"theme_color": "#ffcb76",
"shortcuts": [
{
"name": "Plan",
"short_name": "Plan",
"description": "View your meal Plan",
"url": "./plan"
},
{
"name": "Books",
"short_name": "Cookbooks",
"description": "View your cookbooks",
"url": "./books"
},
{
"name": "Shopping",
"short_name": "Shopping",
"description": "View your shopping lists",
"url": "./list/shopping-list/"
}
]
"name": "Tandoor Recipes",
"short_name": "Tandoor",
"description": "Application to manage, tag and search recipes.",
"icons": [
{
"src": "/static/assets/logo_color144.png",
"type": "image/png",
"sizes": "144x144"
},
{
"src": "/static/assets/logo_color512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "./search",
"background_color": "#ffcb76",
"display": "standalone",
"scope": ".",
"theme_color": "#ffcb76",
"shortcuts": [
{
"name": "Plan",
"short_name": "Plan",
"description": "View your meal Plan",
"url": "./plan"
},
{
"name": "Books",
"short_name": "Cookbooks",
"description": "View your cookbooks",
"url": "./books"
},
{
"name": "Shopping",
"short_name": "Shopping",
"description": "View your shopping lists",
"url": "./list/shopping-list/"
}
],
"share_target": {
"action": "/data/import/url",
"method": "GET",
"params": {
"title": "title",
"url": "url",
"text": "text"
}
}
}

View File

@@ -11,29 +11,39 @@
{% block content %}
<h1>{% trans 'System' %}</h1>
<br/>
<br/>
<br/>
<br/>
<h3>{% trans 'System Information' %}</h3>
{% blocktrans %}
Django Recipes is an open source free software application. It can be found on
<a href="https://github.com/vabene1111/recipes">GitHub</a>.
Changelogs can be found <a href="https://github.com/vabene1111/recipes/releases">here</a>.
{% endblocktrans %}
<br/>
<br/>
Current Version: {% if version and version != '' %}
<a href="https://github.com/vabene1111/recipes/releases/tag/{{ version }}">{{ version }}</a>{% else %}
{{ version }}{% endif %}<br/>
Ref: <a href="https://github.com/vabene1111/recipes/commit/{{ ref }}">{{ ref }}</a>
<br/>
<br/>
<br/>
<h4>{% trans 'Media Serving' %} <span class="badge badge-{% if gunicorn_media %}danger{% else %}success{% endif %}">{% if gunicorn_media %}
<h3 class="mt-5">{% trans 'System Information' %}</h3>
{% if version_info %}
<div class="row">
<div class="col">
<div class="list-group">
{% for v in version_info %}
<div class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ v.name }} ({{ v.branch }}) {% if v.tag %}- {{ v.tag }}{% endif %}</h5>
</div>
<pre class="card-text p-2" style="border: 1px solid lightgrey; border-radius: 5px" target="_blank">{{ v.version }}</pre>
<a href="{{ v.website }}">Website</a>
{% if v.commit_link %}
- <a href="{{ v.commit_link }}" target="_blank">Commit</a>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
{% else %}
{% blocktrans %}
You need to execute <code>version.py</code> in your update script to generate version information (done automatically in docker).
{% endblocktrans %}
{% endif %}
<h4 class="mt-3">{% trans 'Media Serving' %} <span class="badge badge-{% if gunicorn_media %}danger{% else %}success{% endif %}">{% if gunicorn_media %}
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
{% if gunicorn_media %}
{% blocktrans %}Serving media files directly using gunicorn/python is <b>not recommend</b>!
@@ -44,10 +54,9 @@
{% else %}
{% trans 'Everything is fine!' %}
{% endif %}
<br/>
<br/>
<h4>{% trans 'Secret Key' %} <span
<h4 class="mt-3">{% trans 'Secret Key' %} <span
class="badge badge-{% if secret_key %}danger{% else %}success{% endif %}">{% if secret_key %}
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
{% if secret_key %}
@@ -60,10 +69,8 @@
{% else %}
{% trans 'Everything is fine!' %}
{% endif %}
<br/>
<br/>
<h4>{% trans 'Debug Mode' %} <span
<h4 class="mt-3">{% trans 'Debug Mode' %} <span
class="badge badge-{% if debug %}danger{% else %}success{% endif %}">{% if debug %}
{% trans 'Warning' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
{% if debug %}
@@ -75,10 +82,8 @@
{% else %}
{% trans 'Everything is fine!' %}
{% endif %}
<br/>
<br/>
<h4>{% trans 'Database' %} <span
<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 %}
@@ -89,9 +94,8 @@
{% else %}
{% trans 'Everything is fine!' %}
{% endif %}
<br/>
<br/>
<h4>Debug</h4>
<h4 class="mt-3">Debug</h4>
<textarea class="form-control" rows="20">
Gunicorn Media: {{ gunicorn_media }}
Sqlite: {{ postgres }}
@@ -99,9 +103,9 @@ Debug: {{ debug }}
{% for key,value in request.META.items %}{% if key in 'SERVER_PORT,REMOTE_HOST,REMOTE_ADDR,SERVER_PROTOCOL' %}{{ key }}:{{ value }}
{% endif %}{% endfor %}
{% for key,value in request.META.items %}{% if 'HTTP_' in key %}{{ key }}:{{ value }}
{% for key,value in request.META.items %}{% if 'HTTP_' in key %}{{ key }}:{{ value }}
{% endif %}{% endfor %}
{% for key,value in request.META.items %}{% if 'wsgi.' in key %}{{ key }}:{{ value }}
{% for key,value in request.META.items %}{% if 'wsgi.' in key %}{{ key }}:{{ value }}
{% endif %}{% endfor %}
</textarea>
<br/>

View File

@@ -5,7 +5,6 @@ import bleach
import markdown as md
from django_scopes import ScopeError
from markdown.extensions.tables import TableExtension
from bleach_allowlist import markdown_attrs, markdown_tags
from django import template
from django.db.models import Avg
from django.templatetags.static import static
@@ -46,9 +45,17 @@ def delete_url(model, pk):
@register.filter()
def markdown(value):
tags = markdown_tags + [
tags = {
"h1", "h2", "h3", "h4", "h5", "h6",
"b", "i", "strong", "em", "tt",
"p", "br",
"span", "div", "blockquote", "code", "pre", "hr",
"ul", "ol", "li", "dd", "dt",
"img",
"a",
"sub", "sup",
'pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead'
]
}
parsed_md = md.markdown(
value,
extensions=[
@@ -56,7 +63,12 @@ def markdown(value):
UrlizeExtension(), MarkdownFormatExtension()
]
)
markdown_attrs['*'] = markdown_attrs['*'] + ['class']
markdown_attrs = {
"*": ["id", "class"],
"img": ["src", "alt", "title"],
"a": ["href", "alt", "title"],
}
parsed_md = parsed_md[3:] # remove outer paragraph
parsed_md = parsed_md[:len(parsed_md)-4]
return bleach.clean(parsed_md, tags, markdown_attrs)

View File

@@ -475,6 +475,7 @@ def test_root_filter(obj_tree_1, obj_2, obj_3, u1_s1):
with scope(space=obj_tree_1.space):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
obj_2 = Food.objects.get(id=obj_2.id)
parent = obj_tree_1.get_parent()
# should return root objects in the space (obj_1, obj_2), ignoring query filters
@@ -499,17 +500,16 @@ def test_tree_filter(obj_tree_1, obj_2, obj_3, u1_s1):
with scope(space=obj_tree_1.space):
# for some reason the 'path' attribute changes between the factory and the test when using both obj_tree and obj
obj_tree_1 = Food.objects.get(id=obj_tree_1.id)
obj_2 = Food.objects.get(id=obj_2.id)
parent = obj_tree_1.get_parent()
obj_2.move(parent, node_location)
obj_2 = Food.objects.get(id=obj_2.id)
parent = Food.objects.get(id=parent.id)
# should return full tree starting at parent (obj_tree_1, obj_2), ignoring query filters
response = json.loads(
u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}').content)
# should return full tree starting at, but excluding parent (obj_tree_1, obj_2), ignoring query filters
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}').content)
assert response['count'] == 4
response = json.loads(u1_s1.get(
f'{reverse(LIST_URL)}?tree={parent.id}&query={obj_2.name[4:]}').content)
# filtering is ignored - should return identical results as ?tree=x
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?tree={parent.id}&query={obj_2.name[4:]}').content)
assert response['count'] == 4

View File

@@ -81,10 +81,10 @@ def test_share_permission(recipe_1_s1, u1_s1, u1_s2, u2_s1, a_u):
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 200],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 200],
['g1_s2', 404],
['g1_s2', 403],
['u1_s2', 404],
['a1_s2', 404],
])
@@ -140,7 +140,7 @@ def test_update_private_recipe(u1_s1, u2_s1, recipe_1_s1):
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 201],
['g1_s1', 403],
['u1_s1', 201],
['a1_s1', 201],
])

View File

@@ -48,7 +48,7 @@ def recipe(request, space_1, u1_s1):
@pytest.mark.parametrize("arg", [
['g1_s1', 204],
['g1_s1', 403],
['u1_s1', 204],
['u1_s2', 404],
['a1_s1', 204],

View File

@@ -15,9 +15,9 @@ DETAIL_URL = 'api:space-detail'
@pytest.mark.parametrize("arg", [
['a_u', 403, 0],
['g1_s1', 403, 0],
['u1_s1', 403, 0],
['u1_s1', 200, 1],
['a1_s1', 200, 1],
['a2_s1', 200, 0],
['a2_s1', 200, 1],
])
def test_list_permission(arg, request, space_1, a1_s1):
space_1.created_by = auth.get_user(a1_s1)
@@ -29,16 +29,6 @@ def test_list_permission(arg, request, space_1, a1_s1):
assert len(json.loads(result.content)) == arg[2]
def test_list_permission_owner(u1_s1, a1_s1, space_1):
space_1.created_by = auth.get_user(a1_s1)
space_1.save()
assert len(json.loads(a1_s1.get(reverse(LIST_URL)).content)) == 1
assert u1_s1.get(reverse(LIST_URL)).status_code == 403
space_1.created_by = auth.get_user(u1_s1)
space_1.save()
assert u1_s1.get(reverse(LIST_URL)).status_code == 403
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],

View File

@@ -48,7 +48,7 @@ def test_list_filter(obj_1, obj_2, u1_s1):
assert r.status_code == 200
response = json.loads(r.content)
assert len(response) == 2
assert response[0]['name'] == obj_1.name
# assert response[0]['name'] == obj_1.name # assuming an order when it's not always valid
response = json.loads(u1_s1.get(f'{reverse(LIST_URL)}?limit=1').content)
assert len(response) == 1

View File

@@ -27,7 +27,7 @@ def test_list_permission(arg, request, space_1, g1_s1, u1_s1, a1_s1):
result = c.get(reverse(LIST_URL))
assert result.status_code == arg[1]
if arg[1] == 200:
assert len(json.loads(result.content)) == arg[2]
assert len(json.loads(result.content)['results']) == arg[2]
@pytest.mark.parametrize("arg", [

View File

@@ -334,6 +334,8 @@ class StepFactory(factory.django.DjangoModelFactory):
order = factory.Sequence(lambda x: x)
# file = models.ForeignKey('UserFile', on_delete=models.PROTECT, null=True, blank=True)
show_as_header = True
# TODO: need to update to fetch from User's preferences
show_ingredients_table = True
step_recipe__has_recipe = False
ingredients__food_recipe_count = 0
space = factory.SubFactory(SpaceFactory)

View File

@@ -7,7 +7,7 @@ from rest_framework.schemas import get_schema_view
from cookbook.helper import dal
from recipes.settings import DEBUG, PLUGINS
from recipes.version import VERSION_NUMBER
from cookbook.version_info import TANDOOR_VERSION
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage,
@@ -36,6 +36,7 @@ router.register(r'ingredient', api.IngredientViewSet)
router.register(r'invite-link', api.InviteLinkViewSet)
router.register(r'keyword', api.KeywordViewSet)
router.register(r'meal-plan', api.MealPlanViewSet)
router.register(r'auto-plan', api.AutoPlanViewSet, basename='auto-plan')
router.register(r'meal-type', api.MealTypeViewSet)
router.register(r'recipe', api.RecipeViewSet)
router.register(r'recipe-book', api.RecipeBookViewSet)
@@ -148,7 +149,7 @@ urlpatterns = [
path('docs/search/', views.search_info, name='docs_search'),
path('docs/api/', views.api_info, name='docs_api'),
path('openapi/', get_schema_view(title="Django Recipes", version=VERSION_NUMBER, public=True,
path('openapi/', get_schema_view(title="Django Recipes", version=TANDOOR_VERSION, public=True,
permission_classes=(permissions.AllowAny,)), name='openapi-schema'),
path('api/', include((router.urls, 'api'))),

3
cookbook/version_info.py Normal file
View File

@@ -0,0 +1,3 @@
TANDOOR_VERSION = ""
TANDOOR_REF = ""
VERSION_INFO = []

View File

@@ -1,7 +1,9 @@
import datetime
import io
import json
import mimetypes
import pathlib
import random
import re
import threading
import traceback
@@ -25,6 +27,7 @@ from django.core.files import File
from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When, Avg, Max
from django.db.models.fields.related import ForeignObjectRel
from django.db.models.functions import Coalesce, Lower
from django.db.models.signals import post_save
from django.http import FileResponse, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
@@ -60,7 +63,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner,
CustomIsSpaceOwner, CustomIsUser, group_required,
is_space_owner, switch_user_active_space, above_space_limit,
CustomRecipePermission, CustomUserPermission,
CustomTokenHasReadWriteScope, CustomTokenHasScope, has_group_permission)
CustomTokenHasReadWriteScope, CustomTokenHasScope, has_group_permission, IsReadOnlyDRF)
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch
from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup, clean_dict
from cookbook.helper.scrapers.scrapers import text_scraper
@@ -94,7 +97,8 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
SyncLogSerializer, SyncSerializer, UnitSerializer,
UserFileSerializer, UserSerializer, UserPreferenceSerializer,
UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer,
RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer, PropertySerializer)
RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer,
PropertySerializer, AutoMealPlanSerializer)
from cookbook.views.import_export import get_integration
from recipes import settings
@@ -402,11 +406,11 @@ class GroupViewSet(viewsets.ModelViewSet):
class SpaceViewSet(viewsets.ModelViewSet):
queryset = Space.objects
serializer_class = SpaceSerializer
permission_classes = [CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
permission_classes = [IsReadOnlyDRF & CustomIsUser | CustomIsOwner & CustomIsAdmin & CustomTokenHasReadWriteScope]
http_method_names = ['get', 'patch']
def get_queryset(self):
return self.queryset.filter(id=self.request.space.id, created_by=self.request.user)
return self.queryset.filter(id=self.request.space.id)
class UserSpaceViewSet(viewsets.ModelViewSet):
@@ -414,6 +418,7 @@ class UserSpaceViewSet(viewsets.ModelViewSet):
serializer_class = UserSpaceSerializer
permission_classes = [(CustomIsSpaceOwner | CustomIsOwnerReadOnly) & CustomTokenHasReadWriteScope]
http_method_names = ['get', 'patch', 'delete']
pagination_class = DefaultPagination
def destroy(self, request, *args, **kwargs):
if request.space.created_by == UserSpace.objects.get(pk=kwargs['pk']).user:
@@ -665,6 +670,66 @@ class MealPlanViewSet(viewsets.ModelViewSet):
return queryset
class AutoPlanViewSet(viewsets.ViewSet):
def create(self, request):
serializer = AutoMealPlanSerializer(data=request.data)
if serializer.is_valid():
keywords = serializer.validated_data['keywords']
start_date = serializer.validated_data['start_date']
end_date = serializer.validated_data['end_date']
servings = serializer.validated_data['servings']
shared = serializer.get_initial().get('shared', None)
shared_pks = list()
if shared is not None:
for i in range(len(shared)):
shared_pks.append(shared[i]['id'])
days = min((end_date - start_date).days + 1, 14)
recipes = Recipe.objects.values('id', 'name')
meal_plans = list()
for keyword in keywords:
recipes = recipes.filter(keywords__name=keyword['name'])
if len(recipes) == 0:
return Response(serializer.data)
recipes = list(recipes.order_by('?')[:days])
for i in range(0, days):
day = start_date + datetime.timedelta(i)
recipe = recipes[i % len(recipes)]
args = {'recipe_id': recipe['id'], 'servings': servings,
'created_by': request.user,
'meal_type_id': serializer.validated_data['meal_type_id'],
'note': '', 'date': day, 'space': request.space}
m = MealPlan(**args)
meal_plans.append(m)
MealPlan.objects.bulk_create(meal_plans)
for m in meal_plans:
m.shared.set(shared_pks)
if request.data.get('addshopping', False):
SLR = RecipeShoppingEditor(user=request.user, space=request.space)
SLR.create(mealplan=m, servings=servings)
else:
post_save.send(
sender=m.__class__,
instance=m,
created=True,
update_fields=None,
)
return Response(serializer.data)
return Response(serializer.errors, 400)
class MealTypeViewSet(viewsets.ModelViewSet):
"""
returns list of meal types created by the
@@ -896,7 +961,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
try:
url = serializer.validated_data['image_url']
if validators.url(url, public=True):
response = requests.get(url)
response = requests.get(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0"})
image = File(io.BytesIO(response.content))
filetype = mimetypes.guess_extension(response.headers['content-type']) or filetype
except UnidentifiedImageError as e:

View File

@@ -37,7 +37,7 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
obj.space = self.request.space
obj.internal = True
obj.save()
obj.steps.add(Step.objects.create(space=self.request.space, show_as_header=False))
obj.steps.add(Step.objects.create(space=self.request.space, show_as_header=False, show_ingredients_table=self.request.user.userpreference.show_step_ingredients))
return HttpResponseRedirect(reverse('edit_recipe', kwargs={'pk': obj.pk}))
def get_success_url(self):

View File

@@ -22,7 +22,8 @@ from cookbook.helper.permission_helper import group_required, has_group_permissi
from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference, ShareLink,
Space, ViewLog, UserSpace)
from cookbook.tables import (CookLogTable, ViewLogTable)
from recipes.version import BUILD_REF, VERSION_NUMBER
from cookbook.version_info import VERSION_INFO
from recipes.settings import PLUGINS
def index(request):
@@ -320,8 +321,8 @@ def system(request):
'gunicorn_media': settings.GUNICORN_MEDIA,
'debug': settings.DEBUG,
'postgres': postgres,
'version': VERSION_NUMBER,
'ref': BUILD_REF,
'version_info': VERSION_INFO,
'plugins': PLUGINS,
'secret_key': secret_key
})

View File

@@ -100,15 +100,17 @@ AUTH_LDAP_START_TLS=1
AUTH_LDAP_TLS_CACERTFILE=/etc/ssl/certs/own-ca.pem
```
## Reverse Proxy Authentication
## External Authentication
!!! warning "Security Impact"
If you just set `REMOTE_USER_AUTH=1` without any additional configuration, _anybody_ can authenticate with _any_ username!
!!! Info "Community Contributed Tutorial"
This tutorial was provided by a community member. Since I do not use reverse proxy authentication, I cannot provide any
assistance should you choose to use this authentication method.
This tutorial was provided by a community member. We are not able to provide any support! Please only use, if you know what you are doing!
In order use proxy authentication you will need to:
In order use external authentication (i.e. using a proxy auth like Authelia, Authentik, etc.) you will need to:
1. Set `REVERSE_PROXY_AUTH=1` in the `.env` file
1. Set `REMOTE_USER_AUTH=1` in the `.env` file
2. Update your nginx configuration file
Using any of the examples above will automatically generate a configuration file inside a docker volume.
@@ -116,10 +118,10 @@ Use `docker volume inspect recipes_nginx` to find out where your volume is store
!!! warning "Configuration File Volume"
The nginx config volume is generated when the container is first run. You can change the volume to a bind mount in the
warning `docker-compose.yml`, but then you will need to manually create it. See section `Volumes vs Bind Mounts` below
`docker-compose.yml`, but then you will need to manually create it. See section `Volumes vs Bind Mounts` below
for more information.
The following example shows a configuration for Authelia:
### Configuration Example for Authelia
```
server {
@@ -161,7 +163,7 @@ server {
}
```
Please refer to the appropriate documentation on how to setup the reverse proxy, authentication, and networks.
Please refer to the appropriate documentation on how to set up the reverse proxy, authentication, and networks.
Ensure users have been configured for Authelia, and that the endpoint recipes is pointed to is protected but
available.

View File

@@ -1,39 +1,41 @@
!!! warning
Automations are currently in a beta stage. They work pretty stable but if I encounter any
issues while working on them, I might change how they work breaking existing automations.
I will try to avoid this and am pretty confident it won't happen.
Automations are currently in a beta stage. They work pretty stable but if I encounter any
issues while working on them, I might change how they work breaking existing automations.
I will try to avoid this and am pretty confident it won't happen.
Automations allow Tandoor to automatically perform certain tasks, especially when importing recipes, that
Automations allow Tandoor to automatically perform certain tasks, especially when importing recipes, that
would otherwise have to be done manually. Currently, the following automations are supported.
## Unit, Food, Keyword Alias
Foods, Units and Keywords can have automations that automatically replace them with another object
to allow aliasing them.
to allow aliasing them.
This helps to add consistency to the naming of objects, for example to always use the singular form
for the main name if a plural form is configured.
for the main name if a plural form is configured.
These automations are best created by dragging and dropping Foods, Units or Keywords in their respective
views and creating the automation there.
These automations are best created by dragging and dropping Foods, Units or Keywords in their respective
views and creating the automation there.
You can also create them manually by setting the following
- **Parameter 1**: name of food/unit/keyword to match
- **Parameter 2**: name of food/unit/keyword to replace matched food with
- **Parameter 1**: name of food/unit/keyword to match
- **Parameter 2**: name of food/unit/keyword to replace matched food with
These rules are processed whenever you are importing recipes from websites or other apps
and when using the simple ingredient input (shopping, recipe editor, ...).
## Description Replace
This automation is a bit more complicated than the alis rules. It is run when importing a recipe
This automation is a bit more complicated than the alias rules. It is run when importing a recipe
from a website.
It uses Regular Expressions (RegEx) to determine if a description should be altered, what exactly to remove
and what to replace it with.
and what to replace it with.
- **Parameter 1**: pattern of which sites to match (e.g. `.*.chefkoch.de.*`, `.*`)
- **Parameter 2**: pattern of what to replace (e.g. `.*`)
- **Parameter 3**: value to replace matched occurrence of parameter 2 with. Only one occurrence of the pattern is replaced.
- **Parameter 1**: pattern of which sites to match (e.g. `.*.chefkoch.de.*`, `.*`)
- **Parameter 2**: pattern of what to replace (e.g. `.*`)
- **Parameter 3**: value to replace matched occurrence of parameter 2 with. Only one occurrence of the pattern is replaced.
To replace the description the python [re.sub](https://docs.python.org/2/library/re.html#re.sub) function is used
like this `re.sub(<parameter 2>, <parameter 2>, <descriotion>, count=1)`
@@ -41,24 +43,52 @@ like this `re.sub(<parameter 2>, <parameter 2>, <descriotion>, count=1)`
To test out your patterns and learn about RegEx you can use [regexr.com](https://regexr.com/)
!!! info
In order to prevent denial of service attacks on the RegEx engine the number of replace automations
and the length of the inputs that are processed are limited. Those limits should never be reached
during normal usage.
In order to prevent denial of service attacks on the RegEx engine the number of replace automations
and the length of the inputs that are processed are limited. Those limits should never be reached
during normal usage.
## Instruction Replace
This works just like the Description Replace automation but runs against all instruction texts
in all steps of a recipe during import.
in all steps of a recipe during import.
Also instead of just replacing a single occurrence of the matched pattern it will replace all.
## Never Unit
Some ingredients have a pattern of AMOUNT and FOOD, if the food has multiple words (e.g. egg yolk) this can cause Tandoor
to detect the word "egg" as a unit. This automation will detect the word 'egg' as something that should never be considered
a unit.
You can also create them manually by setting the following
- **Parameter 1**: string to detect
- **Parameter 2**: Optional: unit to insert into ingredient (e.g. 1 whole 'egg yolk' instead of 1 <empty> 'egg yolk')
These rules are processed whenever you are importing recipes from websites or other apps
and when using the simple ingredient input (shopping, recipe editor, ...).
## Transpose Words
Some recipes list the food before the units for some foods (garlic cloves). This automation will transpose 2 words in an
ingredient so "garlic cloves" will automatically become "cloves garlic"
- **Parameter 1**: first word to detect
- **Parameter 2**: second word to detect
These rules are processed whenever you are importing recipes from websites or other apps
and when using the simple ingredient input (shopping, recipe editor, ...).
# Order
If the Automation type allows for more than one rule to be executed (for example description replace)
the rules are processed in ascending order (ordered by the *order* property of the automation).
The default order is always 1000 to make it easier to add automations before and after other automations.
If the Automation type allows for more than one rule to be executed (for example description replace)
the rules are processed in ascending order (ordered by the _order_ property of the automation).
The default order is always 1000 to make it easier to add automations before and after other automations.
Example:
1. Rule ABC (order 1000) replaces `everything` with `abc`
2. Rule DEF (order 2000) replaces `everything` with `def`
3. Rule XYZ (order 500) replaces `everything` with `xyz`
After processing rules XYZ, then ABC and then DEF the description will have the value `def`
After processing rules XYZ, then ABC and then DEF the description will have the value `def`

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-26 07:46+0200\n"
"POT-Creation-Date: 2023-08-29 13:09+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,66 +18,120 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\recipes\settings.py:436
#: .\recipes\plugins\enterprise_plugin\helper\permission_helper.py:58
msgid ""
"You do not have the required permissions to view this page! You need to have "
"the following modules licensed: "
msgstr ""
#: .\recipes\plugins\enterprise_plugin\helper\permission_helper.py:76
msgid "You are not logged in and therefore cannot view this page!"
msgstr ""
#: .\recipes\plugins\enterprise_plugin\helper\permission_helper.py:79
msgid "You do not have the required modules to view this page!"
msgstr ""
#: .\recipes\plugins\enterprise_plugin\helper\permission_helper.py:90
msgid "You do not have the required module to view this page!"
msgstr ""
#: .\recipes\plugins\enterprise_plugin\models.py:32
msgid "Recipe"
msgstr ""
#: .\recipes\plugins\enterprise_plugin\models.py:32
msgid "Recipe Keyword"
msgstr ""
#: .\recipes\plugins\enterprise_plugin\models.py:32
msgid "Meal Plan"
msgstr ""
#: .\recipes\plugins\enterprise_plugin\models.py:32
msgid "Shopping"
msgstr ""
#: .\recipes\plugins\enterprise_plugin\models.py:32
msgid "Book"
msgstr ""
#: .\recipes\plugins\enterprise_plugin\models.py:37
msgid "start"
msgstr ""
#: .\recipes\plugins\enterprise_plugin\models.py:37
msgid "center"
msgstr ""
#: .\recipes\plugins\enterprise_plugin\models.py:37
msgid "end"
msgstr ""
#: .\recipes\settings.py:455
msgid "Armenian "
msgstr ""
#: .\recipes\settings.py:437
#: .\recipes\settings.py:456
msgid "Bulgarian"
msgstr ""
#: .\recipes\settings.py:438
#: .\recipes\settings.py:457
msgid "Catalan"
msgstr ""
#: .\recipes\settings.py:439
#: .\recipes\settings.py:458
msgid "Czech"
msgstr ""
#: .\recipes\settings.py:440
#: .\recipes\settings.py:459
msgid "Danish"
msgstr ""
#: .\recipes\settings.py:441
#: .\recipes\settings.py:460
msgid "Dutch"
msgstr ""
#: .\recipes\settings.py:442
#: .\recipes\settings.py:461
msgid "English"
msgstr ""
#: .\recipes\settings.py:443
#: .\recipes\settings.py:462
msgid "French"
msgstr ""
#: .\recipes\settings.py:444
#: .\recipes\settings.py:463
msgid "German"
msgstr ""
#: .\recipes\settings.py:445
#: .\recipes\settings.py:464
msgid "Hungarian"
msgstr ""
#: .\recipes\settings.py:446
#: .\recipes\settings.py:465
msgid "Italian"
msgstr ""
#: .\recipes\settings.py:447
#: .\recipes\settings.py:466
msgid "Latvian"
msgstr ""
#: .\recipes\settings.py:448
#: .\recipes\settings.py:467
msgid "Norwegian "
msgstr ""
#: .\recipes\settings.py:468
msgid "Polish"
msgstr ""
#: .\recipes\settings.py:449
#: .\recipes\settings.py:469
msgid "Russian"
msgstr ""
#: .\recipes\settings.py:450
#: .\recipes\settings.py:470
msgid "Spanish"
msgstr ""
#: .\recipes\settings.py:451
#: .\recipes\settings.py:471
msgid "Swedish"
msgstr ""

View File

@@ -46,7 +46,11 @@ INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(
# allow djangos wsgi server to server mediafiles
GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', True)))
REVERSE_PROXY_AUTH = bool(int(os.getenv('REVERSE_PROXY_AUTH', 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".')
REMOTE_USER_AUTH = bool(int(os.getenv('REVERSE_PROXY_AUTH', False)))
else:
REMOTE_USER_AUTH = bool(int(os.getenv('REMOTE_USER_AUTH', False)))
# default value for user preference 'comment'
COMMENT_PREF_DEFAULT = bool(int(os.getenv('COMMENT_PREF_DEFAULT', True)))
@@ -148,6 +152,9 @@ try:
plugin_config = {
'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 '',
'module': f'recipes.plugins.{d}',
'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d),
'base_url': plugin_class.base_url,
@@ -270,7 +277,7 @@ SITE_ID = int(os.getenv('ALLAUTH_SITE_ID', 1))
ACCOUNT_ADAPTER = 'cookbook.helper.AllAuthCustomAdapter'
if REVERSE_PROXY_AUTH:
if REMOTE_USER_AUTH:
MIDDLEWARE.insert(8, 'recipes.middleware.CustomRemoteUser')
AUTHENTICATION_BACKENDS.append(
'django.contrib.auth.backends.RemoteUserBackend')

View File

@@ -1,2 +0,0 @@
VERSION_NUMBER = "0.0.0"
BUILD_REF = ""

View File

@@ -1,18 +1,17 @@
Django==4.1.9
cryptography==41.0.0
Django==4.1.10
cryptography===41.0.3
django-annoying==0.10.6
django-autocomplete-light==3.9.4
django-cleanup==7.0.0
django-cleanup==8.0.0
django-crispy-forms==1.14.0
django-tables2==2.5.3
djangorestframework==3.14.0
drf-writable-nested==0.7.0
django-oauth-toolkit==2.2.0
django-debug-toolbar==3.8.1
bleach==5.0.1
bleach-allowlist==1.0.3
bleach==6.0.0
gunicorn==20.1.0
lxml==4.9.2
lxml==4.9.3
Markdown==3.4.3
Pillow==9.4.0
psycopg2-binary==2.9.5
@@ -20,9 +19,9 @@ python-dotenv==0.21.0
requests==2.31.0
six==1.16.0
webdavclient3==3.14.6
whitenoise==6.2.0
whitenoise==6.5.0
icalendar==5.0.4
pyyaml==6.0
pyyaml==6.0.1
uritemplate==4.1.1
beautifulsoup4==4.11.1
microdata==0.8.0
@@ -41,8 +40,8 @@ boto3==1.26.41
django-prometheus==2.2.0
django-hCaptcha==0.2.0
python-ldap==3.4.3
django-auth-ldap==4.2.0
django-auth-ldap==4.4.0
pytest-factoryboy==2.5.1
pyppeteer==1.0.2
validators==0.20.0
pytube==12.1.0
pytube==15.0.0

74
version.py Normal file
View File

@@ -0,0 +1,74 @@
import os
import re
import subprocess
import sys
import traceback
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PLUGINS_DIRECTORY = os.path.join(BASE_DIR, 'recipes', 'plugins')
version_info = []
tandoor_tag = ''
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_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:
pass
version_info.append({
'name': 'Tandoor ',
'version': re.sub(r'<.*>', '', r),
'website': 'https://github.com/TandoorRecipes/recipes',
'commit_link': 'https://github.com/TandoorRecipes/recipes/commit/' + r.split('\n')[0].split(' ')[1],
'ref': tandoor_hash,
'branch': tandoor_branch,
'tag': tandoor_tag
})
if os.path.isdir(PLUGINS_DIRECTORY):
for d in os.listdir(PLUGINS_DIRECTORY):
if d != '__pycache__':
try:
apps_path = f'recipes.plugins.{d}.apps'
__import__(apps_path)
app_config_classname = dir(sys.modules[apps_path])[1]
plugin_module = f'recipes.plugins.{d}.apps.{app_config_classname}'
plugin_class = getattr(sys.modules[apps_path], app_config_classname)
print('getting plugin version for ', plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name)
r = subprocess.check_output(['git', 'show', '-s'], cwd=os.path.join(BASE_DIR, 'recipes', 'plugins', d)).decode()
branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd=os.path.join(BASE_DIR, 'recipes', 'plugins', d)).decode()
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 = ''
version_info.append({
'name': 'Plugin: ' + plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name,
'version': re.sub(r'<.*>', '', r),
'website': plugin_class.website if hasattr(plugin_class, 'website') else '',
'commit_link': plugin_class.github if hasattr(plugin_class, 'github') else '' + '/commit/' + commit_hash,
'ref': commit_hash,
'branch': branch,
'tag': tag
})
except subprocess.CalledProcessError as e:
print("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output))
except Exception:
traceback.print_exc()
except subprocess.CalledProcessError as e:
print("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output))
except:
traceback.print_exc()
with open('cookbook/version_info.py', 'w+', encoding='UTF-8') as f:
print(f"writing version info {version_info}")
f.write(f'TANDOOR_VERSION = "{tandoor_tag}"\nTANDOOR_REF = "{tandoor_hash}"\nVERSION_INFO = {version_info}')

View File

@@ -59,9 +59,9 @@
"@vue/compiler-sfc": "^3.2.45",
"@vue/eslint-config-typescript": "^10.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.28.0",
"eslint": "^8.46.0",
"eslint-plugin-vue": "^8.7.1",
"typescript": "~4.9.3",
"typescript": "~5.1.6",
"vue-cli-plugin-i18n": "^2.3.2",
"webpack-bundle-tracker": "1.8.1",
"workbox-background-sync": "^6.5.4",

View File

@@ -511,6 +511,10 @@ export default {
this.website_url = urlParams.get('url')
this.loadRecipe(this.website_url)
}
if (urlParams.has("text")) {
this.website_url = urlParams.get('text')
this.loadRecipe(this.website_url)
}
},
methods: {
/**
@@ -675,12 +679,26 @@ export default {
*/
autoImport: function () {
this.collapse_visible.import = true
this.website_url_list.split('\n').forEach(r => {
this.loadRecipe(r, true, undefined).then((recipe_json) => {
let url_list = this.website_url_list.split('\n').filter(x => x.trim() !== '')
if (url_list.length > 0) {
let url = url_list.shift()
this.website_url_list = url_list.join('\n')
this.loadRecipe(url, true, undefined).then((recipe_json) => {
this.importRecipe('multi_import', recipe_json, true)
setTimeout(() => {
this.autoImport()
}, 2000)
}).catch((err) => {
})
})
this.website_url_list = ''
} else {
this.import_loading = false
this.loading = false
}
},
/**
* Import recipes with uploaded files and app integration

View File

@@ -103,6 +103,7 @@
import draggable from "vuedraggable";
import stringSimilarity from "string-similarity"
import {getUserPreference} from "@/utils/utils"
export default {
name: "ImportViewStepEditor",
@@ -117,6 +118,7 @@ export default {
recipe_json: undefined,
current_edit_ingredient: null,
current_edit_step: null,
user_preferences: null,
}
},
watch: {
@@ -126,6 +128,7 @@ export default {
},
mounted() {
this.recipe_json = this.recipe
this.user_preferences = getUserPreference();
},
methods: {
/**
@@ -138,7 +141,7 @@ export default {
let steps = []
step.instruction.split(split_character).forEach(part => {
if (part.trim() !== '') {
steps.push({'instruction': part, 'ingredients': []})
steps.push({'instruction': part, 'ingredients': [], 'show_ingredients_table': this.user_preferences.show_step_ingredients})
}
})
steps[0].ingredients = step.ingredients // put all ingredients from the original step in the ingredients of the first step of the split step list

View File

@@ -83,7 +83,7 @@
</div>
</b-list-group-item>
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.entry.id" >
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.entry.id">
<div class="d-flex flex-row align-items-center">
<div>
<b-img style="height: 50px; width: 50px; object-fit: cover"
@@ -279,12 +279,19 @@
:create_date="mealplan_default_date"
@reload-meal-types="refreshMealTypes"
></meal-plan-edit-modal>
<auto-meal-plan-modal
:modal_title="'Auto create meal plan'"
:current_period="current_period"
></auto-meal-plan-modal>
<div class="row d-none d-lg-block">
<div class="col-12 float-right">
<button class="btn btn-success shadow-none" @click="createEntryClick(new Date())"><i
class="fas fa-calendar-plus"></i> {{ $t("Create") }}
</button>
<button class="btn btn-primary shadow-none" @click="createAutoPlan(new Date())"><i
class="fas fa-calendar-plus"></i> {{ $t("Auto_Planner") }}
</button>
<a class="btn btn-primary shadow-none" :href="iCalUrl"><i class="fas fa-download"></i>
{{ $t("Export_To_ICal") }}
</a>
@@ -293,10 +300,11 @@
<bottom-navigation-bar :create_links="[{label:$t('Export_To_ICal'), url: iCalUrl, icon:'fas fa-download'}]">
<template #custom_create_functions>
<h6 class="dropdown-header">{{ $t('Meal_Plan')}}</h6>
<h6 class="dropdown-header">{{ $t('Meal_Plan') }}</h6>
<a class="dropdown-item" @click="createEntryClick(new Date())"><i
class="fas fa-calendar-plus fa-fw"></i> {{ $t("Create") }}</a>
</template>
</bottom-navigation-bar>
</div>
</template>
@@ -322,6 +330,8 @@ import {CalendarView, CalendarMathMixin} from "vue-simple-calendar/src/component
import {ApiApiFactory} from "@/utils/openapi/api"
import BottomNavigationBar from "@/components/BottomNavigationBar.vue";
import {useMealPlanStore} from "@/stores/MealPlanStore";
import axios from "axios";
import AutoMealPlanModal from "@/components/AutoMealPlanModal";
const {makeToast} = require("@/utils/utils")
@@ -334,6 +344,7 @@ let SETTINGS_COOKIE_NAME = "mealplan_settings"
export default {
name: "MealPlanView",
components: {
AutoMealPlanModal,
MealPlanEditModal,
MealPlanCard,
CalendarView,
@@ -347,6 +358,16 @@ export default {
mixins: [CalendarMathMixin, ApiMixin, ResolveUrlMixin],
data: function () {
return {
AutoPlan: {
meal_types: [],
keywords: [[]],
servings: 1,
date: Date.now(),
startDay: null,
endDay: null,
shared: [],
addshopping: false
},
showDate: new Date(),
plan_entries: [],
recipe_viewed: {},
@@ -656,7 +677,11 @@ export default {
this.$bvModal.show(`id_meal_plan_edit_modal`)
})
}
},
createAutoPlan() {
this.$bvModal.show(`autoplan-modal`)
},
},
directives: {
hover: {

View File

@@ -240,6 +240,16 @@
v-if="step_index !== recipe.steps.length - 1">
<i class="fa fa-arrow-down fa-fw"></i> {{ $t("Move_Down") }}
</button>
<!-- Show "Hide step ingredients if state is currently set to shown" -->
<button class="dropdown-item" @click="setStepShowIngredientsTable(step, false)"
v-if="step.show_ingredients_table">
<i class="op-icon fa fa-mavon-eye-slash"></i> {{ $t("hide_step_ingredients") }}
</button>
<!-- Show "Show step ingredients if state is currently set to hidden" -->
<button class="dropdown-item" @click="setStepShowIngredientsTable(step, true)"
v-if="! step.show_ingredients_table">
<i class="op-icon fa fa-mavon-eye"></i> {{ $t("show_step_ingredients") }}
</button>
</div>
</div>
</div>
@@ -270,7 +280,6 @@
@click="step.time_visible = true" v-if="!step.time_visible">
<i class="fas fa-plus-circle"></i> {{ $t("Time") }}
</b-button>
<b-button pill variant="primary" size="sm" class="ml-1 mb-1 mb-md-0"
@click="step.ingredients_visible = true" v-if="!step.ingredients_visible">
<i class="fas fa-plus-circle"></i> {{ $t("Ingredients") }}
@@ -770,7 +779,8 @@ import {
ResolveUrlMixin,
StandardToasts,
convertEnergyToCalories,
energyHeading
energyHeading,
getUserPreference
} from "@/utils/utils"
import Multiselect from "vue-multiselect"
import {ApiApiFactory} from "@/utils/openapi/api"
@@ -813,6 +823,7 @@ export default {
show_file_create: false,
step_for_file_create: undefined,
use_plural: false,
user_preferences: undefined,
additional_visible: false,
create_food: undefined,
md_editor_toolbars: {
@@ -858,9 +869,9 @@ export default {
this.searchKeywords("")
this.searchFiles("")
this.searchRecipes("")
this.$i18n.locale = window.CUSTOM_LOCALE
let apiClient = new ApiApiFactory()
this.user_preferences = getUserPreference()
apiClient.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.use_plural = r.data.use_plural
})
@@ -925,7 +936,10 @@ 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)
this.$set(s, "ingredients_visible", s.ingredients.length > 0 || this.recipe.steps.length === 1)
// 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, "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)
@@ -1028,6 +1042,7 @@ export default {
show_as_header: false,
time_visible: false,
ingredients_visible: true,
show_ingredients_table: this.user_preferences.show_step_ingredients,
instruction_visible: true,
step_recipe_visible: false,
file_visible: false,
@@ -1083,6 +1098,9 @@ export default {
this.recipe.steps.splice(new_index < 0 ? 0 : new_index, 0, step)
this.sortSteps()
},
setStepShowIngredientsTable: function (step, show_state) {
step.show_ingredients_table = show_state
},
moveIngredient: function (step, ingredient, new_index) {
step.ingredients.splice(step.ingredients.indexOf(ingredient), 1)
step.ingredients.splice(new_index < 0 ? 0 : new_index, 0, ingredient)
@@ -1281,6 +1299,7 @@ export default {
},
duplicateIngredient: function (step, ingredient, new_index) {
delete ingredient.id
ingredient = JSON.parse(JSON.stringify(ingredient))
step.ingredients.splice(new_index < 0 ? 0 : new_index, 0, ingredient)
}
},

View File

@@ -48,7 +48,7 @@
</tr>
</thead>
<tr v-for="us in user_spaces" :key="us.id">
<td>{{ us.user.username }}</td>
<td>{{ us.user.display_name }}</td>
<td>
<generic-multiselect
class="input-group-text m-0 p-0"
@@ -238,8 +238,8 @@ export default {
apiFactory.retrieveSpace(window.ACTIVE_SPACE_ID).then(r => {
this.space = r.data
})
apiFactory.listUserSpaces().then(r => {
this.user_spaces = r.data
apiFactory.listUserSpaces(1, 25).then(r => { //TODO build proper pagination
this.user_spaces = r.data.results
})
this.loadInviteLinks()
},

View File

@@ -0,0 +1,255 @@
<template>
<b-modal :id="modal_id" size="lg" :title="modal_title" hide-footer aria-label="" @show="showModal">
<h5>{{ $t("Meal_Types") }}</h5>
<div>
<div>
<b-card no-body class="mt-1 p-2"
v-for="(meal_type, k) in AutoPlan.meal_types" :key="meal_type.id">
<b-card-header class="p-2 border-0">
<div class="row">
<div class="col-10">
<h5 class="mt-1 mb-1">
{{ meal_type.icon }} {{
meal_type.name
}}
</h5>
</div>
</div>
<div class="col-12">
<generic-multiselect
@change="genericSelectChanged"
:initial_selection="AutoPlan.keywords[meal_type]"
:parent_variable="`${k}`"
:model="Models.KEYWORD"
:placeholder="$t('Keywords, leave blank to exclude meal type')"
:limit="50"
/>
</div>
</b-card-header>
</b-card>
</div>
<div class="row-cols-1 m-3">
<b-form-input class="w-25 m-2 mb-0" :type="'number'" v-model="AutoPlan.servings"></b-form-input>
<small tabindex="-1" class="m-2 mt-0 form-text text-muted">{{ $t("Servings") }}</small>
</div>
<b-form-group class="mt-3">
<generic-multiselect
required
@change="AutoPlan.shared = $event.val"
parent_variable="entryEditing.shared"
:label="'display_name'"
:model="Models.USER_NAME"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Share')"
:limit="10"
:multiple="true"
:initial_selection="AutoPlan.shared"
></generic-multiselect>
<small tabindex="-1" class="form-text text-muted">{{ $t("Share") }}</small>
</b-form-group>
<b-input-group v-if="!autoMealPlan">
<b-form-checkbox id="AddToShopping" v-model="mealplan_settings.addshopping"/>
<small tabindex="-1" class="form-text text-muted">{{
$t("AddToShopping")
}}</small>
</b-input-group>
<div class="">
<div class="row m-3 mb-0">
<b-form-datepicker class="col" :value-as-date="true" v-model="AutoPlan.startDay"></b-form-datepicker>
<div class="col"></div>
<b-form-datepicker class="col" :value-as-date="true" v-model="AutoPlan.endDay"></b-form-datepicker>
</div>
<div class="row align-top m-3 mt-0">
<small tabindex="-1" class="col align-text-top text-muted">{{ $t("Start Day") }}</small>
<div class="col"></div>
<small tabindex="-1" class="col align-self-end text-muted">{{ $t("End Day") }}</small>
</div>
</div>
</div>
<div class="row mt-3 mb-3">
<div class="col-12">
<b-button class="float-right" variant="primary" @click="createPlan" :disabled="loading">
<b-spinner small v-if="loading"></b-spinner>
{{ $t("Create Meal Plan") }}
</b-button>
<b-button class="" variant="danger" @click="exitPlan">{{ $t("Exit") }}</b-button>
</div>
</div>
</b-modal>
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import GenericMultiselect from "@/components/GenericMultiselect"
import {ApiMixin} from "@/utils/utils"
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import VueCookies from "vue-cookies";
import moment from "moment/moment";
import {useMealPlanStore} from "@/stores/MealPlanStore";
const {ApiApiFactory} = require("@/utils/openapi/api")
const {StandardToasts} = require("@/utils/utils")
Vue.use(BootstrapVue)
Vue.use(VueCookies)
let MEALPLAN_COOKIE_NAME = "mealplan_settings"
export default {
name: "AutoMealPlanModal",
components: {
GenericMultiselect
},
props: {
modal_title: String,
modal_id: {
type: String,
default: "autoplan-modal",
},
current_period: Object
},
mixins: [ApiMixin],
data() {
return {
AutoPlan: {
meal_types: [],
keywords: [[]],
servings: 1,
date: Date.now(),
startDay: null,
endDay: null,
shared: [],
addshopping: false
},
mealplan_settings: {
addshopping: false,
},
loading: false,
}
},
watch: {
mealplan_settings: {
handler(newVal) {
this.$cookies.set(MEALPLAN_COOKIE_NAME, this.mealplan_settings)
},
deep: true,
},
},
mounted: function () {
useUserPreferenceStore().updateIfStaleOrEmpty()
},
computed: {
autoMealPlan: function () {
return useUserPreferenceStore().getStaleData()?.mealplan_autoadd_shopping
},
},
methods: {
genericSelectChanged: function (obj) {
this.AutoPlan.keywords[obj.var] = obj.val
},
showModal() {
if (this.$cookies.isKey(MEALPLAN_COOKIE_NAME)) {
this.mealplan_settings = Object.assign({}, this.mealplan_settings, this.$cookies.get(MEALPLAN_COOKIE_NAME))
}
this.refreshMealTypes()
this.AutoPlan.servings = 1
this.AutoPlan.startDay = new Date()
this.AutoPlan.endDay = this.current_period.periodEnd
useUserPreferenceStore().getData().then(userPreference => {
this.AutoPlan.shared = userPreference.plan_share
})
this.AutoPlan.addshopping = this.mealplan_settings.addshopping
this.loading = false
},
sortMealTypes() {
this.meal_types.forEach(function (element, index) {
element.order = index
})
let updated = 0
this.meal_types.forEach((meal_type) => {
let apiClient = new ApiApiFactory()
apiClient
.updateMealType(this.AutoPlan.meal_type, meal_type)
.then((e) => {
if (updated === this.meal_types.length - 1) {
this.periodChangedCallback(this.current_period)
} else {
updated++
}
})
.catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
})
},
refreshMealTypes() {
let apiClient = new ApiApiFactory()
Promise.resolve(apiClient.listMealTypes().then((result) => {
result.data.forEach((meal_type) => {
meal_type.editing = false
})
this.AutoPlan.meal_types = result.data
})).then(() => {
let mealArray = this.AutoPlan.meal_types
for (let i = 0; i < mealArray.length; i++) {
this.AutoPlan.keywords[i] = [];
}
}
)
},
createPlan() {
if (!this.loading) {
this.loading = true
let requests = []
for (let i = 0; i < this.AutoPlan.meal_types.length; i++) {
if (this.AutoPlan.keywords[i].length === 0) continue
requests.push(this.autoPlanThread(this.AutoPlan, i))
}
Promise.allSettled(requests).then(r => {
this.refreshEntries()
this.loading = false
this.$bvModal.hide(`autoplan-modal`)
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)
this.loading = false
})
}
},
async autoPlanThread(autoPlan, mealTypeIndex) {
let apiClient = new ApiApiFactory()
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],
"servings": autoPlan.servings,
"shared": autoPlan.shared,
"addshopping": autoPlan.addshopping
}
return apiClient.createAutoPlanViewSet(data)
},
refreshEntries() { //TODO move properly to MealPLanStore (save period for default refresh)
let date = this.current_period
useMealPlanStore().refreshFromAPI(moment(date.periodStart).format("YYYY-MM-DD"), moment(date.periodEnd).format("YYYY-MM-DD"))
},
exitPlan() {
this.$bvModal.hide(`autoplan-modal`)
}
},
}
</script>
<style scoped></style>

View File

@@ -25,6 +25,9 @@
<b-form-group :label="$t('Plural')" description="">
<b-form-input v-model="food.plural_name"></b-form-input>
</b-form-group>
<b-form-group :label="$t('Description')" description="">
<b-form-textarea v-model="food.description" rows="2"></b-form-textarea>
</b-form-group>
<!-- Food properties -->
@@ -167,6 +170,10 @@
></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('URL')" description="">
<b-form-input v-model="food.url"></b-form-input>
</b-form-group>
<b-form-group :description="$t('OnHand_help')">
<b-form-checkbox v-model="food.food_onhand">{{ $t('OnHand') }}</b-form-checkbox>
</b-form-group>
@@ -306,6 +313,7 @@ export default {
description: "",
shopping: false,
recipe: null,
url: '',
properties: [],
properties_food_amount: 100,
properties_food_unit: {name: 'g'},

View File

@@ -30,21 +30,13 @@
</td>
<td @click="done">
<template v-if="ingredient.food !== null">
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)" v-if="ingredient.food.recipe !== null" target="_blank" rel="noopener noreferrer">{{
ingredient.food.name
}}</a>
<template v-if="ingredient.food.recipe === null">
<template>
<template v-if="ingredient.food.plural_name === '' || ingredient.food.plural_name === null">
<span>{{ ingredient.food.name }}</span>
</template>
<template v-else>
<span v-if="ingredient.always_use_plural_food">{{ ingredient.food.plural_name }}</span>
<span v-else-if="ingredient.no_amount">{{ ingredient.food.name }}</span>
<span v-else-if="ingredient.amount * this.ingredient_factor > 1">{{ ingredient.food.plural_name }}</span>
<span v-else>{{ ingredient.food.name }}</span>
</template>
</template>
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)" v-if="ingredient.food.recipe !== null" target="_blank" rel="noopener noreferrer">
{{ ingredientName(ingredient) }}
</a>
<a :href="ingredient.food.url" v-else-if="ingredient.food.url !== ''" target="_blank" rel="noopener noreferrer">
{{ ingredientName(ingredient) }}</a>
<template v-else>
<span>{{ ingredientName(ingredient) }}</span>
</template>
</template>
</td>
@@ -62,7 +54,7 @@
</template>
<script>
import { calculateAmount, ResolveUrlMixin } from "@/utils/utils"
import {calculateAmount, ResolveUrlMixin} from "@/utils/utils"
import Vue from "vue"
import VueSanitize from "vue-sanitize"
@@ -73,8 +65,8 @@ export default {
name: "IngredientComponent",
props: {
ingredient: Object,
ingredient_factor: { type: Number, default: 1 },
detailed: { type: Boolean, default: true },
ingredient_factor: {type: Number, default: 1},
detailed: {type: Boolean, default: true},
},
mixins: [ResolveUrlMixin],
data() {
@@ -83,7 +75,8 @@ export default {
}
},
watch: {},
mounted() {},
mounted() {
},
methods: {
calculateAmount: function (x) {
return this.$sanitize(calculateAmount(x, this.ingredient_factor))
@@ -92,6 +85,20 @@ export default {
done: function () {
this.$emit("checked-state-changed", this.ingredient)
},
ingredientName: function (ingredient) {
if (ingredient.food.plural_name == null || ingredient.food.plural_name === '') {
return ingredient.food.name
}
if (ingredient.always_use_plural_food) {
return ingredient.food.plural_name
} else if (ingredient.no_amount) {
return ingredient.food.name
} else if (ingredient.amount * this.ingredient_factor > 1) {
return ingredient.food.plural_name
} else {
return ingredient.food.name
}
}
},
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<template v-if="form_component !== undefined">
<template v-if="form_component !== undefined && (action === Actions.UPDATE || action === Actions.CREATE)">
<component :is="form_component" :id="'modal_' + id" :show="show" @hidden="cancelAction" :item1="item1"></component>
</template>
<template v-else>

View File

@@ -1,28 +1,28 @@
<template>
<div>
<iframe :src="pdfUrl" width="100%" height="700px" style="border: none;"></iframe>
</div>
<div>
<iframe :src="pdfUrl" width="100%" height="700px" style="border: none;"></iframe>
</div>
</template>
<script>
import {resolveDjangoUrl, ResolveUrlMixin} from "@/utils/utils";
import {resolveDjangoStatic, resolveDjangoUrl, ResolveUrlMixin} from "@/utils/utils";
export default {
name: 'PdfViewer',
mixins: [
ResolveUrlMixin
],
props: {
recipe: Object,
},
computed: {
pdfUrl: function() {
return '/static/pdfjs/viewer.html?file=' + resolveDjangoUrl('api_get_recipe_file', (this.recipe.id))
}
},
name: 'PdfViewer',
mixins: [
ResolveUrlMixin
],
props: {
recipe: Object,
},
computed: {
pdfUrl: function () {
return resolveDjangoStatic('pdfjs/viewer.html?file=' + resolveDjangoUrl('api_get_recipe_file', (this.recipe.id)))
}
},
}
</script>

View File

@@ -8,13 +8,6 @@
<h5><i class="fas fa-database"></i> {{ $t('Properties') }}</h5>
</b-col>
<b-col class="text-right">
<span v-if="!show_total">{{ $t('per_serving') }} </span>
<span v-if="show_total">{{ $t('total') }} </span>
<a href="#" @click="show_total = !show_total">
<i class="fas fa-toggle-on" v-if="!show_total"></i>
<i class="fas fa-toggle-off" v-if="show_total"></i>
</a>
<div v-if="hasRecipeProperties && hasFoodProperties">
<span v-if="!show_recipe_properties">{{ $t('Food') }} </span>
@@ -31,13 +24,20 @@
<table class="table table-bordered table-sm">
<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>
<td style="border-top: none"></td>
</tr>
<tr v-for="p in property_list" v-bind:key="`id_${p.id}`">
<td>
{{ p.icon }} {{ p.name }}
</td>
<td class="text-right">{{ get_amount(p.property_amount) }}</td>
<td class="text-right">{{ p.property_amount_per_serving }}</td>
<td class="text-right">{{ p.property_amount_total }}</td>
<td class=""> {{ p.unit }}</td>
<td class="align-middle text-center" v-if="!show_recipe_properties">
@@ -97,7 +97,6 @@ export default {
selected_property: undefined,
selected_food: undefined,
show_food_edit_modal: false,
show_total: false,
show_recipe_properties: false,
}
},
@@ -128,9 +127,11 @@ export default {
'description': rp.property_type.description,
'icon': rp.property_type.icon,
'food_values': [],
'property_amount': rp.property_amount,
'property_amount_per_serving': rp.property_amount,
'property_amount_total': rp.property_amount * this.recipe.servings * (this.servings / this.recipe.servings),
'missing_value': false,
'unit': rp.property_type.unit,
'type': rp.property_type,
}
)
})
@@ -143,14 +144,27 @@ export default {
'description': fp.description,
'icon': fp.icon,
'food_values': fp.food_values,
'property_amount': fp.total_value,
'property_amount_per_serving': fp.total_value / this.recipe.servings,
'property_amount_total': fp.total_value * (this.servings / this.recipe.servings),
'missing_value': fp.missing_value,
'unit': fp.unit,
'type': fp,
}
)
}
}
return pt_list
function compare(a,b){
if(a.type.order > b.type.order){
return 1
}
if(a.type.order < b.type.order){
return -1
}
return 0
}
return pt_list.sort(compare)
}
},
mounted() {
@@ -159,19 +173,6 @@ export default {
}
},
methods: {
get_amount: function (amount) {
if (this.show_total) {
return (amount * (this.servings / this.recipe.servings)).toLocaleString(window.CUSTOM_LOCALE, {
'maximumFractionDigits': 2,
'minimumFractionDigits': 2
})
} else {
return (amount / this.recipe.servings).toLocaleString(window.CUSTOM_LOCALE, {
'maximumFractionDigits': 2,
'minimumFractionDigits': 2
})
}
},
openFoodEditModal: function (food) {
console.log(food)
let apiClient = ApiApiFactory()

View File

@@ -149,7 +149,8 @@
<div class="row text-center d-print-none" style="margin-top: 3vh; margin-bottom: 3vh"
v-if="share_uid !== 'None' && !loading">
<div class="col col-md-12">
<import-tandoor></import-tandoor> <br/>
<import-tandoor></import-tandoor>
<br/>
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)" class="mt-3">{{ $t("Report Abuse") }}</a>
</div>
</div>
@@ -236,6 +237,7 @@ export default {
},
props: {
recipe_id: Number,
recipe_obj: {type: Object, default: null},
show_context_menu: {type: Boolean, default: true},
enable_keyword_links: {type: Boolean, default: true},
show_recipe_switcher: {type: Boolean, default: true},
@@ -247,7 +249,14 @@ export default {
},
},
mounted() {
this.loadRecipe(this.recipe_id)
if (this.recipe_obj !== null) {
this.recipe = this.rootrecipe = this.recipe_obj
this.prepareView()
} else {
this.loadRecipe(this.recipe_id)
}
this.$i18n.locale = window.CUSTOM_LOCALE
this.requestWakeLock()
window.addEventListener('resize', this.handleResize);
@@ -291,33 +300,36 @@ export default {
},
loadRecipe: function (recipe_id) {
apiLoadRecipe(recipe_id).then((recipe) => {
let total_time = 0
for (let step of recipe.steps) {
for (let ingredient of step.ingredients) {
this.$set(ingredient, "checked", false)
}
step.time_offset = total_time
total_time += step.time
}
// set start time only if there are any steps with timers (otherwise no timers are rendered)
if (total_time > 0) {
this.start_time = moment().format("yyyy-MM-DDTHH:mm")
}
if (recipe.image === null) this.printReady()
this.recipe = this.rootrecipe = recipe
this.servings = this.servings_cache[this.rootrecipe.id] = recipe.servings
this.loading = false
setTimeout(() => {
this.handleResize()
}, 100)
this.prepareView()
})
},
prepareView: function () {
let total_time = 0
for (let step of this.recipe.steps) {
for (let ingredient of step.ingredients) {
this.$set(ingredient, "checked", false)
}
step.time_offset = total_time
total_time += step.time
}
// set start time only if there are any steps with timers (otherwise no timers are rendered)
if (total_time > 0) {
this.start_time = moment().format("yyyy-MM-DDTHH:mm")
}
if (this.recipe.image === null) this.printReady()
this.servings = this.servings_cache[this.rootrecipe.id] = this.recipe.servings
this.loading = false
setTimeout(() => {
this.handleResize()
}, 100)
},
updateStartTime: function (e) {
this.start_time = e
},

View File

@@ -31,6 +31,12 @@
</b-form-checkbox>
</b-form-group>
<b-form-group :description="$t('show_step_ingredients_setting_help')">
<b-form-checkbox v-model="user_preferences.show_step_ingredients" @change="updateSettings(false);">
{{ $t('show_step_ingredients_setting') }}
</b-form-checkbox>
</b-form-group>
<hr/>

View File

@@ -33,7 +33,7 @@
<div class="row">
<!-- ingredients table -->
<div class="col col-md-4"
v-if="step.ingredients.length > 0 && (recipe.steps.length > 1 || force_ingredients)">
v-if="step.show_ingredients_table && step.ingredients.length > 0 && (recipe.steps.length > 1 || force_ingredients)">
<table class="table table-sm">
<ingredients-card :steps="[step]" :ingredient_factor="ingredient_factor"
@checked-state-changed="$emit('checked-state-changed', $event)"/>
@@ -124,10 +124,7 @@
</template>
<script>
import {calculateAmount} from "@/utils/utils"
import {GettextMixin} from "@/utils/utils"
import {calculateAmount, GettextMixin, getUserPreference} from "@/utils/utils"
import CompileComponent from "@/components/CompileComponent"
import IngredientsCard from "@/components/IngredientsCard"
import Vue from "vue"

View File

@@ -4,6 +4,8 @@ import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
function loadLocaleMessages () {
const start_time = Date.now();
console.log('started loading locale messages')
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages = {}
locales.keys().forEach(key => {
@@ -13,6 +15,7 @@ function loadLocaleMessages () {
messages[locale] = locales(key)
}
})
console.log('finished loading messages in ', Date.now() - start_time, ' ms')
return messages
}

View File

@@ -1,21 +1,21 @@
{
"warning_feature_beta": "Tato funkce je momentálně ve fázi Beta (testování). Očekávejte, prosím, chyby. V budoucnosti může dojít i ke ztrátě dat.",
"err_fetching_resource": "",
"err_creating_resource": "",
"err_updating_resource": "",
"err_deleting_resource": "",
"err_deleting_protected_resource": "",
"err_moving_resource": "",
"err_merging_resource": "",
"success_fetching_resource": "",
"success_creating_resource": "",
"success_updating_resource": "",
"success_deleting_resource": "",
"success_moving_resource": "",
"success_merging_resource": "",
"err_fetching_resource": "Během načítání došlo k chybě!",
"err_creating_resource": "Během vytváření došlo k chybě!",
"err_updating_resource": "Během úprav došlo k chybě!",
"err_deleting_resource": "Během mazání došlo k chybě!",
"err_deleting_protected_resource": "Položka, kterou chcete smazat, je stále používána a nemůže být odstraněna.",
"err_moving_resource": "Během přesunu došlo k chybě!",
"err_merging_resource": "Během slučování došlo k chybě!",
"success_fetching_resource": "Úspěšně načteno!",
"success_creating_resource": "Úspěšně vytvořeno!",
"success_updating_resource": "Úspěšně upraveno!",
"success_deleting_resource": "Úspěšně smazáno!",
"success_moving_resource": "Úspěšně přesunuto!",
"success_merging_resource": "Úspěšně sloučeno!",
"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": "",
"food_inherit_info": "Pole potravin, která budou standardně zděděna.",
"facet_count_info": "Zobraz počet receptů u filtrů vyhledávání.",
"step_time_minutes": "Nastavte čas v minutách",
"confirm_delete": "Jste si jisti že chcete odstranit tento {objekt}?",
@@ -102,13 +102,13 @@
"Failure": "Selhání",
"Protected": "Chráněný",
"Ingredients": "Ingredience",
"Supermarket": "",
"Supermarket": "Obchod",
"Categories": "Kategorie",
"Category": "Kategorie",
"Selected": "Vybrané",
"min": "min",
"Servings": "Porce",
"Waiting": "",
"Waiting": "Čekající",
"Preparation": "Příprava",
"External": "Externí",
"Size": "Velikost",
@@ -144,26 +144,26 @@
"create_rule": "a vytvořit automatizaci",
"move_selection": "Vybrat nadřazený {type} kam {source} přesunout.",
"merge_selection": "Nahradit všechny výskyty {source} za vybraný {type}.",
"Root": "",
"Root": "Kořen",
"Ignore_Shopping": "Ignorovat nákupní seznam",
"Shopping_Category": "Kategorie nákupního seznamu",
"Shopping_Categories": "Kategorie nákupního seznamu",
"Edit_Food": "Upravit jídlo",
"Move_Food": "Přesunout jídlo",
"New_Food": "Nové jídlo",
"Hide_Food": "Skrýt jídlo",
"Food_Alias": "Přezdívka jídla",
"Edit_Food": "Upravit potravinu",
"Move_Food": "Přesunout potravinu",
"New_Food": "Nová potravina",
"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",
"Delete_Food": "Smazat jídlo",
"Delete_Food": "Smazat potravinu",
"No_ID": "ID nenalezeno, odstranění není možné.",
"Meal_Plan_Days": "Budoucí jídelníčky",
"merge_title": "Sloučit {type}",
"move_title": "Přesunout {type}",
"Food": "Jídlo",
"Food": "Potravina",
"Original_Text": "Původní text",
"Recipe_Book": "Kuchařka",
"del_confirmation_tree": "",
"del_confirmation_tree": "Jste si jistí, že chcete odstranit {source} se všemi pořazenými ?",
"delete_title": "Smazat {type}",
"create_title": "Nový {type}",
"edit_title": "Upravit {type}",
@@ -179,7 +179,7 @@
"No_Results": "Žádné výsledky",
"New_Unit": "Nová jednotka",
"Create_New_Shopping Category": "Vytvořit novou nákupní kategorii",
"Create_New_Food": "Přidat nové jídlo",
"Create_New_Food": "Přidat novou potravinu",
"Create_New_Keyword": "Přidat nové klíčové slovo",
"Create_New_Unit": "Přidat novou jednotku",
"Create_New_Meal_Type": "Přidat nový druh jídla",
@@ -196,7 +196,7 @@
"Text": "Text",
"Shopping_list": "Nákupní seznam",
"Added_by": "Přidáno uživatelem",
"Added_on": "",
"Added_on": "Přidáno v",
"AddToShopping": "Přidat do nákupního seznamu",
"IngredientInShopping": "Tato ingredience je na vašem nákupním seznamu.",
"NotInShopping": "{food} není na vašem nákupním seznamu.",
@@ -231,7 +231,7 @@
"AddFoodToShopping": "Přidat {food} na váš nákupní seznam",
"RemoveFoodFromShopping": "Odstranit {food} z nákupního seznamu",
"DeleteShoppingConfirm": "Jste si jistí, že chcete odstranit všechno {food} z nákupního seznamu?",
"IgnoredFood": "",
"IgnoredFood": "{food} bude ignorovat nákup.",
"Add_Servings_to_Shopping": "Přidat {servings} porce na nákupní seznam",
"Week_Numbers": "Číslo týdne",
"Show_Week_Numbers": "Zobrazit čísla týdnů?",
@@ -245,21 +245,21 @@
"Current_Period": "Současné období",
"Next_Day": "Následující den",
"Previous_Day": "Předchozí den",
"Inherit": "",
"InheritFields": "",
"FoodInherit": "",
"Inherit": "Propsat",
"InheritFields": "Propsat hodnoty polí",
"FoodInherit": "Propisovatelná pole potraviny",
"ShowUncategorizedFood": "Zobrazit nedefinované",
"GroupBy": "Seskupit podle",
"Language": "Jazyk",
"Theme": "Téma",
"SupermarketCategoriesOnly": "",
"MoveCategory": "Předunout do: ",
"SupermarketCategoriesOnly": "Pouze kategorie obchodu",
"MoveCategory": "Přesunout do: ",
"CountMore": "...+{count} víc",
"IgnoreThis": "Nikdy automaticky nepřídávat {food} na nákupní seznam",
"DelayFor": "Odložit na {hours} hodin",
"Warning": "Varování",
"NoCategory": "Není vybrána žádná kategorie.",
"InheritWarning": "",
"InheritWarning": "{food} se propisuje, změny nemusí setrvat.",
"ShowDelayed": "Zobrazit odložené položky",
"Completed": "Dokončeno",
"OfflineAlert": "Jste offline, nákupní seznam nemusí být synchronizován.",
@@ -267,7 +267,7 @@
"shopping_auto_sync": "Automatická synchronizace",
"one_url_per_line": "Jeden URL odkaz na řádek",
"mealplan_autoadd_shopping": "Automaticky přidat jídelníček",
"mealplan_autoexclude_onhand": "Nezahrnovat jídlo k dispozici",
"mealplan_autoexclude_onhand": "Nezahrnovat potraviny k dispozici",
"mealplan_autoinclude_related": "Přidat podobné recepty",
"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.",
@@ -277,7 +277,7 @@
"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ů.",
"default_delay_desc": "Výchozí odložení položek v nákupním seznamu v hodinách.",
"filter_to_supermarket": "",
"filter_to_supermarket": "Filtrovat podle obchodu",
"Coming_Soon": "Již brzy",
"Auto_Planner": "Automatický plánovač",
"New_Cookbook": "Nová kuchařka",
@@ -299,7 +299,7 @@
"CategoryName": "Název kategorie",
"SupermarketName": "Název obchodu",
"CategoryInstruction": "Přetáhnutím kategorií změníte pořadí, ve kterém se zobrazují v nákupním seznamu.",
"shopping_recent_days_desc": "",
"shopping_recent_days_desc": "Počet dní k zobrazení posledních přidaných položek na nákupním seznamu.",
"shopping_recent_days": "Nedávné dny",
"download_pdf": "Stáhnout PDF",
"download_csv": "Stáhnout CSV",
@@ -307,8 +307,8 @@
"csv_delim_label": "Oddělovač záznamů v CSV",
"SuccessClipboard": "Nákupní seznam byl zkopírován do schránky",
"copy_to_clipboard": "Zkopírovat do schránky",
"csv_prefix_help": "",
"csv_prefix_label": "",
"csv_prefix_help": "Prefix přidaný ke zkopírovanému seznamu do schránky.",
"csv_prefix_label": "Prefix seznamu",
"copy_markdown_table": "Kopírovat jako Markdown tabulku",
"in_shopping": "V nákupním seznamu",
"DelayUntil": "Odložit do",
@@ -376,7 +376,7 @@
"Website": "Web",
"App": "Aplikace",
"Message": "Zpráva",
"Bookmarklet": "Bookmarklet",
"Bookmarklet": "Skript v záložce",
"Sticky_Nav": "Připnout navigační panel",
"Sticky_Nav_Help": "Vždy zobrazit navigační panel na vrchu stránky.",
"Nav_Color": "Barva navigačního panelu",
@@ -392,66 +392,66 @@
"search_import_help_text": "Importovat recept z externí webové stránky nebo aplikace.",
"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": "",
"reset_children": "Resetovat propsání podřízených",
"reset_children_help": "",
"reset_food_inheritance": "",
"reset_food_inheritance": "Resetovat propisování",
"reset_food_inheritance_info": "",
"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.",
"substitute_siblings": "",
"substitute_children": "",
"substitute_siblings": "Související náhrady",
"substitute_children": "Podřízené náhrady",
"SubstituteOnHand": "Máte k dispozici náhradu.",
"ChildInheritFields": "",
"ChildInheritFields_help": "",
"InheritFields_help": "",
"show_ingredient_overview": "",
"Ingredient Overview": "",
"ChildInheritFields": "Propisovaná pole podřízených",
"ChildInheritFields_help": "Podřízeným se budou standardně propisovat tato pole.",
"InheritFields_help": "Hodnoty těchto polí budou propsány z nadřazených (Výjimka: prázdné nákupní kategorie nejsou propsány)",
"show_ingredient_overview": "Zobrazit seznam všech ingrediencí na začátku receptu.",
"Ingredient Overview": "Přehled ingrediencí",
"last_viewed": "Naposledy zobrazeno",
"created_on": "Vytvořeno",
"updatedon": "Upraveno",
"Imported_From": "",
"advanced_search_settings": "",
"nothing_planned_today": "",
"no_pinned_recipes": "",
"Planned": "",
"Pinned": "",
"Imported": "",
"Quick actions": "",
"Imported_From": "Importováno z",
"advanced_search_settings": "Rozšířené vyhledávání",
"nothing_planned_today": "Dnes nemáte nic naplánováno!",
"no_pinned_recipes": "Nemáte žádné připnuté recepty!",
"Planned": "Naplánované",
"Pinned": "Připnuté",
"Imported": "Importované",
"Quick actions": "Rychlé akce",
"Ratings": "Hodnocení",
"Internal": "",
"Internal": "Interní",
"Units": "Jednotky",
"Manage_Emails": "",
"Manage_Emails": "Spravovat emaily",
"Change_Password": "Změna hesla",
"Social_Authentication": "",
"Social_Authentication": "Přihlašování pomocí účtů sociálních sítí",
"Random Recipes": "Náhodné recepty",
"parameter_count": "",
"select_keyword": "",
"add_keyword": "",
"select_file": "",
"select_recipe": "",
"select_unit": "",
"select_food": "",
"remove_selection": "",
"empty_list": "",
"Select": "",
"Supermarkets": "",
"parameter_count": "Parametr {count}",
"select_keyword": "Vybrat klíčové slovo",
"add_keyword": "Přidat klíčové slovo",
"select_file": "Vybrat soubor",
"select_recipe": "Vybrat recept",
"select_unit": "Vybrat jednotku",
"select_food": "Vybrat potravinu",
"remove_selection": "Odznačit",
"empty_list": "Seznam je prázdný.",
"Select": "Vybrat",
"Supermarkets": "Obchody",
"User": "Uživatel",
"Username": "Uživatelské jméno",
"First_name": "Jméno",
"Last_name": "Příjmení",
"Keyword": "",
"Advanced": "",
"Page": "",
"Single": "",
"Multiple": "",
"Reset": "",
"Disabled": "",
"Disable": "",
"Options": "",
"Create Food": "",
"create_food_desc": "",
"additional_options": "",
"Keyword": "Klíčové slovo",
"Advanced": "Rozšířené",
"Page": "Stránka",
"Single": "Jednoduchý",
"Multiple": "Vícenásobný",
"Reset": "Resetovat",
"Disabled": "Deaktivované",
"Disable": "Deaktivovat",
"Options": "Možnosti",
"Create Food": "Vytvořit potravinu",
"create_food_desc": "Vytvořit potravinu a propojit ji s tímto receptem.",
"additional_options": "Rozšířené možnosti",
"Importer_Help": "Nápověda k importu z této aplikace:",
"Documentation": "Dokumentace",
"Select_App_To_Import": "Vyberte aplikaci, ze které chcete importovat",
@@ -461,27 +461,27 @@
"Export_Not_Yet_Supported": "Export není zatím podporován",
"Import_Result_Info": "{imported} z {total} receptů naimportováno",
"Recipes_In_Import": "Receptů v importním souboru",
"Toggle": "",
"Import_Error": "",
"Warning_Delete_Supermarket_Category": "",
"New_Supermarket": "",
"New_Supermarket_Category": "",
"Are_You_Sure": "",
"Valid Until": "",
"Split_All_Steps": "",
"Combine_All_Steps": "",
"Plural": "",
"plural_short": "",
"Use_Plural_Unit_Always": "",
"Use_Plural_Unit_Simple": "",
"Use_Plural_Food_Always": "",
"Use_Plural_Food_Simple": "",
"plural_usage_info": "",
"Toggle": "Přepnout",
"Import_Error": "Během importu došlo k chybě. Pro více informací rozbalte Detaily na konci stránky.",
"Warning_Delete_Supermarket_Category": "Vymazáním kategorie obchodu dojde k odstranění všech vazeb na potraviny. Jste si jistí?",
"New_Supermarket": "Vytvořit nový obchod",
"New_Supermarket_Category": "Vytvořit novou kategorii obchodu",
"Are_You_Sure": "Jste si jistí?",
"Valid Until": "Platné do",
"Split_All_Steps": "Rozdělit každý řádek do samostatného kroku.",
"Combine_All_Steps": "Zkombinovat všechny kroky do jednoho kroku.",
"Plural": "Množné číslo",
"plural_short": "množné číslo",
"Use_Plural_Unit_Always": "Vždy použít množné číslo pro jednotku",
"Use_Plural_Unit_Simple": "Dynamicky použít množné číslo pro jednotku",
"Use_Plural_Food_Always": "Použít u potraviny vždy množné číslo",
"Use_Plural_Food_Simple": "Použít u potraviny množné číslo dynamicky",
"plural_usage_info": "Použít množné číslo pro jednotky a potraviny v tomto prostoru.",
"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 jídel, jednotek a další spravované komunitou, a vylepšete tak svoji sbírku receptů.",
"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ů.",
"Update_Existing_Data": "Aktualizovat existující data",
"Use_Metric": "Používat metrické jednotky",
"Learn_More": "Zjistit víc",
@@ -494,5 +494,6 @@
"Property": "Vlastnost",
"Conversion": "Převod",
"Properties": "Vlastnosti",
"recipe_property_info": "Můžete také přidávat vlastnosti k Vašim jídlům. Hodnoty budou automaticky přepočteny na základě Vašeho receptu!"
"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"
}

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