Compare commits

...

417 Commits
1.5.2 ... 1.5.9

Author SHA1 Message Date
vabene1111
ad1e64fb9a Merge pull request #2753 from smilerz/patch_custom_icon
Patch custom icon
2023-11-28 17:34:28 +01:00
smilerz
add600f3ca safely get icon from request.space in tag logo_url 2023-11-28 08:55:27 -06:00
Mahmoud Aljouhari
ad036d7e6c Translated using Weblate (Arabic)
Currently translated at 20.6% (109 of 528 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/ar/
2023-11-28 11:03:17 +00:00
vabene1111
d7017902ab Merge branch 'develop' 2023-11-27 22:56:13 +01:00
vabene1111
35743e8be9 fixed constraint 2023-11-27 22:47:56 +01:00
vabene1111
75523c06f6 Merge branch 'develop' 2023-11-27 22:34:02 +01:00
vabene1111
7e2aee53db fixed import ingredient edit modal cleanup 2023-11-27 22:33:48 +01:00
vabene1111
9c74730461 added first draft of property editor 2023-11-27 22:20:09 +01:00
vabene1111
977d2822bc added ability to set custom logo in navbar 2023-11-27 20:29:01 +01:00
vabene1111
31f93285d8 Merge branch 'feature/space-icon' into develop 2023-11-27 20:23:45 +01:00
vabene1111
da9002a7fd Revert "WIP"
This reverts commit 58e70c982e.
2023-11-27 20:23:36 +01:00
vabene1111
1bf7af7027 hide properties if none are present in recipe 2023-11-27 20:21:18 +01:00
vabene1111
1145a8cf26 fixed PR 2693 2023-11-27 20:15:41 +01:00
vabene1111
5e918297f8 Merge pull request #2693 from blowk/develop
Update to import tags on mealie and chowdown recipes
2023-11-27 20:07:09 +01:00
vabene1111
69adad70c8 Merge pull request #2727 from jrester/dark-theme-fixes
Improve dark theme
2023-11-27 20:05:30 +01:00
vabene1111
d3905f1e80 Merge pull request #2709 from TandoorRecipes/dependabot/pip/markdown-3.5.1
Bump markdown from 3.4.3 to 3.5.1
2023-11-27 20:01:49 +01:00
vabene1111
1a1ff52725 Merge pull request #2707 from TandoorRecipes/dependabot/pip/boto3-1.28.75
Bump boto3 from 1.28.57 to 1.28.75
2023-11-27 20:01:24 +01:00
vabene1111
731958fdaa Merge pull request #2708 from TandoorRecipes/dependabot/pip/recipe-scrapers-14.52.0
Bump recipe-scrapers from 14.36.1 to 14.52.0
2023-11-27 20:01:07 +01:00
vabene1111
8a19c8eeb0 Merge pull request #2711 from TandoorRecipes/dependabot/pip/whitenoise-6.6.0
Bump whitenoise from 6.5.0 to 6.6.0
2023-11-27 20:00:53 +01:00
vabene1111
4e7368f7b6 Merge pull request #2710 from TandoorRecipes/dependabot/pip/pytest-django-4.6.0
Bump pytest-django from 4.5.2 to 4.6.0
2023-11-27 20:00:46 +01:00
vabene1111
b00f1009a6 Merge pull request #2731 from TandoorRecipes/dependabot/npm_and_yarn/vue/axios-1.6.0
Bump axios from 1.5.0 to 1.6.0 in /vue
2023-11-27 20:00:00 +01:00
vabene1111
2d3ecaaf3c Merge pull request #2700 from TandoorRecipes/dependabot/npm_and_yarn/vue/browserify-sign-4.2.2
Bump browserify-sign from 4.2.1 to 4.2.2 in /vue
2023-11-27 19:59:44 +01:00
vabene1111
339049c785 Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2023-11-27 19:59:14 +01:00
vabene1111
d0481ed18c fixed recipe card description overlay 2023-11-27 19:59:06 +01:00
vabene1111
bcee66c7a4 fixed mela recipes importer 2023-11-27 19:58:56 +01:00
vabene1111
7e993ca50e Merge pull request #2686 from TandoorRecipes/dependabot/npm_and_yarn/vue/babel/traverse-7.23.2
Bump @babel/traverse from 7.22.17 to 7.23.2 in /vue
2023-11-27 19:58:35 +01:00
vabene1111
638dc845c0 Merge pull request #2682 from djstini/fix-import
open-data-import further duplicate handling
2023-11-27 19:56:19 +01:00
vabene1111
4aea0fea8c Merge pull request #2685 from harry48225/delete-confirmation
Increase specificity of the delete confirmation dialog
2023-11-27 19:50:26 +01:00
vabene1111
2ba94df9a8 Merge pull request #2691 from gorrilla10101/patch-1
Update authentication.md
2023-11-27 19:45:25 +01:00
Spreez
5723d87768 Translated using Weblate (German)
Currently translated at 100.0% (534 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-11-22 18:19:57 +00:00
Thomas
24b1f4028f Translated using Weblate (German)
Currently translated at 100.0% (534 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-11-22 18:19:57 +00:00
Spreez
984c863ff6 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-11-22 18:19:57 +00:00
vabene1111
0f207c2fa7 Merge pull request #2718 from TandoorRecipes/dependabot/pip/django-4.2.7
Bump django from 4.2.5 to 4.2.7
2023-11-22 09:15:55 +01:00
vabene1111
58e70c982e WIP 2023-11-21 22:34:17 +01:00
blowk
4cb94a1759 Update chowdown.py 2023-11-17 19:34:36 +01:00
blowk
3e568f7bb5 Merge branch 'TandoorRecipes:develop' into develop 2023-11-17 16:16:51 +01:00
Jan-Niklas Weghorn
b69c6bc97a fix placeholder text 2023-11-15 09:52:16 +01:00
avi meyer
9132ab8f33 Translated using Weblate (Hebrew)
Currently translated at 0.9% (5 of 508 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/he/
2023-11-15 08:20:01 +00:00
avi meyer
45858d5107 Translated using Weblate (Hebrew)
Currently translated at 93.2% (498 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/he/
2023-11-15 08:19:56 +00:00
avi meyer
1e332977c5 Added translation using Weblate (Hebrew) 2023-11-14 07:56:00 +00:00
dependabot[bot]
7b70ffab5f Bump axios from 1.5.0 to 1.6.0 in /vue
Bumps [axios](https://github.com/axios/axios) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.5.0...v1.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-11 15:26:26 +00:00
Jan-Niklas Weghorn
9b367e5d08 fix bottom navigation bar 2023-11-10 14:09:24 +01:00
Jan-Niklas Weghorn
243cac0389 cleanup 2023-11-10 12:16:43 +01:00
Jan-Niklas Weghorn
8205812c84 fix markdown editor and sidebar 2023-11-10 11:52:26 +01:00
Jan-Niklas Weghorn
94279b74c9 improve dark theme 2023-11-10 11:17:20 +01:00
noobdog
8a588db429 Translated using Weblate (Lithuanian)
Currently translated at 13.4% (72 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/lt/
2023-11-06 08:03:51 +00:00
dependabot[bot]
5150807ab7 Bump django from 4.2.5 to 4.2.7
Bumps [django](https://github.com/django/django) from 4.2.5 to 4.2.7.
- [Commits](https://github.com/django/django/compare/4.2.5...4.2.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-02 21:55:41 +00:00
smilerz
225ddc8eeb Update boot.sh
updated to handle postgres defined in database_url
2023-11-01 07:49:07 -05:00
dependabot[bot]
a6965fb3c4 Bump whitenoise from 6.5.0 to 6.6.0
Bumps [whitenoise](https://github.com/evansd/whitenoise) from 6.5.0 to 6.6.0.
- [Changelog](https://github.com/evansd/whitenoise/blob/main/docs/changelog.rst)
- [Commits](https://github.com/evansd/whitenoise/compare/6.5.0...6.6.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-11-01 00:16:11 +00:00
dependabot[bot]
90354305c4 Bump pytest-django from 4.5.2 to 4.6.0
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.5.2 to 4.6.0.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.5.2...v4.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 00:16:09 +00:00
dependabot[bot]
220d98a85c Bump markdown from 3.4.3 to 3.5.1
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.4.3 to 3.5.1.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.4.3...3.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 00:16:05 +00:00
dependabot[bot]
7fb4155ebe Bump recipe-scrapers from 14.36.1 to 14.52.0
Bumps [recipe-scrapers](https://github.com/hhursev/recipe-scrapers) from 14.36.1 to 14.52.0.
- [Release notes](https://github.com/hhursev/recipe-scrapers/releases)
- [Commits](https://github.com/hhursev/recipe-scrapers/compare/14.36.1...14.52.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 00:16:01 +00:00
dependabot[bot]
a39e6e8a6a Bump boto3 from 1.28.57 to 1.28.75
Bumps [boto3](https://github.com/boto/boto3) from 1.28.57 to 1.28.75.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.28.57...1.28.75)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 00:15:57 +00:00
smilerz
97fc15ded3 Merge pull request #2705 from smilerz/move_sqlite
better support for sqlite
2023-10-30 22:12:55 -05:00
smilerz
8bee2e3976 allow arbitrary path for sqlite using DATABASE_URL 2023-10-30 21:54:25 -05:00
smilerz
e89c1742fb remove deprecated django.db.backends.postgresql_psycopg2 2023-10-30 21:42:57 -05:00
smilerz
6680fbb644 changes to enable sqlite3 in docker container 2023-10-30 21:37:45 -05:00
smilerz
4cec643e08 Update ExportResponseView.vue 2023-10-29 16:33:22 -05:00
dependabot[bot]
03bd51893e Bump browserify-sign from 4.2.1 to 4.2.2 in /vue
Bumps [browserify-sign](https://github.com/crypto-browserify/browserify-sign) from 4.2.1 to 4.2.2.
- [Changelog](https://github.com/browserify/browserify-sign/blob/main/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/browserify-sign/compare/v4.2.1...v4.2.2)

---
updated-dependencies:
- dependency-name: browserify-sign
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 22:30:06 +00:00
smilerz
66b0e381ec Merge pull request #2699 from smilerz/fix_export
fixes recipe export
2023-10-27 14:45:03 -05:00
smilerz
fd70adf19d fixes recipe export 2023-10-27 14:43:48 -05:00
blowk
eb8422cb51 Import tags on mealie recipes 2023-10-22 20:09:27 +02:00
gorrilla10101
1008d880c9 Update authentication.md
Changed provider list url because existing one doesn't work anymore.
2023-10-21 06:07:37 -05:00
Ferenc
9cb1c21cd8 Translated using Weblate (Hungarian)
Currently translated at 81.4% (435 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/hu/
2023-10-20 14:05:55 +00:00
Boris Holowka
5149cb0609 Translated using Weblate (German)
Currently translated at 96.6% (516 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2023-10-20 14:05:55 +00:00
Ferenc
08adf4eb6f Translated using Weblate (Hungarian)
Currently translated at 97.7% (479 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/hu/
2023-10-20 14:05:55 +00:00
dependabot[bot]
62f38d00f3 Bump @babel/traverse from 7.22.17 to 7.23.2 in /vue
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.17 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-19 06:30:23 +00:00
harry
43a55c8c82 Fix bug when ingredients have no name 2023-10-18 21:46:07 +01:00
harry
d09eb64a41 Change step deletion confirmation 2023-10-18 21:30:17 +01:00
harry
8bbbc1b9ef Change remove ingredient confirmation 2023-10-18 21:09:33 +01:00
Jonas
cc367bfed2 Translated using Weblate (Lithuanian)
Currently translated at 4.6% (25 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/lt/
2023-10-15 14:19:56 +00:00
Ferenc
b18aa831ac Translated using Weblate (Hungarian)
Currently translated at 81.2% (434 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/hu/
2023-10-15 14:19:55 +00:00
Ferenc
6205fbe1c4 Translated using Weblate (Hungarian)
Currently translated at 97.7% (479 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/hu/
2023-10-15 14:19:55 +00:00
Jonas
879a54524c Added translation using Weblate (Lithuanian) 2023-10-14 21:14:27 +00:00
Ferenc
36678692be Translated using Weblate (Hungarian)
Currently translated at 73.7% (394 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/hu/
2023-10-14 12:14:48 +00:00
Tomasz Klimczak
de6285e5f8 Translated using Weblate (Polish)
Currently translated at 100.0% (534 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/
2023-10-14 12:14:48 +00:00
Guilherme Roda
7ff7409f56 Translated using Weblate (Portuguese (Brazil))
Currently translated at 89.8% (480 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pt_BR/
2023-10-13 14:18:58 +00:00
Ferenc
50b3636c86 Translated using Weblate (Hungarian)
Currently translated at 63.8% (341 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/hu/
2023-10-13 11:43:22 +00:00
dennisstinauer
1f72a3f62f open-data-import further duplicate handling 2023-10-13 12:50:59 +02:00
Ferenc
aea796bd6d Translated using Weblate (Hungarian)
Currently translated at 51.4% (275 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/hu/
2023-10-12 22:10:32 +00:00
Ferenc
edcddc3183 Translated using Weblate (Hungarian)
Currently translated at 41.0% (219 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/hu/
2023-10-12 20:19:57 +00:00
pharok
45a24a4720 Translated using Weblate (French)
Currently translated at 91.0% (486 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2023-10-12 20:19:57 +00:00
Charles Pare
bed95105f3 Translated using Weblate (French)
Currently translated at 91.0% (486 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2023-10-12 20:19:57 +00:00
pharok
a39fdb4226 Translated using Weblate (French)
Currently translated at 92.8% (455 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/fr/
2023-10-12 20:19:57 +00:00
smilerz
9d629b03b3 Update __init__.py 2023-10-12 10:44:59 -05:00
Ferenc
4eeb87cb95 Translated using Weblate (Hungarian)
Currently translated at 37.0% (198 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/hu/
2023-10-11 18:33:38 +00:00
Ferenc
3ce7e43b46 Translated using Weblate (Hungarian)
Currently translated at 29.0% (155 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/hu/
2023-10-10 11:19:55 +00:00
Ferenc
a3a995ef77 Translated using Weblate (Hungarian)
Currently translated at 97.5% (478 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/hu/
2023-10-10 11:19:55 +00:00
Ferenc
a386b45a03 Translated using Weblate (Hungarian)
Currently translated at 2.6% (14 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/hu/
2023-10-09 01:54:03 +00:00
Guilherme Roda
74bd2ba2c0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 59.6% (335 of 562 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/pt_BR/
2023-10-09 01:54:03 +00:00
Guilherme Roda
695f467126 Translated using Weblate (Portuguese (Brazil))
Currently translated at 89.3% (477 of 534 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pt_BR/
2023-10-09 01:54:03 +00:00
Ferenc
6270b46951 Translated using Weblate (Hungarian)
Currently translated at 89.5% (439 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/hu/
2023-10-09 01:54:03 +00:00
vabene1111
a3ad131e6a Merge pull request #2672 from harry48225/Improve-App-Import-Layout
Make import list layout responsive
2023-10-08 11:55:02 +02:00
Guilherme Roda
e2d5287cc6 Translated using Weblate (Portuguese (Brazil))
Currently translated at 51.0% (287 of 562 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/pt_BR/
2023-10-07 18:02:02 +00:00
Guilherme Roda
1c39d8089c Translated using Weblate (Portuguese (Brazil))
Currently translated at 87.0% (464 of 533 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pt_BR/
2023-10-07 18:02:02 +00:00
Guilherme Roda
2230b9e9ab Translated using Weblate (Portuguese)
Currently translated at 35.7% (175 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/pt/
2023-10-07 18:02:02 +00:00
harry
339d7b1c96 Make import list layout responsive 2023-10-07 17:09:53 +01:00
vabene1111
4e8c955555 fixed width recipe card skeleton 2023-10-07 08:37:53 +02:00
vabene1111
221c466c18 fixed recipe sage import and image procssing with pillow 10 2023-10-07 08:11:50 +02:00
vabene1111
2c8e029811 Merge pull request #2656 from TandoorRecipes/dependabot/github_actions/docker/metadata-action-5
Bump docker/metadata-action from 4 to 5
2023-10-05 21:14:24 +02:00
vabene1111
019825bfcb Merge pull request #2655 from TandoorRecipes/dependabot/github_actions/docker/login-action-3
Bump docker/login-action from 2 to 3
2023-10-05 21:14:13 +02:00
vabene1111
8e5ea47d5e Merge pull request #2654 from TandoorRecipes/dependabot/github_actions/docker/setup-qemu-action-3
Bump docker/setup-qemu-action from 2 to 3
2023-10-05 21:14:04 +02:00
vabene1111
425ac7f379 Merge pull request #2653 from TandoorRecipes/dependabot/github_actions/docker/setup-buildx-action-3
Bump docker/setup-buildx-action from 2 to 3
2023-10-05 21:13:57 +02:00
vabene1111
f0caef4759 Merge pull request #2652 from TandoorRecipes/dependabot/github_actions/docker/build-push-action-5
Bump docker/build-push-action from 4 to 5
2023-10-05 21:13:40 +02:00
vabene1111
c56a76f264 Merge pull request #2404 from ignas2526/feature/2402-make-now-count
Add ability to set maximum missing ingredient count
2023-10-05 19:01:48 +02:00
vabene1111
429886e6a6 Merge branch 'develop' into feature/2402-make-now-count 2023-10-05 19:01:33 +02:00
vabene1111
339ab57df7 Merge pull request #2647 from JohnTheNerd/develop
Added support for keeping SECRET_KEY and POSTGRES_PASSWORD in a file
2023-10-05 18:59:24 +02:00
vabene1111
cb6d98a357 fixed system page permission 2023-10-05 18:58:38 +02:00
vabene1111
e746b44f3b Merge pull request #2632 from smilerz/unique_name
updates to multiple models uniqueness capabilities
2023-10-05 18:57:08 +02:00
vabene1111
bc63ba6713 Merge pull request #2630 from smilerz/imports_cleanup
Imports cleanup
2023-10-05 18:55:32 +02:00
vabene1111
ea8661ab03 fixed property view roundign 2023-10-05 18:50:47 +02:00
vabene1111
bc9a5c9435 Merge pull request #2651 from TandoorRecipes/dependabot/pip/beautifulsoup4-4.12.2
Bump beautifulsoup4 from 4.11.1 to 4.12.2
2023-10-05 18:35:47 +02:00
vabene1111
365ffa29fa Merge pull request #2650 from TandoorRecipes/dependabot/pip/django-cors-headers-4.2.0
Bump django-cors-headers from 3.13.0 to 4.2.0
2023-10-05 18:35:41 +02:00
vabene1111
b3aeee6a63 Merge pull request #2648 from TandoorRecipes/dependabot/pip/boto3-1.28.57
Bump boto3 from 1.26.41 to 1.28.57
2023-10-05 18:35:32 +02:00
vabene1111
d503dc77c3 Merge pull request #2649 from TandoorRecipes/dependabot/pip/python-dotenv-1.0.0
Bump python-dotenv from 0.21.0 to 1.0.0
2023-10-05 18:35:04 +02:00
dependabot[bot]
fee364ee4a Bump python-dotenv from 0.21.0 to 1.0.0
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 0.21.0 to 1.0.0.
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v0.21.0...v1.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-05 16:32:22 +00:00
vabene1111
680a8d0fce Merge pull request #2639 from TandoorRecipes/dependabot/pip/cryptography-41.0.4
Bump cryptography from 41.0.3 to 41.0.4
2023-10-05 18:31:55 +02:00
vabene1111
0944d72e32 Merge pull request #2664 from TandoorRecipes/dependabot/pip/pillow-10.0.1
Bump pillow from 9.4.0 to 10.0.1
2023-10-05 18:31:32 +02:00
vabene1111
6809ded468 Merge pull request #2659 from nabim777/add-step-on-docs-for-manually-installation
add step on docs for manual installation
2023-10-05 18:30:13 +02:00
dependabot[bot]
64d07a65dc Bump pillow from 9.4.0 to 10.0.1
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.4.0 to 10.0.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.4.0...10.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-04 01:12:50 +00:00
Samuel
745abb57a8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 36.9% (197 of 533 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pt_BR/
2023-10-02 20:19:56 +00:00
nabim777
dfe5083451 add step on docs for manual installation 2023-10-01 10:18:43 +05:45
dependabot[bot]
9377e208e8 Bump docker/metadata-action from 4 to 5
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4 to 5.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md)
- [Commits](https://github.com/docker/metadata-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 00:40:41 +00:00
dependabot[bot]
40f38e6c6d Bump docker/login-action from 2 to 3
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 00:40:37 +00:00
dependabot[bot]
3ee0717d84 Bump docker/setup-qemu-action from 2 to 3
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 00:40:33 +00:00
dependabot[bot]
47155ce338 Bump docker/setup-buildx-action from 2 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 00:40:28 +00:00
dependabot[bot]
611080b739 Bump docker/build-push-action from 4 to 5
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 00:40:24 +00:00
dependabot[bot]
416d1badda Bump beautifulsoup4 from 4.11.1 to 4.12.2
Bumps [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/bs4/) from 4.11.1 to 4.12.2.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 00:20:58 +00:00
dependabot[bot]
0ef5d3ad92 Bump django-cors-headers from 3.13.0 to 4.2.0
Bumps [django-cors-headers](https://github.com/adamchainz/django-cors-headers) from 3.13.0 to 4.2.0.
- [Changelog](https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/adamchainz/django-cors-headers/compare/3.13.0...4.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 00:20:55 +00:00
dependabot[bot]
efb8784b91 Bump boto3 from 1.26.41 to 1.28.57
Bumps [boto3](https://github.com/boto/boto3) from 1.26.41 to 1.28.57.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.26.41...1.28.57)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 00:20:46 +00:00
Tomasz Klimczak
a1a6f476e0 Translated using Weblate (Polish)
Currently translated at 100.0% (533 of 533 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/
2023-09-30 22:19:56 +00:00
John Karabudak
ccd0667f04 Added support for keeping SECRET_KEY and POSTGRES_PASSWORD in a file
This commit adds two optional environment variables:

- SECRET_KEY_FILE
- POSTGRES_PASSWORD_FILE

This change allows mounting secret data when running this in Docker Swarm, instead of having to hard-code it in our docker-compose file or provide it alongside all other environment variables.
2023-09-30 01:12:30 -02:30
Henrique Nepomuceno
38cf825816 Translated using Weblate (Portuguese (Brazil))
Currently translated at 32.2% (172 of 533 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pt_BR/
2023-09-29 20:19:56 +00:00
Leo Mu
a8dc8e7190 Translated using Weblate (Italian)
Currently translated at 87.2% (465 of 533 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/it/
2023-09-29 20:19:56 +00:00
Luis Cacho
76aca6cf38 Translated using Weblate (Spanish)
Currently translated at 68.2% (361 of 529 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/es/
2023-09-25 09:59:47 +00:00
Matias Laporte
89c31a018f Translated using Weblate (Spanish)
Currently translated at 68.2% (361 of 529 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/es/
2023-09-25 09:59:47 +00:00
Leo Mu
e54f55b6d0 Translated using Weblate (Italian)
Currently translated at 88.0% (466 of 529 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/it/
2023-09-25 09:59:47 +00:00
Matias Laporte
fff7cb607c 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-09-25 09:59:47 +00:00
dependabot[bot]
54c2478869 Bump cryptography from 41.0.3 to 41.0.4
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.3 to 41.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.3...41.0.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-21 21:06:20 +00:00
smilerz
a7795092b3 make 'name' unique in space for MealType
make MealType, Unit, Supermarket, Supermarket Category, PropoertyType
case insenstive for get_or_create
convert unit conversion create serializer to get_or_create behavior
enable create duplicate tests for unitconversion and mealtype api
2023-09-14 14:46:37 -05:00
smilerz
538fb8b42e remove unused imports, vairables and commented code
from views, and base cookbook and recipes modules
2023-09-13 13:31:53 -05:00
smilerz
1f0cd58d7d remove unused imports, variables and commented code
from tests
2023-09-13 13:08:26 -05:00
smilerz
78b1386a1c remove unused imports, variables and commented code
from integrations and templatetags
2023-09-13 09:35:22 -05:00
smilerz
aba7f8db5c remove unused imports, variables and commented code in helpers 2023-09-13 09:29:48 -05:00
vabene1111
d7fadffbfd date format in meal plan simple grid 2023-09-13 16:17:02 +02:00
vabene1111
22c7f5d85d changed date label format 2023-09-13 16:16:38 +02:00
vabene1111
c18d8daece fixed meal plan simple grid on search view after merge 2023-09-13 16:08:56 +02:00
smilerz
d91c4b33f3 Merge pull request #2609 from smilerz/automation_tests
Automation Refactor and Tests
2023-09-12 15:07:18 -05:00
smilerz
2ad6f21b9c Merge pull request #2611 from smilerz/export_fix
fix custom_filter exports and errors on Download link
2023-09-12 15:06:31 -05:00
smilerz
554170a84e Merge pull request #2615 from smilerz/clear_food_after_add
clears search on food in shopping form
2023-09-12 15:06:23 -05:00
smilerz
d43a6e551d Merge pull request #2617 from smilerz/ignore_shopping_fix
respect ignore_shopping flag
2023-09-12 15:06:13 -05:00
smilerz
02cb6d1be7 Merge pull request #2619 from smilerz/meal_plan_date
update Meal Plan grid to respect localization on date format
2023-09-12 15:05:58 -05:00
smilerz
45b1eca48b Merge pull request #2621 from smilerz/fix_recipe_count
fix recipe counting issue on extended mixin
2023-09-12 15:05:30 -05:00
smilerz
6dacd44f1f regenerate openapi 2023-09-12 09:53:59 -05:00
smilerz
1b97472368 Squashed commit of the following:
commit 52909e8117
Author: smilerz <smilerz@gmail.com>
Date:   Wed Sep 6 15:54:23 2023 -0500

    fix recipe counting issue on extended mixin
2023-09-12 09:48:41 -05:00
smilerz
d467352029 Squashed commit of the following:
commit c8fc6b5237
Author: smilerz <smilerz@gmail.com>
Date:   Wed Sep 6 14:01:27 2023 -0500

    update Meal Plan grid to respect localization on date format
2023-09-12 09:48:22 -05:00
smilerz
a0256b607e Squashed commit of the following:
commit f8f08ae337
Author: smilerz <smilerz@gmail.com>
Date:   Wed Sep 6 10:27:43 2023 -0500

    respect ignore_shopping flag
2023-09-12 09:47:55 -05:00
smilerz
847fceaf10 Squashed commit of the following:
commit 4aa3e04df0
Author: smilerz <smilerz@gmail.com>
Date:   Wed Sep 6 09:01:07 2023 -0500

    clears search on food in shopping form
2023-09-12 09:47:07 -05:00
smilerz
9e831a22df Squashed commit of the following:
commit bcfe6ca707
Author: smilerz <smilerz@gmail.com>
Date:   Fri Sep 1 11:36:10 2023 -0500

    fix custom_filter exports and errors on Download link
2023-09-12 09:46:42 -05:00
smilerz
768a5ea237 Squashed commit of the following:
commit 36403ecbae
Author: smilerz <smilerz@gmail.com>
Date:   Fri Sep 1 12:04:04 2023 -0500

    update migration for new Automation Types

commit 4620ebaf30
Author: smilerz <smilerz@gmail.com>
Date:   Fri Sep 1 07:49:10 2023 -0500

    add Name and Instruction automation to YouTube importer

commit c907da84c1
Author: smilerz <smilerz@gmail.com>
Date:   Fri Sep 1 07:45:32 2023 -0500

    remove old commented automation code

commit 9b5e39415e
Author: smilerz <smilerz@gmail.com>
Date:   Fri Sep 1 07:37:36 2023 -0500

    test for automations applied during url import
    renamed TITLE_REPLACE to NAME_REPLACE

commit 2679a22464
Author: smilerz <smilerz@gmail.com>
Date:   Thu Aug 31 15:29:59 2023 -0500

    added tests for regex_replace

commit 8bae21025b
Author: smilerz <smilerz@gmail.com>
Date:   Thu Aug 31 13:51:46 2023 -0500

    updated Automation Modal and translations

commit 4120adc546
Author: smilerz <smilerz@gmail.com>
Date:   Thu Aug 31 13:12:41 2023 -0500

    applied regex_replace automation to food and unit automations
    updated automation documentation

commit 30c891abfc
Author: smilerz <smilerz@gmail.com>
Date:   Thu Aug 31 12:46:34 2023 -0500

    migrate regex_replace functions to AutomationEngine
    create TITLE_REPLACE, UNIT_REPLACE and FOOD REPLACE automation types
    create migration for new types

commit b8317c2c29
Author: smilerz <smilerz@gmail.com>
Date:   Wed Aug 30 20:44:40 2023 -0500

    move transpose words to AutomationEngine
    create tests for transpose words

commit 39253cfd02
Author: smilerz <smilerz@gmail.com>
Date:   Wed Aug 30 17:03:29 2023 -0500

    refactor never_unit automation to AutomationEngine
    create tests for never_unit

commit 7c0b8b151c
Author: smilerz <smilerz@gmail.com>
Date:   Wed Aug 30 11:21:06 2023 -0500

    update ingredient parser to use AutomationEngine for unt, keyword, food
    update test_ingredient_parser tests to accomodate changes

commit 8e1b8923af
Author: smilerz <smilerz@gmail.com>
Date:   Mon Aug 28 16:44:35 2023 -0500

    keyword and unit Automtations refactored to Automation Engine
    keyword and unit automation tests added

commit 52eb876a08
Author: smilerz <smilerz@gmail.com>
Date:   Mon Aug 28 15:03:19 2023 -0500

    food_alias tests added

commit a820b9c09e
Author: smilerz <smilerz@gmail.com>
Date:   Sat Aug 26 12:37:16 2023 -0500

    create AutomationEngine class
    create food_automation method
    refactor food automations to use AutomationEngine
2023-09-12 09:46:08 -05:00
smilerz
36403ecbae update migration for new Automation Types 2023-09-12 09:42:09 -05:00
smilerz
4620ebaf30 add Name and Instruction automation to YouTube importer 2023-09-12 09:42:09 -05:00
smilerz
c907da84c1 remove old commented automation code 2023-09-12 09:42:08 -05:00
smilerz
9b5e39415e test for automations applied during url import
renamed TITLE_REPLACE to NAME_REPLACE
2023-09-12 09:42:08 -05:00
smilerz
2679a22464 added tests for regex_replace 2023-09-12 09:42:07 -05:00
smilerz
8bae21025b updated Automation Modal and translations 2023-09-12 09:42:07 -05:00
smilerz
4120adc546 applied regex_replace automation to food and unit automations
updated automation documentation
2023-09-12 09:41:49 -05:00
smilerz
30c891abfc migrate regex_replace functions to AutomationEngine
create TITLE_REPLACE, UNIT_REPLACE and FOOD REPLACE automation types
create migration for new types
2023-09-12 09:41:49 -05:00
smilerz
b8317c2c29 move transpose words to AutomationEngine
create tests for transpose words
2023-09-12 09:40:17 -05:00
smilerz
39253cfd02 refactor never_unit automation to AutomationEngine
create tests for never_unit
2023-09-12 09:40:17 -05:00
smilerz
7c0b8b151c update ingredient parser to use AutomationEngine for unt, keyword, food
update test_ingredient_parser tests to accomodate changes
2023-09-12 09:40:17 -05:00
smilerz
8e1b8923af keyword and unit Automtations refactored to Automation Engine
keyword and unit automation tests added
2023-09-12 09:40:16 -05:00
smilerz
52eb876a08 food_alias tests added 2023-09-12 09:39:45 -05:00
smilerz
a820b9c09e create AutomationEngine class
create food_automation method
refactor food automations to use AutomationEngine
2023-09-12 09:39:45 -05:00
smilerz
bcfe6ca707 fix custom_filter exports and errors on Download link 2023-09-12 09:36:44 -05:00
smilerz
4aa3e04df0 clears search on food in shopping form 2023-09-12 09:30:47 -05:00
smilerz
f8f08ae337 respect ignore_shopping flag 2023-09-12 09:29:11 -05:00
smilerz
c8fc6b5237 update Meal Plan grid to respect localization on date format 2023-09-12 09:25:42 -05:00
smilerz
52909e8117 fix recipe counting issue on extended mixin 2023-09-12 09:21:18 -05:00
vabene1111
c72bf57ccb Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-09-12 16:08:33 +02:00
vabene1111
d3c21cf97f updated django 2023-09-12 16:08:28 +02:00
vabene1111
942edd9336 Merge pull request #2604 from TandoorRecipes/dependabot/pip/icalendar-5.0.7
Bump icalendar from 5.0.4 to 5.0.7
2023-09-12 16:07:35 +02:00
vabene1111
8fa6c98254 updated crispy forms 2023-09-12 16:07:16 +02:00
vabene1111
73c6bfce44 Merge pull request #2595 from djstini/develop
#2514 Importing a community curated list leads to an error
2023-09-12 16:00:25 +02:00
vabene1111
c105909933 Merge pull request #2603 from TandoorRecipes/dependabot/pip/django-oauth-toolkit-2.3.0
Bump django-oauth-toolkit from 2.2.0 to 2.3.0
2023-09-12 15:57:57 +02:00
vabene1111
13baf4f30a Merge pull request #2605 from TandoorRecipes/dependabot/pip/django-scopes-2.0.0
Bump django-scopes from 1.2.0.post1 to 2.0.0
2023-09-12 15:56:52 +02:00
vabene1111
da5fd16338 fixed lockfile 2023-09-12 15:56:28 +02:00
vabene1111
83a52bd204 Merge pull request #2593 from BrainWart/issue-2261
allow signup for social accounts when the provider is set up
2023-09-12 15:50:32 +02:00
vabene1111
fe4bd6a127 Merge pull request #2624 from WoosterInitiative/Update-settings
Update and add settings
2023-09-12 15:43:41 +02:00
vabene1111
d193d91e6a fixed migration 2023-09-12 15:41:38 +02:00
vabene1111
a2f9ef2e74 Merge pull request #2623 from smilerz/remove_facets
remove facets and treeselect
2023-09-12 15:36:24 +02:00
vabene1111
3f63eab68c Merge branch 'develop' into remove_facets 2023-09-12 15:36:14 +02:00
Karl
9b6ed7a63a Update .env.template
Add option for CSRF_TRUSTED_ORIGINS for better discoverability.

Add options for newly added CORS_ALLOW_ALL_ORIGINS for discoverability as well as flexibility.
2023-09-08 12:31:46 -07:00
Karl
e2f8efb521 Update settings.py
Add deprecation notice for `CORS_ORIGIN_ALLOW_ALL` and auto switch to `CORS_ALLOW_ALL_ORIGINS`
2023-09-08 12:29:06 -07:00
vabene1111
ce29283a52 fixed auto meal plan 2023-09-08 17:05:35 +02:00
vabene1111
dcf9d59b06 add more height to meal plan 2023-09-08 16:59:52 +02:00
vabene1111
794f9cf5b9 fixed meal plan factory 2023-09-08 16:38:38 +02:00
vabene1111
9954bb9410 fixed test and added meal plan client settings load save 2023-09-08 16:27:39 +02:00
vabene1111
e57be4a704 fixed lockfile 2023-09-08 15:47:08 +02:00
vabene1111
ffaecc066f improved search page meal plan style 2023-09-08 15:36:59 +02:00
vabene1111
94f398a7f6 added multi day meal planning 2023-09-08 15:31:42 +02:00
vabene1111
65d670a995 improved meal plan UI 2023-09-08 14:13:27 +02:00
dao cat
15ed040533 Translated using Weblate (Chinese (Simplified))
Currently translated at 90.5% (479 of 529 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/zh_Hans/
2023-09-08 05:19:56 +00:00
smilerz
5d3f44ffee missing caches import 2023-09-07 13:53:30 -05:00
smilerz
9ee4be621b remove facets and treeselect 2023-09-07 13:20:51 -05:00
vabene1111
d33b0d2254 added meal type settings to meal plan settings component 2023-09-06 16:27:36 +02:00
vabene1111
1a20c4bef5 fixed syntax server 2023-09-05 16:48:59 +02:00
AJ
b350ab1b59 Translated using Weblate (French)
Currently translated at 91.6% (449 of 490 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/fr/
2023-09-02 20:25:29 +00:00
dependabot[bot]
687e8a1f6a Bump django-scopes from 1.2.0.post1 to 2.0.0
Bumps [django-scopes](https://github.com/raphaelm/django-scopes) from 1.2.0.post1 to 2.0.0.
- [Commits](https://github.com/raphaelm/django-scopes/commits/2.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-01 00:48:41 +00:00
dependabot[bot]
64b9605871 Bump icalendar from 5.0.4 to 5.0.7
Bumps [icalendar](https://github.com/collective/icalendar) from 5.0.4 to 5.0.7.
- [Changelog](https://github.com/collective/icalendar/blob/master/CHANGES.rst)
- [Commits](https://github.com/collective/icalendar/compare/v5.0.4...v5.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-01 00:48:38 +00:00
dependabot[bot]
8320473606 Bump django-oauth-toolkit from 2.2.0 to 2.3.0
Bumps [django-oauth-toolkit](https://github.com/jazzband/django-oauth-toolkit) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/jazzband/django-oauth-toolkit/releases)
- [Changelog](https://github.com/jazzband/django-oauth-toolkit/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jazzband/django-oauth-toolkit/compare/2.2.0...2.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-01 00:48:33 +00:00
NeoID
88228ab853 Translated using Weblate (Norwegian Bokmål)
Currently translated at 70.5% (373 of 529 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nb_NO/
2023-08-31 11:37:04 +00:00
vabene1111
dcfb269909 meal plan stuff 2023-08-30 10:30:12 +02:00
vabene1111
4a1ec5adf7 fixed icon issues 2023-08-30 09:05:24 +02:00
Cody McGinnis
56cdc14cc1 docs: explain social authentication auto sign up
Attempt to explain to the user that social auth with automatically allow
users to sign up for social accounts. `ALLOW_SIGNUP` now applies to
local account sign up only.
2023-08-29 20:28:30 -04:00
Cody McGinnis
b8959036bf allow signup for social accounts when the provider is set up
When a social provider is set up, the social account signup view is enabled. This seems to cover issue #2261, but it needs further testing.
2023-08-29 20:28:30 -04:00
djstini
ab24177c89 Merge branch 'TandoorRecipes:develop' into develop 2023-08-29 19:26:40 +02:00
vabene1111
4ffc9cc72f removed icons 2023-08-29 15:58:57 +02:00
vabene1111
7f62ec28e3 WIP meal plan and icon stuff 2023-08-29 14:36:57 +02:00
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
dennisstinauer
b1aa70787c #2514 Importing a community curated list leads to an error 2023-08-28 22:06:49 +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
vabene1111
c78b7a6928 Merge branch 'develop' 2023-07-05 16:33:51 +02:00
vabene1111
7a2ccc075c improved shopping entry api endpoint performance 2023-07-04 16:49:56 +02:00
vabene1111
237054c23e improved commonly used administrative admin fields 2023-07-03 22:30:27 +02:00
vabene1111
ac1d641bd5 added RO DRF permission and internal_note filters for invite/userspace 2023-07-03 21:59:15 +02:00
vabene1111
3545b6e98a plugin loader improvements 2023-07-03 17:56:05 +02:00
vabene1111
d3a56e00ea allow disabling plugins 2023-07-03 07:41:56 +02:00
vabene1111
e9f8578c25 re added path to plugin check 2023-07-03 07:02:56 +02:00
vabene1111
dccfc436be Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-07-03 05:55:17 +02:00
vabene1111
1e85c8587b fixed plugin error message 2023-07-03 05:55:12 +02:00
vabene1111
b8f92ab054 Merge pull request #2531 from michael-genson/feature/add-source-url-to-recipe-export
Add source URL to recipe export
2023-07-03 05:47:31 +02:00
Mára Štěpánek
766ed31f8e Translated using Weblate (Czech)
Currently translated at 79.7% (398 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/cs/
2023-07-02 21:19:57 +00:00
Michael Genson
cad78e115d added source url to recipe export 2023-07-02 10:42:41 -05: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
vabene1111
b599c4f6a9 added internal notes and improved invite link form 2023-06-30 23:09:22 +02:00
vabene1111
439539f56d show optional fields in generic forms 2023-06-30 23:09:01 +02:00
vabene1111
237bcb92c9 fixed food editor default properties unit 2023-06-29 17:26:49 +02:00
vabene1111
ce02a23dbb fixed quick ingredient import in recipe editor 2023-06-29 17:13:53 +02:00
vabene1111
8e81512735 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2023-06-29 17:05:37 +02:00
vabene1111
c69f0394a8 possibly fixed bug with food editor ingredient delete page reload 2023-06-29 17:05:32 +02:00
vabene1111
d7ca9e05de Merge pull request #2521 from gloriousDan/improve-docs
add note to docker-compose files and update postgres tag
2023-06-29 17:04:31 +02:00
vabene1111
64534ff810 fixed navbar color for non logged in users 2023-06-29 17:03:05 +02:00
vabene1111
d0164a6c28 Merge pull request #2522 from gloriousDan/fix-raspi
Fix Raspi build and consolidate with normal build and image
2023-06-27 16:10:39 +02:00
Daniel Schulz
0f898ddf4a unify raspi and normal build again 2023-06-27 00:51:55 +02:00
Daniel Schulz
e903382034 update alpine to v3.18 2023-06-27 00:51:22 +02:00
Daniel Schulz
0d225450da add note to docker-compose files and update postgres tag 2023-06-27 00:33:29 +02:00
vabene1111
c077a64484 further improvements 2023-06-26 20:57:51 +02:00
vabene1111
6c16094b42 added initial version of tandoor dark theme 2023-06-26 20:43:50 +02:00
vabene1111
5aa80746f9 Merge branch 'develop' 2023-06-26 20:25:58 +02:00
vabene1111
cc64717818 auto add schema attrs in json importer 2023-06-26 20:22:59 +02:00
vabene1111
6acd892116 fixed broken image would fail default importer 2023-06-26 20:18:36 +02:00
vabene1111
3955408aa4 dont show properties view if no properties are present in DB 2023-06-26 20:03:25 +02:00
vabene1111
3de2468df3 fixed to light nav color in some themes 2023-06-26 19:57:38 +02:00
vabene1111
b1d983fbc3 fixed required field in food 2023-06-26 17:08:45 +02:00
vabene1111
5f443d2593 fixed issue when creating food with properties 2023-06-26 16:48:50 +02:00
vabene1111
436158f596 fixed allow decimals in food property amount 2023-06-26 15:47:44 +02:00
vabene1111
dcc56fc138 added new docs entry to nav 2023-06-26 15:21:05 +02:00
vabene1111
0eef10079b Merge pull request #2517 from 16cdlogan/patch-1
Create Truenas-Portainer
2023-06-26 15:19:03 +02:00
16cdlogan
2b839dfb19 Create Truenas-Portainer
Install Tandoor Recipes on TrueNAS Core and Portainer
2023-06-25 21:32:54 -04:00
sweeney
491b678d6e Translated using Weblate (Greek)
Currently translated at 1.4% (7 of 499 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/el/
2023-06-25 14:19:55 +00:00
sweeney
151dce006d Translated using Weblate (Greek)
Currently translated at 54.7% (287 of 524 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/el/
2023-06-25 14:19:55 +00:00
sweeney
d4f538b4aa Translated using Weblate (Greek)
Currently translated at 35.4% (186 of 524 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/el/
2023-06-24 13:32:57 +00:00
sweeney
a727439c57 Added translation using Weblate (Greek) 2023-06-24 13:32:57 +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
Ignas Poklad
497321799c Add ability to set maximum missing ingredient count 2023-04-04 18:35:32 +02:00
Ignas Poklad
78f1ee175b Add ability to set maximum missing ingredient count 2023-03-30 00:47:21 +02: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
243 changed files with 23026 additions and 7004 deletions

View File

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

View File

@@ -13,9 +13,18 @@ DEBUG_TOOLBAR=0
# hosts the application can run under e.g. recipes.mydomain.com,cooking.mydomain.com,...
ALLOWED_HOSTS=*
# Cross Site Request Forgery protection
# (https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-CSRF_TRUSTED_ORIGINS)
# CSRF_TRUSTED_ORIGINS = []
# Cross Origin Resource Sharing
# (https://github.com/adamchainz/django-cors-header)
# CORS_ALLOW_ALL_ORIGINS = True
# random secret key, use for example `base64 /dev/urandom | head -c50` to generate one
# ---------------------------- REQUIRED -------------------------
# ---------------------------- AT LEAST ONE REQUIRED -------------------------
SECRET_KEY=
SECRET_KEY_FILE=
# ---------------------------------------------------------------
# your default timezone See https://timezonedb.com/time-zones for a list of timezones
@@ -27,8 +36,9 @@ DB_ENGINE=django.db.backends.postgresql
POSTGRES_HOST=db_recipes
POSTGRES_PORT=5432
POSTGRES_USER=djangouser
# ---------------------------- REQUIRED -------------------------
# ---------------------------- AT LEAST ONE REQUIRED -------------------------
POSTGRES_PASSWORD=
POSTGRES_PASSWORD_FILE=
# ---------------------------------------------------------------
POSTGRES_DB=djangodb
@@ -100,10 +110,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
@@ -111,7 +123,8 @@ REVERSE_PROXY_AUTH=0
# SPACE_DEFAULT_MAX_FILES=0 # Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.
# SPACE_DEFAULT_ALLOW_SHARING=1 # Allow users to share recipes with public links
# allow people to create accounts on your application instance (without an invite link)
# allow people to create local accounts on your application instance (without an invite link)
# social accounts will always be able to sign up
# when unset: 0 (false)
# ENABLE_SIGNUP=0

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
@@ -74,17 +64,17 @@ jobs:
run: yarn build
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
if: github.secret_source == 'Actions'
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
if: github.secret_source == 'Actions'
with:
registry: ghcr.io
@@ -92,7 +82,7 @@ jobs:
password: ${{ github.token }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: |
vabene1111/recipes
@@ -107,7 +97,7 @@ jobs:
type=semver,suffix=-open-data-plugin,pattern={{major}}
type=ref,suffix=-open-data-plugin,event=branch
- name: Build and Push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.dockerfile }}

View File

@@ -17,15 +17,9 @@ jobs:
# Standard build config
- name: Standard
dockerfile: Dockerfile
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7
suffix: ""
continue-on-error: false
# Raspi build config
- name: Raspi
dockerfile: Dockerfile-raspi
platforms: linux/arm/v7
suffix: "-raspi"
continue-on-error: true
steps:
- uses: actions/checkout@v3
@@ -40,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:
@@ -64,17 +48,17 @@ jobs:
run: yarn build
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
if: github.secret_source == 'Actions'
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
if: github.secret_source == 'Actions'
with:
registry: ghcr.io
@@ -82,7 +66,7 @@ jobs:
password: ${{ github.token }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: |
vabene1111/recipes
@@ -97,7 +81,7 @@ jobs:
type=semver,pattern={{major}}
type=ref,event=branch
- name: Build and Push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.dockerfile }}

1
.gitignore vendored
View File

@@ -74,6 +74,7 @@ mediafiles/
\.env
staticfiles/
postgresql/
data/
/docker-compose.override.yml

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.15
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 py-cryptography 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
@@ -15,7 +15,11 @@ WORKDIR /opt/recipes
COPY requirements.txt ./
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev git && \
RUN \
if [ `apk --print-arch` = "armv7" ]; then \
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
fi
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev && \
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
@@ -26,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

@@ -1,33 +0,0 @@
# builds of cryptography for raspberry pi (or better arm v7) fail for some
FROM python:3.9-alpine3.15
#Install all dependencies.
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev py-cryptography openldap gcompat
#Print all logs without buffering it.
ENV PYTHONUNBUFFERED 1
#This port will be used by gunicorn.
EXPOSE 8080
#Create app dir and install requirements.
RUN mkdir /opt/recipes
WORKDIR /opt/recipes
COPY requirements.txt ./
RUN \
if [ `apk --print-arch` = "armv7" ]; then \
printf "[global]\nextra-index-url=https://www.piwheels.org/simple\n" > /etc/pip.conf ; \
fi
RUN apk add --no-cache --virtual .build-deps gcc musl-dev zlib-dev jpeg-dev libwebp-dev python3-dev git && \
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
venv/bin/pip install wheel==0.37.1 && \
venv/bin/pip install -r requirements.txt --no-cache-dir --no-binary=Pillow && \
apk --purge del .build-deps
#Copy project and execute it.
COPY . ./
RUN chmod +x boot.sh
ENTRYPOINT ["/opt/recipes/boot.sh"]

20
boot.sh
View File

@@ -19,9 +19,14 @@ if [ ! -f "$NGINX_CONF_FILE" ] && [ $GUNICORN_MEDIA -eq 0 ]; then
display_warning "Nginx configuration file could not be found at the default location!\nPath: ${NGINX_CONF_FILE}"
fi
# SECRET_KEY must be set in .env file
# SECRET_KEY (or a valid file at SECRET_KEY_FILE) must be set in .env file
if [ -f "${SECRET_KEY_FILE}" ]; then
export SECRET_KEY=$(cat "$SECRET_KEY_FILE")
fi
if [ -z "${SECRET_KEY}" ]; then
display_warning "The environment variable 'SECRET_KEY' is not set but REQUIRED for running Tandoor!"
display_warning "The environment variable 'SECRET_KEY' (or 'SECRET_KEY_FILE' that points to an existing file) is not set but REQUIRED for running Tandoor!"
fi
@@ -30,11 +35,16 @@ echo "Waiting for database to be ready..."
attempt=0
max_attempts=20
if [ "${DB_ENGINE}" != 'django.db.backends.sqlite3' ]; then
if [ "${DB_ENGINE}" == 'django.db.backends.postgresql' ] || [ "${DATABASE_URL}" == 'postgres'* ]; then
# POSTGRES_PASSWORD (or a valid file at POSTGRES_PASSWORD_FILE) must be set in .env file
if [ -f "${POSTGRES_PASSWORD_FILE}" ]; then
export POSTGRES_PASSWORD=$(cat "$POSTGRES_PASSWORD_FILE")
fi
# POSTGRES_PASSWORD must be set in .env file
if [ -z "${POSTGRES_PASSWORD}" ]; then
display_warning "The environment variable 'POSTGRES_PASSWORD' is not set but REQUIRED for running Tandoor!"
display_warning "The environment variable 'POSTGRES_PASSWORD' (or 'POSTGRES_PASSWORD_FILE' that points to an existing file) is not set but REQUIRED for running Tandoor!"
fi
while pg_isready --host=${POSTGRES_HOST} --port=${POSTGRES_PORT} --user=${POSTGRES_USER} -q; status=$?; attempt=$((attempt+1)); [ $status -ne 0 ] && [ $attempt -le $max_attempts ]; do

View File

@@ -10,13 +10,13 @@ from treebeard.forms import movenodeform_factory
from cookbook.managers import DICTIONARY
from .models import (Automation, BookmarkletImport, Comment, CookLog, Food, FoodInheritField,
ImportLog, Ingredient, InviteLink, Keyword, MealPlan, MealType,
NutritionInformation, Property, PropertyType, Recipe, RecipeBook,
RecipeBookEntry, RecipeImport, SearchPreference, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket,
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, TelegramBot,
Unit, UnitConversion, UserFile, UserPreference, UserSpace, ViewLog)
from .models import (BookmarkletImport, Comment, CookLog, Food, ImportLog, Ingredient, InviteLink,
Keyword, MealPlan, MealType, NutritionInformation, Property, PropertyType,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
ViewLog)
class CustomUserAdmin(UserAdmin):
@@ -39,6 +39,8 @@ def delete_space_action(modeladmin, request, queryset):
class SpaceAdmin(admin.ModelAdmin):
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
search_fields = ('name', 'created_by__username')
autocomplete_fields = ('created_by',)
filter_horizontal = ('food_inherit',)
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
date_hierarchy = 'created_at'
actions = [delete_space_action]
@@ -50,6 +52,8 @@ admin.site.register(Space, SpaceAdmin)
class UserSpaceAdmin(admin.ModelAdmin):
list_display = ('user', 'space',)
search_fields = ('user__username', 'space__name',)
filter_horizontal = ('groups',)
autocomplete_fields = ('user', 'space',)
admin.site.register(UserSpace, UserSpaceAdmin)
@@ -60,6 +64,7 @@ class UserPreferenceAdmin(admin.ModelAdmin):
search_fields = ('user__username',)
list_filter = ('theme', 'nav_color', 'default_page',)
date_hierarchy = 'created_at'
filter_horizontal = ('plan_share', 'shopping_share',)
@staticmethod
def name(obj):
@@ -187,7 +192,7 @@ class RecipeAdmin(admin.ModelAdmin):
def created_by(obj):
return obj.created_by.get_user_display_name()
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql':
actions = [rebuild_index]
@@ -272,7 +277,7 @@ admin.site.register(RecipeBookEntry, RecipeBookEntryAdmin)
class MealPlanAdmin(admin.ModelAdmin):
list_display = ('user', 'recipe', 'meal_type', 'date')
list_display = ('user', 'recipe', 'meal_type', 'from_date', 'to_date')
@staticmethod
def user(obj):
@@ -309,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

@@ -9,8 +9,8 @@ from django_scopes import scopes_disabled
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
from hcaptcha.fields import hCaptchaField
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, MealType, Recipe, RecipeBook,
RecipeBookEntry, SearchPreference, Space, Storage, Sync, User, UserPreference)
from .models import (Comment, Food, InviteLink, Keyword, Recipe, RecipeBook, RecipeBookEntry,
SearchPreference, Space, Storage, Sync, User, UserPreference)
class SelectWidget(widgets.Select):
@@ -45,7 +45,7 @@ class UserPreferenceForm(forms.ModelForm):
model = UserPreference
fields = (
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed',
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed', 'show_step_ingredients',
)
labels = {
@@ -60,29 +60,29 @@ 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 = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
'use_fractions': _(
'Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'),
'use_kj': _('Display nutritional energy amounts in joules instead of calories'),
'use_kj': _('Display nutritional energy amounts in joules instead of calories'),
'plan_share': _('Users with whom newly created meal plans should be shared by default.'),
'shopping_share': _('Users with whom to share shopping lists.'),
'ingredient_decimals': _('Number of decimals to round ingredients.'),
'comments': _('If you want to be able to create and see comments underneath recipes.'),
'ingredient_decimals': _('Number of decimals to round ingredients.'),
'comments': _('If you want to be able to create and see comments underneath recipes.'),
'shopping_auto_sync': _(
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
'of mobile data. If lower than instance limit it is reset when saving.'
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
'of mobile data. If lower than instance limit it is reset when saving.'
),
'sticky_navbar': _('Makes the navbar stick to the top of the page.'),
'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 = {
@@ -184,6 +184,7 @@ class MultipleFileField(forms.FileField):
result = single_file_clean(data, initial)
return result
class ImportForm(ImportExportBase):
files = MultipleFileField(required=True)
duplicates = forms.BooleanField(help_text=_(
@@ -322,50 +323,6 @@ class ImportRecipeForm(forms.ModelForm):
}
# TODO deprecate
class MealPlanForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['recipe'].queryset = Recipe.objects.filter(space=space).all()
self.fields['meal_type'].queryset = MealType.objects.filter(space=space).all()
self.fields['shared'].queryset = User.objects.filter(userpreference__space=space).all()
def clean(self):
cleaned_data = super(MealPlanForm, self).clean()
if cleaned_data['title'] == '' and cleaned_data['recipe'] is None:
raise forms.ValidationError(
_('You must provide at least a recipe or a title.')
)
return cleaned_data
class Meta:
model = MealPlan
fields = (
'recipe', 'title', 'meal_type', 'note',
'servings', 'date', 'shared'
)
help_texts = {
'shared': _('You can list default users to share recipes with in the settings.'),
'note': _('You can use markdown to format this field. See the <a href="/docs/markdown/">docs here</a>')
}
widgets = {
'recipe': SelectWidget,
'date': DateWidget,
'shared': MultiSelectWidget
}
field_classes = {
'recipe': SafeModelChoiceField,
'meal_type': SafeModelChoiceField,
'shared': SafeModelMultipleChoiceField,
}
class InviteLinkForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
@@ -506,8 +463,8 @@ class ShoppingPreferenceForm(forms.ModelForm):
help_texts = {
'shopping_share': _('Users will see all items you add to your shopping list. They must add you to see items on their list.'),
'shopping_auto_sync': _(
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
'of mobile data. If lower than instance limit it is reset when saving.'
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
'of mobile data. If lower than instance limit it is reset when saving.'
),
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
'mealplan_autoinclude_related': _('When adding a meal plan to the shopping list (manually or automatically), include all related recipes.'),
@@ -551,11 +508,10 @@ class SpacePreferenceForm(forms.ModelForm):
class Meta:
model = Space
fields = ('food_inherit', 'reset_food_inherit', 'show_facet_count', 'use_plural')
fields = ('food_inherit', 'reset_food_inherit', 'use_plural')
help_texts = {
'food_inherit': _('Fields on food that should be inherited by default.'),
'show_facet_count': _('Show recipe counts on search filters'),
'use_plural': _('Use the plural form for units and food inside this space.'),
}

View File

@@ -1,11 +1,10 @@
import datetime
from django.conf import settings
from gettext import gettext as _
from allauth.account.adapter import DefaultAccountAdapter
from django.conf import settings
from django.contrib import messages
from django.core.cache import caches
from gettext import gettext as _
from cookbook.models import InviteLink
@@ -17,10 +16,13 @@ class AllAuthCustomAdapter(DefaultAccountAdapter):
Whether to allow sign-ups.
"""
signup_token = False
if 'signup_token' in request.session and InviteLink.objects.filter(valid_until__gte=datetime.datetime.today(), used_by=None, uuid=request.session['signup_token']).exists():
if 'signup_token' in request.session and InviteLink.objects.filter(
valid_until__gte=datetime.datetime.today(), used_by=None, uuid=request.session['signup_token']).exists():
signup_token = True
if (request.resolver_match.view_name == 'account_signup' or request.resolver_match.view_name == 'socialaccount_signup') and not settings.ENABLE_SIGNUP and not signup_token:
if request.resolver_match.view_name == 'account_signup' and not settings.ENABLE_SIGNUP and not signup_token:
return False
elif request.resolver_match.view_name == 'socialaccount_signup' and len(settings.SOCIAL_PROVIDERS) < 1:
return False
else:
return super(AllAuthCustomAdapter, self).is_open_for_signup(request)
@@ -33,7 +35,7 @@ class AllAuthCustomAdapter(DefaultAccountAdapter):
if c == default:
try:
super(AllAuthCustomAdapter, self).send_mail(template_prefix, email, context)
except Exception: # dont fail signup just because confirmation mail could not be send
except Exception: # dont fail signup just because confirmation mail could not be send
pass
else:
messages.add_message(self.request, messages.ERROR, _('In order to prevent spam, the requested email was not send. Please wait a few minutes and try again.'))

View File

@@ -0,0 +1,227 @@
import re
from django.core.cache import caches
from django.db.models.functions import Lower
from cookbook.models import Automation
class AutomationEngine:
request = None
source = None
use_cache = None
food_aliases = None
keyword_aliases = None
unit_aliases = None
never_unit = None
transpose_words = None
regex_replace = {
Automation.DESCRIPTION_REPLACE: None,
Automation.INSTRUCTION_REPLACE: None,
Automation.FOOD_REPLACE: None,
Automation.UNIT_REPLACE: None,
Automation.NAME_REPLACE: None,
}
def __init__(self, request, use_cache=True, source=None):
self.request = request
self.use_cache = use_cache
if not source:
self.source = "default_string_to_avoid_false_regex_match"
else:
self.source = source
def apply_keyword_automation(self, keyword):
keyword = keyword.strip()
if self.use_cache and self.keyword_aliases is None:
self.keyword_aliases = {}
KEYWORD_CACHE_KEY = f'automation_keyword_alias_{self.request.space.pk}'
if c := caches['default'].get(KEYWORD_CACHE_KEY, None):
self.keyword_aliases = c
caches['default'].touch(KEYWORD_CACHE_KEY, 30)
else:
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.KEYWORD_ALIAS).only('param_1', 'param_2').order_by('order').all():
self.keyword_aliases[a.param_1.lower()] = a.param_2
caches['default'].set(KEYWORD_CACHE_KEY, self.keyword_aliases, 30)
else:
self.keyword_aliases = {}
if self.keyword_aliases:
try:
keyword = self.keyword_aliases[keyword.lower()]
except KeyError:
pass
else:
if automation := Automation.objects.filter(space=self.request.space, type=Automation.KEYWORD_ALIAS, param_1__iexact=keyword, disabled=False).order_by('order').first():
return automation.param_2
return keyword
def apply_unit_automation(self, unit):
unit = unit.strip()
if self.use_cache and self.unit_aliases is None:
self.unit_aliases = {}
UNIT_CACHE_KEY = f'automation_unit_alias_{self.request.space.pk}'
if c := caches['default'].get(UNIT_CACHE_KEY, None):
self.unit_aliases = c
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.lower()] = a.param_2
caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
else:
self.unit_aliases = {}
if self.unit_aliases:
try:
unit = self.unit_aliases[unit.lower()]
except KeyError:
pass
else:
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 self.apply_regex_replace_automation(unit, Automation.UNIT_REPLACE)
def apply_food_automation(self, food):
food = food.strip()
if self.use_cache and self.food_aliases is None:
self.food_aliases = {}
FOOD_CACHE_KEY = f'automation_food_alias_{self.request.space.pk}'
if c := caches['default'].get(FOOD_CACHE_KEY, None):
self.food_aliases = c
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.lower()] = a.param_2
caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
else:
self.food_aliases = {}
if self.food_aliases:
try:
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__iexact=food, disabled=False).order_by('order').first():
return automation.param_2
return self.apply_regex_replace_automation(food, Automation.FOOD_REPLACE)
def apply_never_unit_automation(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.use_cache and self.never_unit is None:
self.never_unit = {}
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)
else:
self.never_unit = {}
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 a := Automation.objects.annotate(param_1_lower=Lower('param_1')).filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1_lower__in=[
tokens[1].lower(), alt_unit.lower()], disabled=False).order_by('order').first():
new_unit = a.param_2
never_unit = True
if never_unit:
tokens.insert(1, new_unit)
return tokens
def apply_transpose_automation(self, string):
"""
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.use_cache and self.transpose_words is None:
self.transpose_words = {}
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()[:512]:
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.transpose_words = {}
tokens = [x.lower() for x in string.replace(',', ' ').split()]
if self.transpose_words:
for key, value in self.transpose_words.items():
if value[0] in tokens and value[1] in tokens:
string = re.sub(rf"\b({value[0]})\W*({value[1]})\b", r"\2 \1", string, 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(param_1_lower__in=tokens, param_2_lower__in=tokens).order_by('order')[:512]:
if rule.param_1 in tokens and rule.param_2 in tokens:
string = re.sub(rf"\b({rule.param_1})\W*({rule.param_2})\b", r"\2 \1", string, flags=re.IGNORECASE)
return string
def apply_regex_replace_automation(self, string, automation_type):
# TODO add warning - maybe on SPACE page? when a max of 512 automations of a specific type is exceeded (ALIAS types excluded?)
"""
Replaces strings in a recipe field that are from a matched source
field_type are Automation.type that apply regex replacements
Automation.DESCRIPTION_REPLACE
Automation.INSTRUCTION_REPLACE
Automation.FOOD_REPLACE
Automation.UNIT_REPLACE
Automation.NAME_REPLACE
regex replacment utilized the following fields from the Automation model
:param 1: source that should apply the automation in regex format ('.*' for all)
:param 2: regex pattern to match ()
:param 3: replacement string (leave blank to delete)
return: new string
"""
if self.use_cache and self.regex_replace[automation_type] is None:
self.regex_replace[automation_type] = {}
REGEX_REPLACE_CACHE_KEY = f'automation_regex_replace_{self.request.space.pk}'
if c := caches['default'].get(REGEX_REPLACE_CACHE_KEY, None):
self.regex_replace[automation_type] = c[automation_type]
caches['default'].touch(REGEX_REPLACE_CACHE_KEY, 30)
else:
i = 0
for a in Automation.objects.filter(space=self.request.space, disabled=False, type=automation_type).only(
'param_1', 'param_2', 'param_3').order_by('order').all()[:512]:
self.regex_replace[automation_type][i] = [a.param_1, a.param_2, a.param_3]
i += 1
caches['default'].set(REGEX_REPLACE_CACHE_KEY, self.regex_replace, 30)
else:
self.regex_replace[automation_type] = {}
if self.regex_replace[automation_type]:
for rule in self.regex_replace[automation_type].values():
if re.match(rule[0], (self.source)[:512]):
string = re.sub(rule[1], rule[2], string, flags=re.IGNORECASE)
else:
for rule in Automation.objects.filter(space=self.request.space, disabled=False, type=automation_type).only(
'param_1', 'param_2', 'param_3').order_by('order').all()[:512]:
if re.match(rule.param_1, (self.source)[:512]):
string = re.sub(rule.param_2, rule.param_3, string, flags=re.IGNORECASE)
return string

View File

@@ -1,8 +1,7 @@
import os
import sys
from io import BytesIO
from PIL import Image
from io import BytesIO
def rescale_image_jpeg(image_object, base_width=1020):
@@ -11,7 +10,7 @@ def rescale_image_jpeg(image_object, base_width=1020):
width_percent = (base_width / float(img.size[0]))
height = int((float(img.size[1]) * float(width_percent)))
img = img.resize((base_width, height), Image.ANTIALIAS)
img = img.resize((base_width, height), Image.LANCZOS)
img_bytes = BytesIO()
img.save(img_bytes, 'JPEG', quality=90, optimize=True, icc_profile=icc_profile)
@@ -22,7 +21,7 @@ def rescale_image_png(image_object, base_width=1020):
image_object = Image.open(image_object)
wpercent = (base_width / float(image_object.size[0]))
hsize = int((float(image_object.size[1]) * float(wpercent)))
img = image_object.resize((base_width, hsize), Image.ANTIALIAS)
img = image_object.resize((base_width, hsize), Image.LANCZOS)
im_io = BytesIO()
img.save(im_io, 'PNG', quality=90)

View File

@@ -2,18 +2,16 @@ import re
import string
import unicodedata
from django.core.cache import caches
from cookbook.models import Unit, Food, Automation, Ingredient
from cookbook.helper.automation_helper import AutomationEngine
from cookbook.models import Food, Ingredient, Unit
class IngredientParser:
request = None
ignore_rules = False
food_aliases = {}
unit_aliases = {}
automation = None
def __init__(self, request, cache_mode, ignore_automations=False):
def __init__(self, request, cache_mode=True, ignore_automations=False):
"""
Initialize ingredient parser
:param request: request context (to control caching, rule ownership, etc.)
@@ -22,65 +20,8 @@ class IngredientParser:
"""
self.request = request
self.ignore_rules = ignore_automations
if cache_mode:
FOOD_CACHE_KEY = f'automation_food_alias_{self.request.space.pk}'
if c := caches['default'].get(FOOD_CACHE_KEY, None):
self.food_aliases = c
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
caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30)
UNIT_CACHE_KEY = f'automation_unit_alias_{self.request.space.pk}'
if c := caches['default'].get(UNIT_CACHE_KEY, None):
self.unit_aliases = c
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
caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30)
else:
self.food_aliases = {}
self.unit_aliases = {}
def apply_food_automation(self, food):
"""
Apply food alias automations to passed food
:param food: unit as string
:return: food as string (possibly changed by automation)
"""
if self.ignore_rules:
return food
else:
if self.food_aliases:
try:
return self.food_aliases[food]
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():
return automation.param_2
return food
def apply_unit_automation(self, unit):
"""
Apply unit alias automations to passed unit
:param unit: unit as string
:return: unit as string (possibly changed by automation)
"""
if self.ignore_rules:
return unit
else:
if self.unit_aliases:
try:
return self.unit_aliases[unit]
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():
return automation.param_2
return unit
if not self.ignore_rules:
self.automation = AutomationEngine(self.request, use_cache=cache_mode)
def get_unit(self, unit):
"""
@@ -91,7 +32,10 @@ class IngredientParser:
if not unit:
return None
if len(unit) > 0:
u, created = Unit.objects.get_or_create(name=self.apply_unit_automation(unit), space=self.request.space)
if self.ignore_rules:
u, created = Unit.objects.get_or_create(name=unit.strip(), space=self.request.space)
else:
u, created = Unit.objects.get_or_create(name=self.automation.apply_unit_automation(unit), space=self.request.space)
return u
return None
@@ -104,7 +48,10 @@ class IngredientParser:
if not food:
return None
if len(food) > 0:
f, created = Food.objects.get_or_create(name=self.apply_food_automation(food), space=self.request.space)
if self.ignore_rules:
f, created = Food.objects.get_or_create(name=food.strip(), space=self.request.space)
else:
f, created = Food.objects.get_or_create(name=self.automation.apply_food_automation(food), space=self.request.space)
return f
return None
@@ -133,10 +80,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 +107,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
@@ -230,8 +178,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 +187,15 @@ 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)
if not self.ignore_rules:
ingredient = self.automation.apply_transpose_automation(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 +208,8 @@ class IngredientParser:
# three arguments if it already has a unit there can't be
# a fraction for the amount
if len(tokens) > 2:
if not self.ignore_rules:
tokens = self.automation.apply_never_unit_automation(tokens)
try:
if unit is not None:
# a unit is already found, no need to try the second argument for a fraction
@@ -303,10 +256,11 @@ class IngredientParser:
if unit_note not in note:
note += ' ' + unit_note
if unit:
unit = self.apply_unit_automation(unit.strip())
if unit and not self.ignore_rules:
unit = self.automation.apply_unit_automation(unit)
food = self.apply_food_automation(food.strip())
if food and not self.ignore_rules:
food = self.automation.apply_food_automation(food)
if len(food) > Food._meta.get_field('name').max_length: # test if food name is to long
# try splitting it at a space and taking only the first arg
if len(food.split()) > 1 and len(food.split()[0]) < Food._meta.get_field('name').max_length:

View File

@@ -1,6 +1,5 @@
from django.db.models import Q
from cookbook.models import Unit, SupermarketCategory, Property, PropertyType, Supermarket, SupermarketCategoryRelation, Food, Automation, UnitConversion, FoodProperty
from cookbook.models import (Food, FoodProperty, Property, PropertyType, Supermarket,
SupermarketCategory, SupermarketCategoryRelation, Unit, UnitConversion)
class OpenDataImporter:
@@ -33,7 +32,8 @@ class OpenDataImporter:
))
if self.update_existing:
return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('name', 'plural_name', 'base_unit', 'open_data_slug'), unique_fields=('space', 'name',))
return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=(
'name', 'plural_name', 'base_unit', 'open_data_slug'), unique_fields=('space', 'name',))
else:
return Unit.objects.bulk_create(insert_list, update_conflicts=True, update_fields=('open_data_slug',), unique_fields=('space', 'name',))
@@ -116,27 +116,25 @@ class OpenDataImporter:
self._update_slug_cache(Unit, 'unit')
self._update_slug_cache(PropertyType, 'property')
# pref_unit_key = 'preferred_unit_metric'
# pref_shopping_unit_key = 'preferred_packaging_unit_metric'
# if not self.use_metric:
# pref_unit_key = 'preferred_unit_imperial'
# pref_shopping_unit_key = 'preferred_packaging_unit_imperial'
insert_list = []
insert_list_flat = []
update_list = []
update_field_list = []
for k in list(self.data[datatype].keys()):
if not (self.data[datatype][k]['name'] in existing_objects_flat or self.data[datatype][k]['plural_name'] in existing_objects_flat):
insert_list.append({'data': {
'name': self.data[datatype][k]['name'],
'plural_name': self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
# 'preferred_unit_id': self.slug_id_cache['unit'][self.data[datatype][k][pref_unit_key]],
# 'preferred_shopping_unit_id': self.slug_id_cache['unit'][self.data[datatype][k][pref_shopping_unit_key]],
'supermarket_category_id': self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
'fdc_id': self.data[datatype][k]['fdc_id'] if self.data[datatype][k]['fdc_id'] != '' else None,
'open_data_slug': k,
'space': self.request.space.id,
}})
if not (self.data[datatype][k]['name'] in insert_list_flat or self.data[datatype][k]['plural_name'] in insert_list_flat):
insert_list.append({'data': {
'name': self.data[datatype][k]['name'],
'plural_name': self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
'supermarket_category_id': self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
'fdc_id': self.data[datatype][k]['fdc_id'] if self.data[datatype][k]['fdc_id'] != '' else None,
'open_data_slug': k,
'space': self.request.space.id,
}})
# build a fake second flat array to prevent duplicate foods from being inserted.
# trying to insert a duplicate would throw a db error :(
insert_list_flat.append(self.data[datatype][k]['name'])
insert_list_flat.append(self.data[datatype][k]['plural_name'])
else:
if self.data[datatype][k]['name'] in existing_objects:
existing_food_id = existing_objects[self.data[datatype][k]['name']][0]
@@ -149,8 +147,6 @@ class OpenDataImporter:
id=existing_food_id,
name=self.data[datatype][k]['name'],
plural_name=self.data[datatype][k]['plural_name'] if self.data[datatype][k]['plural_name'] != '' else None,
# preferred_unit_id=self.slug_id_cache['unit'][self.data[datatype][k][pref_unit_key]],
# preferred_shopping_unit_id=self.slug_id_cache['unit'][self.data[datatype][k][pref_shopping_unit_key]],
supermarket_category_id=self.slug_id_cache['category'][self.data[datatype][k]['store_category']],
fdc_id=self.data[datatype][k]['fdc_id'] if self.data[datatype][k]['fdc_id'] != '' else None,
open_data_slug=k,
@@ -166,23 +162,20 @@ class OpenDataImporter:
self._update_slug_cache(Food, 'food')
food_property_list = []
alias_list = []
# alias_list = []
for k in list(self.data[datatype].keys()):
for fp in self.data[datatype][k]['properties']['type_values']:
food_property_list.append(Property(
property_type_id=self.slug_id_cache['property'][fp['property_type']],
property_amount=fp['property_value'],
import_food_id=self.slug_id_cache['food'][k],
space=self.request.space,
))
# for a in self.data[datatype][k]['alias']:
# alias_list.append(Automation(
# param_1=a,
# param_2=self.data[datatype][k]['name'],
# space=self.request.space,
# created_by=self.request.user,
# ))
# try catch here because somettimes key "k" is not set for he food cache
try:
food_property_list.append(Property(
property_type_id=self.slug_id_cache['property'][fp['property_type']],
property_amount=fp['property_value'],
import_food_id=self.slug_id_cache['food'][k],
space=self.request.space,
))
except KeyError:
print(str(k) + ' is not in self.slug_id_cache["food"]')
Property.objects.bulk_create(food_property_list, ignore_conflicts=True, unique_fields=('space', 'import_food_id', 'property_type',))
@@ -192,7 +185,6 @@ class OpenDataImporter:
FoodProperty.objects.bulk_create(property_food_relation_list, ignore_conflicts=True, unique_fields=('food_id', 'property_id',))
# Automation.objects.bulk_create(alias_list, ignore_conflicts=True, unique_fields=('space', 'param_1', 'param_2',))
return insert_list + update_list
def import_conversion(self):
@@ -200,15 +192,19 @@ class OpenDataImporter:
insert_list = []
for k in list(self.data[datatype].keys()):
insert_list.append(UnitConversion(
base_amount=self.data[datatype][k]['base_amount'],
base_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['base_unit']],
converted_amount=self.data[datatype][k]['converted_amount'],
converted_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['converted_unit']],
food_id=self.slug_id_cache['food'][self.data[datatype][k]['food']],
open_data_slug=k,
space=self.request.space,
created_by=self.request.user,
))
# try catch here because sometimes key "k" is not set for he food cache
try:
insert_list.append(UnitConversion(
base_amount=self.data[datatype][k]['base_amount'],
base_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['base_unit']],
converted_amount=self.data[datatype][k]['converted_amount'],
converted_unit_id=self.slug_id_cache['unit'][self.data[datatype][k]['converted_unit']],
food_id=self.slug_id_cache['food'][self.data[datatype][k]['food']],
open_data_slug=k,
space=self.request.space,
created_by=self.request.user,
))
except KeyError:
print(str(k) + ' is not in self.slug_id_cache["food"]')
return UnitConversion.objects.bulk_create(insert_list, ignore_conflicts=True, unique_fields=('space', 'base_unit', 'converted_unit', 'food', 'open_data_slug'))

View File

@@ -4,16 +4,16 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.core.cache import cache
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.http import HttpResponseRedirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _
from oauth2_provider.contrib.rest_framework import TokenHasScope, TokenHasReadWriteScope
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope
from oauth2_provider.models import AccessToken
from rest_framework import permissions
from rest_framework.permissions import SAFE_METHODS
from cookbook.models import ShareLink, Recipe, UserSpace
from cookbook.models import Recipe, ShareLink, UserSpace
def get_allowed_groups(groups_required):
@@ -255,9 +255,6 @@ class CustomIsShared(permissions.BasePermission):
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
# # temporary hack to make old shopping list work with new shopping list
# if obj.__class__.__name__ in ['ShoppingList', 'ShoppingListEntry']:
# return is_object_shared(request.user, obj) or obj.created_by in list(request.user.get_shopping_share())
return is_object_shared(request.user, obj)
@@ -322,7 +319,8 @@ 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 +330,8 @@ 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):
@@ -361,7 +360,7 @@ class CustomTokenHasScope(TokenHasScope):
"""
def has_permission(self, request, view):
if type(request.auth) == AccessToken:
if isinstance(request.auth, AccessToken):
return super().has_permission(request, view)
else:
return request.user.is_authenticated
@@ -375,7 +374,7 @@ class CustomTokenHasReadWriteScope(TokenHasReadWriteScope):
"""
def has_permission(self, request, view):
if type(request.auth) == AccessToken:
if isinstance(request.auth, AccessToken):
return super().has_permission(request, view)
else:
return True
@@ -434,3 +433,10 @@ def switch_user_active_space(user, space):
return us
except ObjectDoesNotExist:
return None
class IsReadOnlyDRF(permissions.BasePermission):
message = 'You cannot interact with this object as it is not owned by you!'
def has_permission(self, request, view):
return request.method in SAFE_METHODS

View File

@@ -2,7 +2,7 @@ from django.core.cache import caches
from cookbook.helper.cache_helper import CacheHelper
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
from cookbook.models import PropertyType, Unit, Food, Property, Recipe, Step
from cookbook.models import PropertyType
class FoodPropertyHelper:
@@ -31,10 +31,12 @@ class FoodPropertyHelper:
if not property_types:
property_types = PropertyType.objects.filter(space=self.space).all()
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
# cache is cleared on property type save signal so long duration is fine
caches['default'].set(CacheHelper(self.space).PROPERTY_TYPE_CACHE_KEY, property_types, 60 * 60)
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, 'description': fpt.description,
'unit': fpt.unit, 'order': fpt.order, 'food_values': {}, 'total_value': 0, 'missing_value': False}
uch = UnitConversionHelper(self.space)
@@ -53,7 +55,8 @@ class FoodPropertyHelper:
if c.unit == i.food.properties_food_unit:
found_property = True
computed_properties[pt.id]['total_value'] += (c.amount / i.food.properties_food_amount) * p.property_amount
computed_properties[pt.id]['food_values'] = self.add_or_create(computed_properties[p.property_type.id]['food_values'], c.food.id, (c.amount / i.food.properties_food_amount) * p.property_amount, c.food)
computed_properties[pt.id]['food_values'] = self.add_or_create(
computed_properties[p.property_type.id]['food_values'], c.food.id, (c.amount / i.food.properties_food_amount) * p.property_amount, c.food)
if not found_property:
computed_properties[pt.id]['missing_value'] = True
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': i.food.name, 'value': 0}

View File

@@ -1,191 +0,0 @@
# import json
# import re
# from json import JSONDecodeError
# from urllib.parse import unquote
# from bs4 import BeautifulSoup
# from bs4.element import Tag
# from recipe_scrapers import scrape_html, scrape_me
# from recipe_scrapers._exceptions import NoSchemaFoundInWildMode
# from recipe_scrapers._utils import get_host_name, normalize_string
# from cookbook.helper import recipe_url_import as helper
# from cookbook.helper.scrapers.scrapers import text_scraper
# def get_recipe_from_source(text, url, request):
# def build_node(k, v):
# if isinstance(v, dict):
# node = {
# 'name': k,
# 'value': k,
# 'children': get_children_dict(v)
# }
# elif isinstance(v, list):
# node = {
# 'name': k,
# 'value': k,
# 'children': get_children_list(v)
# }
# else:
# node = {
# 'name': k + ": " + normalize_string(str(v)),
# 'value': normalize_string(str(v))
# }
# return node
# def get_children_dict(children):
# kid_list = []
# for k, v in children.items():
# kid_list.append(build_node(k, v))
# return kid_list
# def get_children_list(children):
# kid_list = []
# for kid in children:
# if type(kid) == list:
# node = {
# 'name': "unknown list",
# 'value': "unknown list",
# 'children': get_children_list(kid)
# }
# kid_list.append(node)
# elif type(kid) == dict:
# for k, v in kid.items():
# kid_list.append(build_node(k, v))
# else:
# kid_list.append({
# 'name': normalize_string(str(kid)),
# 'value': normalize_string(str(kid))
# })
# return kid_list
# recipe_tree = []
# parse_list = []
# soup = BeautifulSoup(text, "html.parser")
# html_data = get_from_html(soup)
# images = get_images_from_source(soup, url)
# text = unquote(text)
# scrape = None
# if url and not text:
# try:
# scrape = scrape_me(url_path=url, wild_mode=True)
# except(NoSchemaFoundInWildMode):
# pass
# if not scrape:
# try:
# parse_list.append(remove_graph(json.loads(text)))
# if not url and 'url' in parse_list[0]:
# url = parse_list[0]['url']
# scrape = text_scraper("<script type='application/ld+json'>" + text + "</script>", url=url)
# except JSONDecodeError:
# for el in soup.find_all('script', type='application/ld+json'):
# el = remove_graph(el)
# if not url and 'url' in el:
# url = el['url']
# if type(el) == list:
# for le in el:
# parse_list.append(le)
# elif type(el) == dict:
# parse_list.append(el)
# for el in soup.find_all(type='application/json'):
# el = remove_graph(el)
# if type(el) == list:
# for le in el:
# parse_list.append(le)
# elif type(el) == dict:
# parse_list.append(el)
# scrape = text_scraper(text, url=url)
# recipe_json = helper.get_from_scraper(scrape, request)
# # TODO: DEPRECATE recipe_tree & html_data. first validate it isn't used anywhere
# for el in parse_list:
# temp_tree = []
# if isinstance(el, Tag):
# try:
# el = json.loads(el.string)
# except TypeError:
# continue
# for k, v in el.items():
# if isinstance(v, dict):
# node = {
# 'name': k,
# 'value': k,
# 'children': get_children_dict(v)
# }
# elif isinstance(v, list):
# node = {
# 'name': k,
# 'value': k,
# 'children': get_children_list(v)
# }
# else:
# node = {
# 'name': k + ": " + normalize_string(str(v)),
# 'value': normalize_string(str(v))
# }
# temp_tree.append(node)
# if '@type' in el and el['@type'] == 'Recipe':
# recipe_tree += [{'name': 'ld+json', 'children': temp_tree}]
# else:
# recipe_tree += [{'name': 'json', 'children': temp_tree}]
# return recipe_json, recipe_tree, html_data, images
# def get_from_html(soup):
# INVISIBLE_ELEMS = ('style', 'script', 'head', 'title')
# html = []
# for s in soup.strings:
# if ((s.parent.name not in INVISIBLE_ELEMS) and (len(s.strip()) > 0)):
# html.append(s)
# return html
# def get_images_from_source(soup, url):
# sources = ['src', 'srcset', 'data-src']
# images = []
# img_tags = soup.find_all('img')
# if url:
# site = get_host_name(url)
# prot = url.split(':')[0]
# urls = []
# for img in img_tags:
# for src in sources:
# try:
# urls.append(img[src])
# except KeyError:
# pass
# for u in urls:
# u = u.split('?')[0]
# filename = re.search(r'/([\w_-]+[.](jpg|jpeg|gif|png))$', u)
# if filename:
# if (('http' not in u) and (url)):
# # sometimes an image source can be relative
# # if it is provide the base url
# u = '{}://{}{}'.format(prot, site, u)
# if 'http' in u:
# images.append(u)
# return images
# def remove_graph(el):
# # recipes type might be wrapped in @graph type
# if isinstance(el, Tag):
# try:
# el = json.loads(el.string)
# if '@graph' in el:
# for x in el['@graph']:
# if '@type' in x and x['@type'] == 'Recipe':
# el = x
# except (TypeError, JSONDecodeError):
# pass
# return el

View File

@@ -1,14 +1,11 @@
import json
from collections import Counter
from datetime import date, timedelta
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
from django.core.cache import cache, caches
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value,
When)
from django.core.cache import cache
from django.db.models import Avg, Case, Count, Exists, F, Max, OuterRef, Q, Subquery, Value, When
from django.db.models.functions import Coalesce, Lower, Substr
from django.utils import timezone, translation
from django.utils.translation import gettext as _
from cookbook.helper.HelperFunctions import Round, str2bool
from cookbook.managers import DICTIONARY
@@ -17,21 +14,25 @@ from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, Searc
from recipes import settings
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
class RecipeSearch():
_postgres = settings.DATABASES['default']['ENGINE'] in [
'django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']
_postgres = settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'
def __init__(self, request, **params):
self._request = request
self._queryset = None
if f := params.get('filter', None):
custom_filter = CustomFilter.objects.filter(id=f, space=self._request.space).filter(Q(created_by=self._request.user) |
Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first()
custom_filter = (
CustomFilter.objects.filter(id=f, space=self._request.space)
.filter(Q(created_by=self._request.user) | Q(shared=self._request.user) | Q(recipebook__shared=self._request.user))
.first()
)
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 +86,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:
@@ -98,24 +99,18 @@ class RecipeSearch():
self._search_type = self._search_prefs.search or 'plain'
if self._string:
if self._postgres:
self._unaccent_include = self._search_prefs.unaccent.values_list(
'field', flat=True)
self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
else:
self._unaccent_include = []
self._icontains_include = [
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
self._istartswith_include = [
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
self._icontains_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
self._istartswith_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
self._trigram_include = None
self._fulltext_include = None
self._trigram = False
if self._postgres and self._string:
self._language = DICTIONARY.get(
translation.get_language(), 'simple')
self._trigram_include = [
x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
self._fulltext_include = self._search_prefs.fulltext.values_list(
'field', flat=True) or None
self._language = DICTIONARY.get(translation.get_language(), 'simple')
self._trigram_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.trigram.values_list('field', flat=True)]
self._fulltext_include = self._search_prefs.fulltext.values_list('field', flat=True) or None
if self._search_type not in ['websearch', 'raw'] and self._trigram_include:
self._trigram = True
@@ -150,7 +145,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:
@@ -166,7 +161,7 @@ class RecipeSearch():
else:
order = []
# TODO add userpreference for default sort order and replace '-favorite'
default_order = ['-name']
default_order = ['name']
# recent and new_recipe are always first; they float a few recipes to the top
if self._num_recent:
order += ['-recent']
@@ -175,7 +170,6 @@ class RecipeSearch():
# if a sort order is provided by user - use that order
if self._sort_order:
if not isinstance(self._sort_order, list):
order += [self._sort_order]
else:
@@ -215,24 +209,18 @@ class RecipeSearch():
self._queryset = self._queryset.filter(query_filter).distinct()
if self._fulltext_include:
if self._fuzzy_match is None:
self._queryset = self._queryset.annotate(
score=Coalesce(Max(self.search_rank), 0.0))
self._queryset = self._queryset.annotate(score=Coalesce(Max(self.search_rank), 0.0))
else:
self._queryset = self._queryset.annotate(
rank=Coalesce(Max(self.search_rank), 0.0))
self._queryset = self._queryset.annotate(rank=Coalesce(Max(self.search_rank), 0.0))
if self._fuzzy_match is not None:
simularity = self._fuzzy_match.filter(
pk=OuterRef('pk')).values('simularity')
simularity = self._fuzzy_match.filter(pk=OuterRef('pk')).values('simularity')
if not self._fulltext_include:
self._queryset = self._queryset.annotate(
score=Coalesce(Subquery(simularity), 0.0))
self._queryset = self._queryset.annotate(score=Coalesce(Subquery(simularity), 0.0))
else:
self._queryset = self._queryset.annotate(
simularity=Coalesce(Subquery(simularity), 0.0))
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
self._queryset = self._queryset.annotate(
score=F('rank') + F('simularity'))
self._queryset = self._queryset.annotate(score=F('rank') + F('simularity'))
else:
query_filter = Q()
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
@@ -241,78 +229,69 @@ class RecipeSearch():
def _cooked_on_filter(self, cooked_date=None):
if self._sort_includes('lastcooked') or cooked_date:
lessthan = self._sort_includes(
'-lastcooked') or '-' in (cooked_date or [])[:1]
lessthan = self._sort_includes('-lastcooked') or '-' in (cooked_date or [])[:1]
if lessthan:
default = timezone.now() - timedelta(days=100000)
else:
default = timezone.now()
self._queryset = self._queryset.annotate(lastcooked=Coalesce(
Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default)))
self._queryset = self._queryset.annotate(
lastcooked=Coalesce(Max(Case(When(cooklog__created_by=self._request.user, cooklog__space=self._request.space, then='cooklog__created_at'))), Value(default))
)
if cooked_date is None:
return
cooked_date = date(*[int(x)
for x in cooked_date.split('-') if x != ''])
cooked_date = date(*[int(x)for x in cooked_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(
lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
self._queryset = self._queryset.filter(lastcooked__date__lte=cooked_date).exclude(lastcooked=default)
else:
self._queryset = self._queryset.filter(
lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
self._queryset = self._queryset.filter(lastcooked__date__gte=cooked_date).exclude(lastcooked=default)
def _created_on_filter(self, created_date=None):
if created_date is None:
return
lessthan = '-' in created_date[:1]
created_date = date(*[int(x)
for x in created_date.split('-') if x != ''])
created_date = date(*[int(x) for x in created_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(
created_at__date__lte=created_date)
self._queryset = self._queryset.filter(created_at__date__lte=created_date)
else:
self._queryset = self._queryset.filter(
created_at__date__gte=created_date)
self._queryset = self._queryset.filter(created_at__date__gte=created_date)
def _updated_on_filter(self, updated_date=None):
if updated_date is None:
return
lessthan = '-' in updated_date[:1]
updated_date = date(*[int(x)
for x in updated_date.split('-') if x != ''])
updated_date = date(*[int(x)for x in updated_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(
updated_at__date__lte=updated_date)
self._queryset = self._queryset.filter(updated_at__date__lte=updated_date)
else:
self._queryset = self._queryset.filter(
updated_at__date__gte=updated_date)
self._queryset = self._queryset.filter(updated_at__date__gte=updated_date)
def _viewed_on_filter(self, viewed_date=None):
if self._sort_includes('lastviewed') or viewed_date:
longTimeAgo = timezone.now() - timedelta(days=100000)
self._queryset = self._queryset.annotate(lastviewed=Coalesce(
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo)))
self._queryset = self._queryset.annotate(
lastviewed=Coalesce(Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__created_at'))), Value(longTimeAgo))
)
if viewed_date is None:
return
lessthan = '-' in viewed_date[:1]
viewed_date = date(*[int(x)
for x in viewed_date.split('-') if x != ''])
viewed_date = date(*[int(x)for x in viewed_date.split('-') if x != ''])
if lessthan:
self._queryset = self._queryset.filter(
lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
self._queryset = self._queryset.filter(lastviewed__date__lte=viewed_date).exclude(lastviewed=longTimeAgo)
else:
self._queryset = self._queryset.filter(
lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
self._queryset = self._queryset.filter(lastviewed__date__gte=viewed_date).exclude(lastviewed=longTimeAgo)
def _new_recipes(self, new_days=7):
# TODO make new days a user-setting
if not self._new:
return
self._queryset = (
self._queryset.annotate(new_recipe=Case(
When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')), default=Value(0), ))
self._queryset = self._queryset.annotate(
new_recipe=Case(
When(created_at__gte=(timezone.now() - timedelta(days=new_days)), then=('pk')),
default=Value(0),
)
)
def _recently_viewed(self, num_recent=None):
@@ -322,34 +301,35 @@ class RecipeSearch():
Max(Case(When(viewlog__created_by=self._request.user, viewlog__space=self._request.space, then='viewlog__pk'))), Value(0)))
return
num_recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space).values(
'recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(
pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
num_recent_recipes = (
ViewLog.objects.filter(created_by=self._request.user, space=self._request.space)
.values('recipe').annotate(recent=Max('created_at')).order_by('-recent')[:num_recent]
)
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
def _favorite_recipes(self, times_cooked=None):
if self._sort_includes('favorite') or times_cooked:
less_than = '-' in (times_cooked or []
) and not self._sort_includes('-favorite')
less_than = '-' in (times_cooked or []) and not self._sort_includes('-favorite')
if less_than:
default = 1000
else:
default = 0
favorite_recipes = CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk')
).values('recipe').annotate(count=Count('pk', distinct=True)).values('count')
self._queryset = self._queryset.annotate(
favorite=Coalesce(Subquery(favorite_recipes), default))
favorite_recipes = (
CookLog.objects.filter(created_by=self._request.user, space=self._request.space, recipe=OuterRef('pk'))
.values('recipe')
.annotate(count=Count('pk', distinct=True))
.values('count')
)
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
if times_cooked is None:
return
if times_cooked == '0':
self._queryset = self._queryset.filter(favorite=0)
elif less_than:
self._queryset = self._queryset.filter(favorite__lte=int(
times_cooked.replace('-', ''))).exclude(favorite=0)
self._queryset = self._queryset.filter(favorite__lte=int(times_cooked.replace('-', ''))).exclude(favorite=0)
else:
self._queryset = self._queryset.filter(
favorite__gte=int(times_cooked))
self._queryset = self._queryset.filter(favorite__gte=int(times_cooked))
def keyword_filters(self, **kwargs):
if all([kwargs[x] is None for x in kwargs]):
@@ -382,8 +362,7 @@ class RecipeSearch():
else:
self._queryset = self._queryset.filter(f_and)
if 'not' in kw_filter:
self._queryset = self._queryset.exclude(
id__in=recipes.values('id'))
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
def food_filters(self, **kwargs):
if all([kwargs[x] is None for x in kwargs]):
@@ -397,8 +376,7 @@ class RecipeSearch():
foods = Food.objects.filter(pk__in=kwargs[fd_filter])
if 'or' in fd_filter:
if self._include_children:
f_or = Q(
steps__ingredients__food__in=Food.include_descendants(foods))
f_or = Q(steps__ingredients__food__in=Food.include_descendants(foods))
else:
f_or = Q(steps__ingredients__food__in=foods)
@@ -410,8 +388,7 @@ class RecipeSearch():
recipes = Recipe.objects.all()
for food in foods:
if self._include_children:
f_and = Q(
steps__ingredients__food__in=food.get_descendants_and_self())
f_and = Q(steps__ingredients__food__in=food.get_descendants_and_self())
else:
f_and = Q(steps__ingredients__food=food)
if 'not' in fd_filter:
@@ -419,8 +396,7 @@ class RecipeSearch():
else:
self._queryset = self._queryset.filter(f_and)
if 'not' in fd_filter:
self._queryset = self._queryset.exclude(
id__in=recipes.values('id'))
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
def unit_filters(self, units=None, operator=True):
if operator != True:
@@ -429,27 +405,25 @@ class RecipeSearch():
return
if not isinstance(units, list):
units = [units]
self._queryset = self._queryset.filter(
steps__ingredients__unit__in=units)
self._queryset = self._queryset.filter(steps__ingredients__unit__in=units)
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))
@@ -477,14 +451,11 @@ class RecipeSearch():
recipes = Recipe.objects.all()
for book in kwargs[bk_filter]:
if 'not' in bk_filter:
recipes = recipes.filter(
recipebookentry__book__id=book)
recipes = recipes.filter(recipebookentry__book__id=book)
else:
self._queryset = self._queryset.filter(
recipebookentry__book__id=book)
self._queryset = self._queryset.filter(recipebookentry__book__id=book)
if 'not' in bk_filter:
self._queryset = self._queryset.exclude(
id__in=recipes.values('id'))
self._queryset = self._queryset.exclude(id__in=recipes.values('id'))
def step_filters(self, steps=None, operator=True):
if operator != True:
@@ -503,25 +474,20 @@ class RecipeSearch():
rank = []
if 'name' in self._fulltext_include:
vectors.append('name_search_vector')
rank.append(SearchRank('name_search_vector',
self.search_query, cover_density=True))
rank.append(SearchRank('name_search_vector', self.search_query, cover_density=True))
if 'description' in self._fulltext_include:
vectors.append('desc_search_vector')
rank.append(SearchRank('desc_search_vector',
self.search_query, cover_density=True))
rank.append(SearchRank('desc_search_vector', self.search_query, cover_density=True))
if 'steps__instruction' in self._fulltext_include:
vectors.append('steps__search_vector')
rank.append(SearchRank('steps__search_vector',
self.search_query, cover_density=True))
rank.append(SearchRank('steps__search_vector', self.search_query, cover_density=True))
if 'keywords__name' in self._fulltext_include:
# explicitly settings unaccent on keywords and foods so that they behave the same as search_vector fields
vectors.append('keywords__name__unaccent')
rank.append(SearchRank('keywords__name__unaccent',
self.search_query, cover_density=True))
rank.append(SearchRank('keywords__name__unaccent', self.search_query, cover_density=True))
if 'steps__ingredients__food__name' in self._fulltext_include:
vectors.append('steps__ingredients__food__name__unaccent')
rank.append(SearchRank('steps__ingredients__food__name',
self.search_query, cover_density=True))
rank.append(SearchRank('steps__ingredients__food__name', self.search_query, cover_density=True))
for r in rank:
if self.search_rank is None:
@@ -529,8 +495,7 @@ class RecipeSearch():
else:
self.search_rank += r
# modifying queryset will annotation creates duplicate results
self._filters.append(Q(id__in=Recipe.objects.annotate(
vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
self._filters.append(Q(id__in=Recipe.objects.annotate(vector=SearchVector(*vectors)).filter(Q(vector=self.search_query))))
def build_text_filters(self, string=None):
if not string:
@@ -555,15 +520,19 @@ class RecipeSearch():
trigram += TrigramSimilarity(f, self._string)
else:
trigram = TrigramSimilarity(f, self._string)
self._fuzzy_match = Recipe.objects.annotate(trigram=trigram).distinct(
).annotate(simularity=Max('trigram')).values('id', 'simularity').filter(simularity__gt=self._search_prefs.trigram_threshold)
self._fuzzy_match = (
Recipe.objects.annotate(trigram=trigram)
.distinct()
.annotate(simularity=Max('trigram'))
.values('id', 'simularity')
.filter(simularity__gt=self._search_prefs.trigram_threshold)
)
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]
shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
onhand_filter = (
Q(steps__ingredients__food__onhand_users__in=shopping_users) # food onhand
@@ -573,264 +542,40 @@ class RecipeSearch():
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
)
makenow_recipes = Recipe.objects.annotate(
count_food=Count('steps__ingredients__food__pk', filter=Q(
steps__ingredients__food__isnull=False), distinct=True),
count_onhand=Count('steps__ingredients__food__pk',
filter=onhand_filter, distinct=True),
count_ignore_shopping=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True,
steps__ingredients__food__recipe__isnull=True), distinct=True),
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(
shopping_users), then=Value(1)), default=Value(0)),
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(
shopping_users), then=Value(1)), default=Value(0))
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood=missing)
self._queryset = self._queryset.distinct().filter(
id__in=makenow_recipes.values('id'))
count_food=Count('steps__ingredients__food__pk', filter=Q(steps__ingredients__food__isnull=False), distinct=True),
count_onhand=Count('steps__ingredients__food__pk', filter=onhand_filter, distinct=True),
count_ignore_shopping=Count(
'steps__ingredients__food__pk', filter=Q(steps__ingredients__food__ignore_shopping=True, steps__ingredients__food__recipe__isnull=True), distinct=True
),
has_child_sub=Case(When(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users), then=Value(1)), default=Value(0)),
has_sibling_sub=Case(When(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users), then=Value(1)), default=Value(0))
).annotate(missingfood=F('count_food') - F('count_onhand') - F('count_ignore_shopping')).filter(missingfood__lte=missing)
self._queryset = self._queryset.distinct().filter(id__in=makenow_recipes.values('id'))
@staticmethod
def __children_substitute_filter(shopping_users=None):
children_onhand_subquery = Food.objects.filter(
path__startswith=OuterRef('path'),
depth__gt=OuterRef('depth'),
onhand_users__in=shopping_users
children_onhand_subquery = Food.objects.filter(path__startswith=OuterRef('path'), depth__gt=OuterRef('depth'), onhand_users__in=shopping_users)
return (
Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
)
.exclude(depth=1, numchild=0)
.filter(substitute_children=True)
.annotate(child_onhand_count=Exists(children_onhand_subquery))
.filter(child_onhand_count=True)
)
return Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
Q(onhand_users__in=shopping_users)
| Q(ignore_shopping=True, recipe__isnull=True)
| Q(substitute__onhand_users__in=shopping_users)
).exclude(depth=1, numchild=0
).filter(substitute_children=True
).annotate(child_onhand_count=Exists(children_onhand_subquery)
).filter(child_onhand_count=True)
@staticmethod
def __sibling_substitute_filter(shopping_users=None):
sibling_onhand_subquery = Food.objects.filter(
path__startswith=Substr(
OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)),
depth=OuterRef('depth'),
onhand_users__in=shopping_users
path__startswith=Substr(OuterRef('path'), 1, Food.steplen * (OuterRef('depth') - 1)), depth=OuterRef('depth'), onhand_users__in=shopping_users
)
return Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
Q(onhand_users__in=shopping_users)
| Q(ignore_shopping=True, recipe__isnull=True)
| Q(substitute__onhand_users__in=shopping_users)
).exclude(depth=1, numchild=0
).filter(substitute_siblings=True
).annotate(sibling_onhand=Exists(sibling_onhand_subquery)
).filter(sibling_onhand=True)
class RecipeFacet():
class CacheEmpty(Exception):
pass
def __init__(self, request, queryset=None, hash_key=None, cache_timeout=3600):
if hash_key is None and queryset is None:
raise ValueError(_("One of queryset or hash_key must be provided"))
self._request = request
self._queryset = queryset
self.hash_key = hash_key or str(hash(self._queryset.query))
self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}"
self._cache_timeout = cache_timeout
self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, {})
if self._cache is None and self._queryset is None:
raise self.CacheEmpty("No queryset provided and cache empty")
self.Keywords = self._cache.get('Keywords', None)
self.Foods = self._cache.get('Foods', None)
self.Books = self._cache.get('Books', None)
self.Ratings = self._cache.get('Ratings', None)
# TODO Move Recent to recipe annotation/serializer: requrires change in RecipeSearch(), RecipeSearchView.vue and serializer
self.Recent = self._cache.get('Recent', None)
if self._queryset is not None:
self._recipe_list = list(
self._queryset.values_list('id', flat=True))
self._search_params = {
'keyword_list': self._request.query_params.getlist('keywords', []),
'food_list': self._request.query_params.getlist('foods', []),
'book_list': self._request.query_params.getlist('book', []),
'search_keywords_or': str2bool(self._request.query_params.get('keywords_or', True)),
'search_foods_or': str2bool(self._request.query_params.get('foods_or', True)),
'search_books_or': str2bool(self._request.query_params.get('books_or', True)),
'space': self._request.space,
}
elif self.hash_key is not None:
self._recipe_list = self._cache.get('recipe_list', [])
self._search_params = {
'keyword_list': self._cache.get('keyword_list', None),
'food_list': self._cache.get('food_list', None),
'book_list': self._cache.get('book_list', None),
'search_keywords_or': self._cache.get('search_keywords_or', None),
'search_foods_or': self._cache.get('search_foods_or', None),
'search_books_or': self._cache.get('search_books_or', None),
'space': self._cache.get('space', None),
}
self._cache = {
**self._search_params,
'recipe_list': self._recipe_list,
'Ratings': self.Ratings,
'Recent': self.Recent,
'Keywords': self.Keywords,
'Foods': self.Foods,
'Books': self.Books
}
caches['default'].set(self._SEARCH_CACHE_KEY,
self._cache, self._cache_timeout)
def get_facets(self, from_cache=False):
if from_cache:
return {
'cache_key': self.hash_key or '',
'Ratings': self.Ratings or {},
'Recent': self.Recent or [],
'Keywords': self.Keywords or [],
'Foods': self.Foods or [],
'Books': self.Books or []
}
return {
'cache_key': self.hash_key,
'Ratings': self.get_ratings(),
'Recent': self.get_recent(),
'Keywords': self.get_keywords(),
'Foods': self.get_foods(),
'Books': self.get_books()
}
def set_cache(self, key, value):
self._cache = {**self._cache, key: value}
caches['default'].set(
self._SEARCH_CACHE_KEY,
self._cache,
self._cache_timeout
return (
Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
Q(onhand_users__in=shopping_users) | Q(ignore_shopping=True, recipe__isnull=True) | Q(substitute__onhand_users__in=shopping_users)
)
.exclude(depth=1, numchild=0)
.filter(substitute_siblings=True)
.annotate(sibling_onhand=Exists(sibling_onhand_subquery))
.filter(sibling_onhand=True)
)
def get_books(self):
if self.Books is None:
self.Books = []
return self.Books
def get_keywords(self):
if self.Keywords is None:
if self._search_params['search_keywords_or']:
keywords = Keyword.objects.filter(
space=self._request.space).distinct()
else:
keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(
depth=1)).filter(space=self._request.space).distinct()
# set keywords to root objects only
keywords = self._keyword_queryset(keywords)
self.Keywords = [{**x, 'children': None}
if x['numchild'] > 0 else x for x in list(keywords)]
self.set_cache('Keywords', self.Keywords)
return self.Keywords
def get_foods(self):
if self.Foods is None:
# # if using an OR search, will annotate all keywords, otherwise, just those that appear in results
if self._search_params['search_foods_or']:
foods = Food.objects.filter(
space=self._request.space).distinct()
else:
foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(
depth=1)).filter(space=self._request.space).distinct()
# set keywords to root objects only
foods = self._food_queryset(foods)
self.Foods = [{**x, 'children': None}
if x['numchild'] > 0 else x for x in list(foods)]
self.set_cache('Foods', self.Foods)
return self.Foods
def get_ratings(self):
if self.Ratings is None:
if not self._request.space.demo and self._request.space.show_facet_count:
if self._queryset is None:
self._queryset = Recipe.objects.filter(
id__in=self._recipe_list)
rating_qs = self._queryset.annotate(rating=Round(Avg(Case(When(
cooklog__created_by=self._request.user, then='cooklog__rating'), default=Value(0)))))
self.Ratings = dict(Counter(r.rating for r in rating_qs))
else:
self.Rating = {}
self.set_cache('Ratings', self.Ratings)
return self.Ratings
def get_recent(self):
if self.Recent is None:
# TODO make days of recent recipe a setting
recent_recipes = ViewLog.objects.filter(created_by=self._request.user, space=self._request.space, created_at__gte=timezone.now() - timedelta(days=14)
).values_list('recipe__pk', flat=True)
self.Recent = list(recent_recipes)
self.set_cache('Recent', self.Recent)
return self.Recent
def add_food_children(self, id):
try:
food = Food.objects.get(id=id)
nodes = food.get_ancestors()
except Food.DoesNotExist:
return self.get_facets()
foods = self._food_queryset(food.get_children(), food)
deep_search = self.Foods
for node in nodes:
index = next((i for i, x in enumerate(
deep_search) if x["id"] == node.id), None)
deep_search = deep_search[index]['children']
index = next((i for i, x in enumerate(
deep_search) if x["id"] == food.id), None)
deep_search[index]['children'] = [
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)]
self.set_cache('Foods', self.Foods)
return self.get_facets()
def add_keyword_children(self, id):
try:
keyword = Keyword.objects.get(id=id)
nodes = keyword.get_ancestors()
except Keyword.DoesNotExist:
return self.get_facets()
keywords = self._keyword_queryset(keyword.get_children(), keyword)
deep_search = self.Keywords
for node in nodes:
index = next((i for i, x in enumerate(
deep_search) if x["id"] == node.id), None)
deep_search = deep_search[index]['children']
index = next((i for i, x in enumerate(deep_search)
if x["id"] == keyword.id), None)
deep_search[index]['children'] = [
{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)]
self.set_cache('Keywords', self.Keywords)
return self.get_facets()
def _recipe_count_queryset(self, field, depth=1, steplen=4):
return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path'), f'{field}__depth__gte': depth}, id__in=self._recipe_list, space=self._request.space
).annotate(count=Coalesce(Func('pk', function='Count'), 0)).values('count')
def _keyword_queryset(self, queryset, keyword=None):
depth = getattr(keyword, 'depth', 0) + 1
steplen = depth * Keyword.steplen
if not self._request.space.demo and self._request.space.show_facet_count:
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0)
).filter(depth=depth, count__gt=0
).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
else:
return queryset.filter(depth=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
def _food_queryset(self, queryset, food=None):
depth = getattr(food, 'depth', 0) + 1
steplen = depth * Food.steplen
if not self._request.space.demo and self._request.space.show_facet_count:
return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0)
).filter(depth__lte=depth, count__gt=0
).values('id', 'name', 'count', 'numchild').order_by(Lower('name').asc())[:200]
else:
return queryset.filter(depth__lte=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())

View File

@@ -1,9 +1,7 @@
# import random
import re
import traceback
from html import unescape
from django.core.cache import caches
from django.utils.dateparse import parse_duration
from django.utils.translation import gettext as _
from isodate import parse_duration as iso_parse_duration
@@ -11,20 +9,37 @@ from isodate.isoerror import ISO8601Error
from pytube import YouTube
from recipe_scrapers._utils import get_host_name, get_minutes
# from cookbook.helper import recipe_url_import as helper
from cookbook.helper.automation_helper import AutomationEngine
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.models import Automation, Keyword, PropertyType
# from unicodedata import decomposition
# from recipe_scrapers._utils import get_minutes ## temporary until/unless upstream incorporates get_minutes() PR
def get_from_scraper(scrape, request):
# converting the scrape_me object to the existing json format based on ld+json
recipe_json = {}
recipe_json = {
'steps': [],
'internal': True
}
keywords = []
# assign source URL
try:
source_url = scrape.canonical_url()
except Exception:
try:
source_url = scrape.url
except Exception:
pass
if source_url:
recipe_json['source_url'] = source_url
try:
keywords.append(source_url.replace('http://', '').replace('https://', '').split('/')[0])
except Exception:
recipe_json['source_url'] = ''
automation_engine = AutomationEngine(request, source=recipe_json.get('source_url'))
# assign recipe name
try:
recipe_json['name'] = parse_name(scrape.title()[:128] or None)
except Exception:
@@ -38,6 +53,10 @@ def get_from_scraper(scrape, request):
if isinstance(recipe_json['name'], list) and len(recipe_json['name']) > 0:
recipe_json['name'] = recipe_json['name'][0]
recipe_json['name'] = automation_engine.apply_regex_replace_automation(recipe_json['name'], Automation.NAME_REPLACE)
# assign recipe description
# TODO notify user about limit if reached - >256 description will be truncated
try:
description = scrape.description() or None
except Exception:
@@ -48,16 +67,20 @@ def get_from_scraper(scrape, request):
except Exception:
description = ''
recipe_json['internal'] = True
recipe_json['description'] = parse_description(description)
recipe_json['description'] = automation_engine.apply_regex_replace_automation(recipe_json['description'], Automation.DESCRIPTION_REPLACE)
# assign servings attributes
try:
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
recipe_json['servings'] = parse_servings(servings)
recipe_json['servings_text'] = parse_servings_text(servings)
# assign time attributes
try:
recipe_json['working_time'] = get_minutes(scrape.prep_time()) or 0
except Exception:
@@ -82,6 +105,7 @@ def get_from_scraper(scrape, request):
except Exception:
pass
# assign image
try:
recipe_json['image'] = parse_image(scrape.image()) or None
except Exception:
@@ -92,7 +116,7 @@ def get_from_scraper(scrape, request):
except Exception:
recipe_json['image'] = ''
keywords = []
# assign keywords
try:
if scrape.schema.data.get("keywords"):
keywords += listify_keywords(scrape.schema.data.get("keywords"))
@@ -117,20 +141,6 @@ def get_from_scraper(scrape, request):
except Exception:
pass
try:
source_url = scrape.canonical_url()
except Exception:
try:
source_url = scrape.url
except Exception:
pass
if source_url:
recipe_json['source_url'] = source_url
try:
keywords.append(source_url.replace('http://', '').replace('https://', '').split('/')[0])
except Exception:
recipe_json['source_url'] = ''
try:
if scrape.author():
keywords.append(scrape.author())
@@ -138,33 +148,25 @@ def get_from_scraper(scrape, request):
pass
try:
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space)
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request)
except AttributeError:
recipe_json['keywords'] = keywords
ingredient_parser = IngredientParser(request, True)
recipe_json['steps'] = []
# assign 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:
recipe_json['steps'].append({'instruction': '', 'ingredients': [], })
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]
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)
if len(parsed_description) > 256: # split at 256 as long descriptions don't look good on recipe cards
recipe_json['steps'][0]['instruction'] = f'*{parsed_description}* \n\n' + recipe_json['steps'][0]['instruction']
if len(recipe_json['description']) > 256: # split at 256 as long descriptions don't look good on recipe cards
recipe_json['steps'][0]['instruction'] = f"*{recipe_json['description']}* \n\n" + recipe_json['steps'][0]['instruction']
else:
recipe_json['description'] = parsed_description[:512]
recipe_json['description'] = recipe_json['description'][:512]
try:
for x in scrape.ingredients():
@@ -205,12 +207,9 @@ def get_from_scraper(scrape, request):
traceback.print_exc()
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]
for a in automations:
if re.match(a.param_1, (recipe_json['source_url'])[:512]):
for s in recipe_json['steps']:
s['instruction'] = re.sub(a.param_2, a.param_3, s['instruction'])
for s in recipe_json['steps']:
s['instruction'] = automation_engine.apply_regex_replace_automation(s['instruction'], Automation.INSTRUCTION_REPLACE)
# re.sub(a.param_2, a.param_3, s['instruction'])
return recipe_json
@@ -260,11 +259,14 @@ def get_from_youtube_scraper(url, request):
]
}
# TODO add automation here
try:
automation_engine = AutomationEngine(request, source=url)
video = YouTube(url=url)
default_recipe_json['name'] = video.title
default_recipe_json['name'] = automation_engine.apply_regex_replace_automation(video.title, Automation.NAME_REPLACE)
default_recipe_json['image'] = video.thumbnail_url
default_recipe_json['steps'][0]['instruction'] = video.description
default_recipe_json['steps'][0]['instruction'] = automation_engine.apply_regex_replace_automation(video.description, Automation.INSTRUCTION_REPLACE)
except Exception:
pass
@@ -272,7 +274,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 +318,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 +343,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 +360,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 +374,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 +396,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:
@@ -403,18 +405,9 @@ def parse_time(recipe_time):
return recipe_time
def parse_keywords(keyword_json, space):
def parse_keywords(keyword_json, request):
keywords = []
keyword_aliases = {}
# retrieve keyword automation cache if it exists, otherwise build from database
KEYWORD_CACHE_KEY = f'automation_keyword_alias_{space.pk}'
if c := caches['default'].get(KEYWORD_CACHE_KEY, None):
keyword_aliases = c
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
caches['default'].set(KEYWORD_CACHE_KEY, keyword_aliases, 30)
automation_engine = AutomationEngine(request)
# keywords as list
for kw in keyword_json:
@@ -422,12 +415,8 @@ def parse_keywords(keyword_json, space):
# if alias exists use that instead
if len(kw) != 0:
if keyword_aliases:
try:
kw = keyword_aliases[kw]
except KeyError:
pass
if k := Keyword.objects.filter(name=kw, space=space).first():
automation_engine.apply_keyword_automation(kw)
if k := Keyword.objects.filter(name=kw, space=request.space).first():
keywords.append({'label': str(k), 'name': k.name, 'id': k.id})
else:
keywords.append({'label': kw, 'name': kw})
@@ -438,15 +427,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 +489,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

@@ -1,8 +1,6 @@
from django.urls import reverse
from django_scopes import scope, scopes_disabled
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
from rest_framework.authentication import TokenAuthentication
from rest_framework.authtoken.models import Token
from rest_framework.exceptions import AuthenticationFailed
from cookbook.views import views
@@ -50,7 +48,6 @@ class ScopeMiddleware:
return views.no_groups(request)
request.space = user_space.space
# with scopes_disabled():
with scope(space=request.space):
return self.get_response(request)
else:

View File

@@ -1,16 +1,13 @@
from datetime import timedelta
from decimal import Decimal
from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import F, OuterRef, Q, Subquery, Value
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.utils.translation import gettext as _
from cookbook.helper.HelperFunctions import Round, str2bool
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
SupermarketCategoryRelation)
from recipes import settings
def shopping_helper(qs, request):
@@ -47,7 +44,7 @@ class RecipeShoppingEditor():
self.mealplan = self._kwargs.get('mealplan', None)
if type(self.mealplan) in [int, float]:
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
if type(self.mealplan) == dict:
if isinstance(self.mealplan, dict):
self.mealplan = MealPlan.objects.filter(id=self.mealplan['id'], space=self.space).first()
self.id = self._kwargs.get('id', None)
@@ -69,11 +66,12 @@ class RecipeShoppingEditor():
@property
def _recipe_servings(self):
return getattr(self.recipe, 'servings', None) or getattr(getattr(self.mealplan, 'recipe', None), 'servings', None) or getattr(getattr(self._shopping_list_recipe, 'recipe', None), 'servings', None)
return getattr(self.recipe, 'servings', None) or getattr(getattr(self.mealplan, 'recipe', None), 'servings',
None) or getattr(getattr(self._shopping_list_recipe, 'recipe', None), 'servings', None)
@property
def _servings_factor(self):
return Decimal(self.servings)/Decimal(self._recipe_servings)
return Decimal(self.servings) / Decimal(self._recipe_servings)
@property
def _shared_users(self):
@@ -90,9 +88,10 @@ class RecipeShoppingEditor():
def get_recipe_ingredients(self, id, exclude_onhand=False):
if exclude_onhand:
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(food__onhand_users__id__in=[x.id for x in self._shared_users])
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(
food__onhand_users__id__in=[x.id for x in self._shared_users])
else:
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
@property
def _include_related(self):
@@ -109,7 +108,7 @@ class RecipeShoppingEditor():
self.servings = float(servings)
if mealplan := kwargs.get('mealplan', None):
if type(mealplan) == dict:
if isinstance(mealplan, dict):
self.mealplan = MealPlan.objects.filter(id=mealplan['id'], space=self.space).first()
else:
self.mealplan = mealplan
@@ -170,14 +169,14 @@ class RecipeShoppingEditor():
try:
self._shopping_list_recipe.delete()
return True
except:
except BaseException:
return False
def _add_ingredients(self, ingredients=None):
if not ingredients:
return
elif type(ingredients) == list:
ingredients = Ingredient.objects.filter(id__in=ingredients)
elif isinstance(ingredients, list):
ingredients = Ingredient.objects.filter(id__in=ingredients, food__ignore_shopping=False)
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
add_ingredients = ingredients.exclude(id__in=existing)
@@ -199,120 +198,3 @@ class RecipeShoppingEditor():
to_delete = self._shopping_list_recipe.entries.exclude(ingredient__in=ingredients)
ShoppingListEntry.objects.filter(id__in=to_delete).delete()
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
# # TODO refactor as class
# def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None, append=False):
# """
# Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
# :param list_recipe: Modify an existing ShoppingListRecipe
# :param recipe: Recipe to use as list of ingredients. One of [recipe, mealplan] are required
# :param mealplan: alternatively use a mealplan recipe as source of ingredients
# :param servings: Optional: Number of servings to use to scale shoppinglist. If servings = 0 an existing recipe list will be deleted
# :param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
# :param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list
# """
# r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
# if not r:
# raise ValueError(_("You must supply a recipe or mealplan"))
# created_by = created_by or getattr(ShoppingListEntry.objects.filter(list_recipe=list_recipe).first(), 'created_by', None)
# if not created_by:
# raise ValueError(_("You must supply a created_by"))
# try:
# servings = float(servings)
# except (ValueError, TypeError):
# servings = getattr(mealplan, 'servings', 1.0)
# servings_factor = servings / r.servings
# shared_users = list(created_by.get_shopping_share())
# shared_users.append(created_by)
# if list_recipe:
# created = False
# else:
# list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings)
# created = True
# related_step_ing = []
# if servings == 0 and not created:
# list_recipe.delete()
# return []
# elif ingredients:
# ingredients = Ingredient.objects.filter(pk__in=ingredients, space=space)
# else:
# ingredients = Ingredient.objects.filter(step__recipe=r, food__ignore_shopping=False, space=space)
# if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
# ingredients = ingredients.exclude(food__onhand_users__id__in=[x.id for x in shared_users])
# if related := created_by.userpreference.mealplan_autoinclude_related:
# # TODO: add levels of related recipes (related recipes of related recipes) to use when auto-adding mealplans
# related_recipes = r.get_related_recipes()
# for x in related_recipes:
# # related recipe is a Step serving size is driven by recipe serving size
# # TODO once/if Steps can have a serving size this needs to be refactored
# if exclude_onhand:
# # if steps are used more than once in a recipe or subrecipe - I don' think this results in the desired behavior
# related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users]).values_list('id', flat=True)
# else:
# related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True)
# x_ing = []
# if ingredients.filter(food__recipe=x).exists():
# for ing in ingredients.filter(food__recipe=x):
# if exclude_onhand:
# x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
# else:
# x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__ignore_shopping=True)
# for i in [x for x in x_ing]:
# ShoppingListEntry.objects.create(
# list_recipe=list_recipe,
# food=i.food,
# unit=i.unit,
# ingredient=i,
# amount=i.amount * Decimal(servings_factor),
# created_by=created_by,
# space=space,
# )
# # dont' add food to the shopping list that are actually recipes that will be added as ingredients
# ingredients = ingredients.exclude(food__recipe=x)
# add_ingredients = list(ingredients.values_list('id', flat=True)) + related_step_ing
# if not append:
# existing_list = ShoppingListEntry.objects.filter(list_recipe=list_recipe)
# # delete shopping list entries not included in ingredients
# existing_list.exclude(ingredient__in=ingredients).delete()
# # add shopping list entries that did not previously exist
# add_ingredients = set(add_ingredients) - set(existing_list.values_list('ingredient__id', flat=True))
# add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
# # if servings have changed, update the ShoppingListRecipe and existing Entries
# if servings <= 0:
# servings = 1
# if not created and list_recipe.servings != servings:
# update_ingredients = set(ingredients.values_list('id', flat=True)) - set(add_ingredients.values_list('id', flat=True))
# list_recipe.servings = servings
# list_recipe.save()
# for sle in ShoppingListEntry.objects.filter(list_recipe=list_recipe, ingredient__id__in=update_ingredients):
# sle.amount = sle.ingredient.amount * Decimal(servings_factor)
# sle.save()
# # add any missing Entries
# for i in [x for x in add_ingredients if x.food]:
# ShoppingListEntry.objects.create(
# list_recipe=list_recipe,
# food=i.food,
# unit=i.unit,
# ingredient=i,
# amount=i.amount * Decimal(servings_factor),
# created_by=created_by,
# space=space,
# )
# # return all shopping list items
# return list_recipe

View File

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

@@ -4,6 +4,7 @@ from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step
@@ -19,6 +20,10 @@ class Chowdown(Integration):
direction_mode = False
description_mode = False
description = None
prep_time = None
serving = None
ingredients = []
directions = []
descriptions = []
@@ -26,6 +31,12 @@ class Chowdown(Integration):
line = fl.decode("utf-8")
if 'title:' in line:
title = line.replace('title:', '').replace('"', '').strip()
if 'description:' in line:
description = line.replace('description:', '').replace('"', '').strip()
if 'prep_time:' in line:
prep_time = line.replace('prep_time:', '').replace('"', '').strip()
if 'yield:' in line:
serving = line.replace('yield:', '').replace('"', '').strip()
if 'image:' in line:
image = line.replace('image:', '').strip()
if 'tags:' in line:
@@ -48,15 +59,43 @@ class Chowdown(Integration):
descriptions.append(line)
recipe = Recipe.objects.create(name=title, created_by=self.request.user, internal=True, space=self.request.space)
if description:
recipe.description = description
for k in tags.split(','):
print(f'adding keyword {k.strip()}')
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
recipe.keywords.add(keyword)
step = Step.objects.create(
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions), space=self.request.space,
)
ingredients_added = False
for direction in directions:
if len(direction.strip()) > 0:
step = Step.objects.create(
instruction=direction, name='', space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
else:
step = Step.objects.create(
instruction=direction, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
if not ingredients_added:
ingredients_added = True
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, food, note = ingredient_parser.parse(ingredient)
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,
))
recipe.steps.add(step)
if serving:
recipe.servings = parse_servings(serving)
recipe.servings_text = 'servings'
if prep_time:
recipe.working_time = parse_time(prep_time)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
@@ -76,6 +115,7 @@ class Chowdown(Integration):
if re.match(f'^images/{image}$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename))
recipe.save()
return recipe
def get_file_from_recipe(self, recipe):

View File

@@ -1,20 +1,15 @@
import base64
import gzip
import json
import re
from gettext import gettext as _
from io import BytesIO
import requests
import validators
import yaml
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import (get_from_scraper, get_images_from_soup,
iso_duration_to_minutes)
from cookbook.helper.scrapers.scrapers import text_scraper
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step
from cookbook.models import Ingredient, Recipe, Step
class CookBookApp(Integration):
@@ -25,7 +20,6 @@ class CookBookApp(Integration):
def get_recipe_from_file(self, file):
recipe_html = file.getvalue().decode("utf-8")
# recipe_json, recipe_tree, html_data, images = get_recipe_from_source(recipe_html, 'CookBookApp', self.request)
scrape = text_scraper(text=recipe_html)
recipe_json = get_from_scraper(scrape, self.request)
images = list(dict.fromkeys(get_images_from_soup(scrape.soup, None)))
@@ -37,7 +31,7 @@ class CookBookApp(Integration):
try:
recipe.servings = re.findall('([0-9])+', recipe_json['recipeYield'])[0]
except Exception as e:
except Exception:
pass
try:
@@ -47,7 +41,8 @@ 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']
@@ -62,7 +57,7 @@ class CookBookApp(Integration):
if unit := ingredient.get('unit', None):
u = ingredient_parser.get_unit(unit.get('name', None))
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=ingredient.get('amount', None), note=ingredient.get('note', None), original_text=ingredient.get('original_text', None), space=self.request.space,
food=f, unit=u, amount=ingredient.get('amount', None), note=ingredient.get('note', None), original_text=ingredient.get('original_text', None), space=self.request.space,
))
if len(images) > 0:

View File

@@ -1,17 +1,12 @@
import base64
import json
from io import BytesIO
from gettext import gettext as _
import requests
import validators
from lxml import etree
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, parse_time, parse_servings_text
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step
from cookbook.models import Ingredient, Recipe, Step
class Cookmate(Integration):
@@ -50,7 +45,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

@@ -1,4 +1,3 @@
import re
from io import BytesIO
from zipfile import ZipFile
@@ -26,12 +25,13 @@ class CopyMeThat(Integration):
except AttributeError:
source = None
recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip()[:128], source_url=source, created_by=self.request.user, internal=True, space=self.request.space, )
recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip(
)[:128], source_url=source, created_by=self.request.user, internal=True, space=self.request.space, )
for category in file.find_all("span", {"class": "recipeCategory"}):
keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space)
recipe.keywords.add(keyword)
try:
recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip())
recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
@@ -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)
@@ -61,7 +61,14 @@ class CopyMeThat(Integration):
if not isinstance(ingredient, Tag) or not ingredient.text.strip() or "recipeIngredient_spacer" in ingredient['class']:
continue
if any(x in ingredient['class'] for x in ["recipeIngredient_subheader", "recipeIngredient_note"]):
step.ingredients.add(Ingredient.objects.create(is_header=True, note=ingredient.text.strip()[:256], original_text=ingredient.text.strip(), space=self.request.space, ))
step.ingredients.add(
Ingredient.objects.create(
is_header=True,
note=ingredient.text.strip()[
:256],
original_text=ingredient.text.strip(),
space=self.request.space,
))
else:
amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(food)
@@ -78,7 +85,7 @@ class CopyMeThat(Integration):
step.save()
recipe.steps.add(step)
step = Step.objects.create(instruction='', space=self.request.space, )
step.name = instruction.text.strip()[:128]
else:
step.instruction += instruction.text.strip() + ' \n\n'

View File

@@ -1,4 +1,5 @@
import json
import traceback
from io import BytesIO, StringIO
from re import match
from zipfile import ZipFile
@@ -19,7 +20,10 @@ class Default(Integration):
recipe = self.decode_recipe(recipe_string)
images = list(filter(lambda v: match('image.*', v), recipe_zip.namelist()))
if images:
self.import_recipe_image(recipe, BytesIO(recipe_zip.read(images[0])), filetype=get_filetype(images[0]))
try:
self.import_recipe_image(recipe, BytesIO(recipe_zip.read(images[0])), filetype=get_filetype(images[0]))
except AttributeError:
traceback.print_exc()
return recipe
def decode_recipe(self, string):
@@ -54,7 +58,7 @@ class Default(Integration):
try:
recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read())
except ValueError:
except (ValueError, FileNotFoundError):
pass
recipe_zip_obj.close()
@@ -67,4 +71,4 @@ class Default(Integration):
export_zip_obj.close()
return [[ self.get_export_file_name(), export_zip_stream.getvalue() ]]
return [[self.get_export_file_name(), export_zip_stream.getvalue()]]

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

@@ -1,4 +1,3 @@
import traceback
import datetime
import traceback
import uuid
@@ -18,8 +17,7 @@ from lxml import etree
from cookbook.helper.image_processing import handle_image
from cookbook.models import Keyword, Recipe
from recipes.settings import DEBUG
from recipes.settings import EXPORT_FILE_CACHE_DURATION
from recipes.settings import DEBUG, EXPORT_FILE_CACHE_DURATION
class Integration:
@@ -39,7 +37,6 @@ class Integration:
self.ignored_recipes = []
description = f'Imported by {request.user.get_user_display_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}. Type: {export_type}'
icon = '📥'
try:
last_kw = Keyword.objects.filter(name__regex=r'^(Import [0-9]+)', space=request.space).latest('created_at')
@@ -52,23 +49,19 @@ class Integration:
self.keyword = parent.add_child(
name=name,
description=description,
icon=icon,
space=request.space
)
except (IntegrityError, ValueError): # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
self.keyword = parent.add_child(
name=f'{name} {str(uuid.uuid4())[0:8]}',
description=description,
icon=icon,
space=request.space
)
def do_export(self, recipes, el):
with scope(space=self.request.space):
el.total_recipes = len(recipes)
el.total_recipes = len(recipes)
el.cache_duration = EXPORT_FILE_CACHE_DURATION
el.save()
@@ -80,7 +73,7 @@ class Integration:
export_file = file
else:
#zip the files if there is more then one file
# zip the files if there is more then one file
export_filename = self.get_export_file_name()
export_stream = BytesIO()
export_obj = ZipFile(export_stream, 'w')
@@ -91,8 +84,7 @@ class Integration:
export_obj.close()
export_file = export_stream.getvalue()
cache.set('export_file_'+str(el.pk), {'filename': export_filename, 'file': export_file}, EXPORT_FILE_CACHE_DURATION)
cache.set('export_file_' + str(el.pk), {'filename': export_filename, 'file': export_file}, EXPORT_FILE_CACHE_DURATION)
el.running = False
el.save()
@@ -100,7 +92,6 @@ class Integration:
response['Content-Disposition'] = 'attachment; filename="' + export_filename + '"'
return response
def import_file_name_filter(self, zip_info_object):
"""
Since zipfile.namelist() returns all files in all subdirectories this function allows filtering of files
@@ -164,7 +155,7 @@ class Integration:
for z in file_list:
try:
if not hasattr(z, 'filename') or type(z) == Tag:
if not hasattr(z, 'filename') or isinstance(z, Tag):
recipe = self.get_recipe_from_file(z)
else:
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
@@ -298,7 +289,6 @@ class Integration:
if DEBUG:
traceback.print_exc()
def get_export_file_name(self, format='zip'):
return "export_{}.{}".format(datetime.datetime.now().strftime("%Y-%m-%d"), format)

View File

@@ -7,7 +7,7 @@ from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text, parse_time
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Recipe, Step
from cookbook.models import Ingredient, Keyword, Recipe, Step
class Mealie(Integration):
@@ -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()
@@ -56,6 +56,12 @@ class Mealie(Integration):
except Exception:
pass
if 'tags' in recipe_json and len(recipe_json['tags']) > 0:
for k in recipe_json['tags']:
if 'name' in k:
keyword, created = Keyword.objects.get_or_create(name=k['name'].strip(), space=self.request.space)
recipe.keywords.add(keyword)
if 'notes' in recipe_json and len(recipe_json['notes']) > 0:
notes_text = "#### Notes \n\n"
for n in recipe_json['notes']:

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

@@ -57,7 +57,7 @@ class MelaRecipes(Integration):
recipe.source_url = recipe_json['link']
step = Step.objects.create(
instruction=instruction, space=self.request.space,
instruction=instruction, space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients
)
ingredient_parser = IngredientParser(self.request, True)

View File

@@ -2,13 +2,14 @@ import json
import re
from io import BytesIO, StringIO
from zipfile import ZipFile
from PIL import Image
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import iso_duration_to_minutes
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step, NutritionInformation
from cookbook.models import Ingredient, Keyword, NutritionInformation, Recipe, Step
class NextcloudCookbook(Integration):
@@ -51,14 +52,13 @@ class NextcloudCookbook(Integration):
ingredients_added = False
for s in recipe_json['recipeInstructions']:
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:
@@ -91,7 +91,7 @@ class NextcloudCookbook(Integration):
if nutrition != {}:
recipe.nutrition = NutritionInformation.objects.create(**nutrition, space=self.request.space)
recipe.save()
except Exception as e:
except Exception:
pass
for f in self.files:

View File

@@ -1,9 +1,11 @@
import json
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Recipe, Step, Keyword, Comment, CookLog
from django.utils.translation import gettext as _
from cookbook.models import Comment, CookLog, Ingredient, Keyword, Recipe, Step
class OpenEats(Integration):
@@ -25,16 +27,16 @@ class OpenEats(Integration):
if file["source"] != '':
instructions += '\n' + _('Recipe source:') + f'[{file["source"]}]({file["source"]})'
cuisine_keyword, created = Keyword.objects.get_or_create(name="Cuisine", space=self.request.space)
cuisine_keyword, created = Keyword.objects.get_or_create(name="Cuisine", space=self.request.space)
if file["cuisine"] != '':
keyword, created = Keyword.objects.get_or_create(name=file["cuisine"].strip(), space=self.request.space)
keyword, created = Keyword.objects.get_or_create(name=file["cuisine"].strip(), space=self.request.space)
if created:
keyword.move(cuisine_keyword, pos="last-child")
recipe.keywords.add(keyword)
course_keyword, created = Keyword.objects.get_or_create(name="Course", space=self.request.space)
course_keyword, created = Keyword.objects.get_or_create(name="Course", space=self.request.space)
if file["course"] != '':
keyword, created = Keyword.objects.get_or_create(name=file["course"].strip(), space=self.request.space)
keyword, created = Keyword.objects.get_or_create(name=file["course"].strip(), space=self.request.space)
if created:
keyword.move(course_keyword, pos="last-child")
recipe.keywords.add(keyword)
@@ -51,7 +53,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:
@@ -90,7 +90,7 @@ class Paprika(Integration):
if validators.url(url, public=True):
response = requests.get(url)
self.import_recipe_image(recipe, BytesIO(response.content))
except:
except Exception:
if recipe_json.get("photo_data", None):
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_json['photo_data'])), filetype='.jpeg')

View File

@@ -1,21 +1,11 @@
import json
from io import BytesIO
from re import match
from zipfile import ZipFile
import asyncio
from pyppeteer import launch
from rest_framework.renderers import JSONRenderer
from cookbook.helper.image_processing import get_filetype
from cookbook.integration.integration import Integration
from cookbook.serializer import RecipeExportSerializer
from cookbook.models import ExportLog
from asgiref.sync import sync_to_async
import django.core.management.commands.runserver as runserver
import logging
from asgiref.sync import sync_to_async
from pyppeteer import launch
from cookbook.integration.integration import Integration
class PDFexport(Integration):
@@ -42,7 +32,6 @@ class PDFexport(Integration):
}
}
files = []
for recipe in recipes:
@@ -50,20 +39,18 @@ class PDFexport(Integration):
await page.emulateMedia('print')
await page.setCookie(cookies)
await page.goto('http://'+cmd.default_addr+':'+cmd.default_port+'/view/recipe/'+str(recipe.id), {'waitUntil': 'domcontentloaded'})
await page.waitForSelector('#printReady');
await page.goto('http://' + cmd.default_addr + ':' + cmd.default_port + '/view/recipe/' + str(recipe.id), {'waitUntil': 'domcontentloaded'})
await page.waitForSelector('#printReady')
files.append([recipe.name + '.pdf', await page.pdf(options)])
await page.close();
await page.close()
el.exported_recipes += 1
el.msg += self.get_recipe_processed_msg(recipe)
await sync_to_async(el.save, thread_sensitive=True)()
await browser.close()
return files
def get_files_from_recipes(self, recipes, el, cookie):
return asyncio.run(self.get_files_from_recipes_async(recipes, el, cookie))

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

@@ -39,7 +39,7 @@ class RecipeSage(Integration):
ingredients_added = False
for s in file['recipeInstructions']:
step = Step.objects.create(
instruction=s['text'], space=self.request.space,
instruction=s['text'], space=self.request.space, show_ingredients_table=self.request.user.userpreference.show_step_ingredients,
)
if not ingredients_added:
ingredients_added = True

View File

@@ -2,12 +2,10 @@ import base64
from io import BytesIO
from xml import etree
from lxml import etree
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_time, parse_servings, parse_servings_text
from cookbook.helper.recipe_url_import import parse_servings, parse_servings_text
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Recipe, Step, Keyword
from cookbook.models import Ingredient, Keyword, Recipe, Step
class Rezeptsuitede(Integration):
@@ -37,7 +35,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:
@@ -61,14 +59,14 @@ class Rezeptsuitede(Integration):
try:
k, created = Keyword.objects.get_or_create(name=recipe_xml.find('head').find('cat').text.strip(), space=self.request.space)
recipe.keywords.add(k)
except Exception as e:
except Exception:
pass
recipe.save()
try:
self.import_recipe_image(recipe, BytesIO(base64.b64decode(recipe_xml.find('head').find('picbin').text)), filetype='.jpeg')
except:
except BaseException:
pass
return recipe

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)
@@ -60,8 +60,8 @@ class RezKonv(Integration):
def split_recipe_file(self, file):
recipe_list = []
current_recipe = ''
encoding_list = ['windows-1250',
'latin-1'] # TODO build algorithm to try trough encodings and fail if none work, use for all importers
# TODO build algorithm to try trough encodings and fail if none work, use for all importers
# encoding_list = ['windows-1250', 'latin-1']
encoding = 'windows-1250'
for fl in file.readlines():
try:

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:
@@ -59,11 +59,11 @@ class Saffron(Integration):
def get_file_from_recipe(self, recipe):
data = "Title: "+recipe.name if recipe.name else ""+"\n"
data += "Description: "+recipe.description if recipe.description else ""+"\n"
data = "Title: " + recipe.name if recipe.name else "" + "\n"
data += "Description: " + recipe.description if recipe.description else "" + "\n"
data += "Source: \n"
data += "Original URL: \n"
data += "Yield: "+str(recipe.servings)+"\n"
data += "Yield: " + str(recipe.servings) + "\n"
data += "Cookbook: \n"
data += "Section: \n"
data += "Image: \n"
@@ -78,13 +78,13 @@ class Saffron(Integration):
data += "Ingredients: \n"
for ingredient in recipeIngredient:
data += ingredient+"\n"
data += ingredient + "\n"
data += "Instructions: \n"
for instruction in recipeInstructions:
data += instruction+"\n"
data += instruction + "\n"
return recipe.name+'.txt', data
return recipe.name + '.txt', data
def get_files_from_recipes(self, recipes, el, cookie):
files = []

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-29 18:42+0200\n"
"PO-Revision-Date: 2022-07-06 14:32+0000\n"
"Last-Translator: Nidhal Brniyah <n1a1b1@gmail.com>\n"
"PO-Revision-Date: 2023-11-28 11:03+0000\n"
"Last-Translator: Mahmoud Aljouhari <mapgohary@gmail.com>\n"
"Language-Team: Arabic <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/ar/>\n"
"Language: ar\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"X-Generator: Weblate 4.10.1\n"
"X-Generator: Weblate 4.15\n"
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
#: .\cookbook\templates\space.html:49 .\cookbook\templates\stats.html:28
@@ -2578,7 +2578,7 @@ msgstr ""
#: .\cookbook\views\views.py:262
msgid "This feature is not available in the demo version!"
msgstr ""
msgstr "هذه الميزة غير موجودة في النسخة التجريبية!"
#: .\cookbook\views\views.py:322
msgid "You must select at least one field to search!"

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-11-22 18:19+0000\n"
"Last-Translator: Spreez <tandoor@larsdev.de>\n"
"Language-Team: German <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/de/>\n"
"Language: de\n"
@@ -161,7 +161,7 @@ msgstr "Name"
#: .\cookbook\forms.py:124 .\cookbook\forms.py:315 .\cookbook\views\lists.py:88
msgid "Keywords"
msgstr "Stichwörter"
msgstr "Schlüsselwörter"
#: .\cookbook\forms.py:125
msgid "Preparation time in minutes"
@@ -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-23 09: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"
@@ -262,6 +262,8 @@ msgid ""
"You can use markdown to format this field. See the <a href=\"/docs/markdown/"
"\">docs here</a>"
msgstr ""
"Μπορείτε να χρησιμοποιήσετε τη μορφοποίηση Markdown για να διαμορφώσετε αυτό "
"το πεδίο. Δείτε τα <a href=\"/docs/markdown/\">έγγραφα εδώ</a>"
#: .\cookbook\forms.py:366
msgid "Maximum number of users for this space reached."
@@ -309,6 +311,8 @@ msgid ""
"Use fuzzy matching on units, keywords and ingredients when editing and "
"importing recipes."
msgstr ""
"Χρησιμοποιήστε ασαφείς (fuzzy) αντιστοιχίες σε μονάδες μέτρησης, λέξεις-"
"κλειδιά και συστατικά κατά την επεξεργασία και εισαγωγή συνταγών."
#: .\cookbook\forms.py:451
msgid ""
@@ -323,6 +327,8 @@ msgid ""
"Fields to search for partial matches. (e.g. searching for 'Pie' will return "
"'pie' and 'piece' and 'soapie')"
msgstr ""
"Πεδία για αναζήτηση μερικών αντιστοιχιών. (π.χ. αναζήτηση για 'πίτα' τα "
"'τυρόπιτα' και 'απιτα' θα βρίσκονται στα αποτελέσματα)"
#: .\cookbook\forms.py:455
msgid ""
@@ -420,7 +426,7 @@ msgstr ""
#: .\cookbook\forms.py:506
msgid "Delimiter to use for CSV exports."
msgstr ""
msgstr "Το σημείο στίξης διαχωρισμού δεκαδικών για τις εξαγωγές σε αρχεία CSV."
#: .\cookbook\forms.py:507
msgid "Prefix to add when copying list to the clipboard."
@@ -454,7 +460,7 @@ msgstr "Προεπιλεγμένες ώρες καθυστέρησης"
#: .\cookbook\forms.py:517
msgid "Filter to Supermarket"
msgstr ""
msgstr "Ταξινόμηση ανά Supermarket"
#: .\cookbook\forms.py:518
msgid "Recent Days"
@@ -462,7 +468,7 @@ msgstr "Πρόσφατες ημέρες"
#: .\cookbook\forms.py:519
msgid "CSV Delimiter"
msgstr ""
msgstr "CSV σημείο στίξης διαχωρισμού δεκαδικών"
#: .\cookbook\forms.py:520
msgid "List Prefix"
@@ -474,7 +480,7 @@ msgstr "Αυτόματα διαθέσιμο"
#: .\cookbook\forms.py:531
msgid "Reset Food Inheritance"
msgstr ""
msgstr "Επαναφορά κληρονομιάς φαγητών"
#: .\cookbook\forms.py:532
msgid "Reset all food to inherit the fields configured."
@@ -531,7 +537,7 @@ msgstr "Έχετε περισσότερους χρήστες από το επι
#: .\cookbook\helper\recipe_search.py:565
msgid "One of queryset or hash_key must be provided"
msgstr ""
msgstr "Πρέπει να παρέχετε είτε το queryset είτε το hash_key"
#: .\cookbook\helper\shopping_helper.py:152
msgid "You must supply a servings size"
@@ -619,6 +625,8 @@ msgstr "Αναδόμηση πλήρους ευρετηρίου αναζήτησ
#: .\cookbook\management\commands\rebuildindex.py:18
msgid "Only Postgresql databases use full text search, no index to rebuild"
msgstr ""
"Μόνο οι βάσεις δεδομένων Postgresql χρησιμοποιούν αναζήτηση πλήρους "
"κειμένου, δεν υπάρχει ανάγκη ανασύνθεσης των ευρετηρίων"
#: .\cookbook\management\commands\rebuildindex.py:29
msgid "Recipe index rebuild complete."
@@ -780,13 +788,15 @@ msgstr "Πρόσκληση στο Tandoor Recipes"
#: .\cookbook\serializer.py:1209
msgid "Existing shopping list to update"
msgstr ""
msgstr "Υπάρχουσα λίστα αγορών για ενημέρωση"
#: .\cookbook\serializer.py:1211
msgid ""
"List of ingredient IDs from the recipe to add, if not provided all "
"ingredients will be added."
msgstr ""
"Λίστα αναγνωριστικών συστατικών (ID) από τη συνταγή προς προσθήκη. Εάν δεν "
"παρέχονται όλα τα συστατικά θα προστεθούν."
#: .\cookbook\serializer.py:1213
msgid ""
@@ -795,21 +805,23 @@ msgstr ""
#: .\cookbook\serializer.py:1222
msgid "Amount of food to add to the shopping list"
msgstr ""
msgstr "Ποσότητα του φαγητού που θα προστεθεί στη λίστα αγορών"
#: .\cookbook\serializer.py:1224
msgid "ID of unit to use for the shopping list"
msgstr ""
msgstr "Το ID της μονάδας μέτρησης που θα χρησιμοποιείται στη λίστα αγορών"
#: .\cookbook\serializer.py:1226
msgid "When set to true will delete all food from active shopping lists."
msgstr ""
"Όταν οριστεί σε true, θα διαγραφούν όλα τα τρόφιμα από τις ενεργές λίστες "
"αγορών."
#: .\cookbook\tables.py:36 .\cookbook\templates\generic\edit_template.html:6
#: .\cookbook\templates\generic\edit_template.html:14
#: .\cookbook\templates\recipes_table.html:82
msgid "Edit"
msgstr ""
msgstr "Τροποποίηση"
#: .\cookbook\tables.py:116 .\cookbook\tables.py:131
#: .\cookbook\templates\generic\delete_template.html:7
@@ -817,28 +829,28 @@ msgstr ""
#: .\cookbook\templates\generic\edit_template.html:28
#: .\cookbook\templates\recipes_table.html:90
msgid "Delete"
msgstr ""
msgstr "Διαγραφή"
#: .\cookbook\templates\404.html:5
msgid "404 Error"
msgstr ""
msgstr "404 Error"
#: .\cookbook\templates\404.html:18
msgid "The page you are looking for could not be found."
msgstr ""
msgstr "Η σελίδα που αναζητάτε δεν μπορεί να βρεθεί."
#: .\cookbook\templates\404.html:33
msgid "Take me Home"
msgstr ""
msgstr "Πήγαινε με στη αρχική σελίδα"
#: .\cookbook\templates\404.html:35
msgid "Report a Bug"
msgstr ""
msgstr "Αναφορά σφάλματος"
#: .\cookbook\templates\account\email.html:6
#: .\cookbook\templates\account\email.html:17
msgid "E-mail Addresses"
msgstr ""
msgstr "Διευθύνσεις e-mail"
#: .\cookbook\templates\account\email.html:12
#: .\cookbook\templates\account\password_change.html:11
@@ -847,68 +859,71 @@ msgstr ""
#: .\cookbook\templates\settings.html:17
#: .\cookbook\templates\socialaccount\connections.html:10
msgid "Settings"
msgstr ""
msgstr "Ρυθμίσεις"
#: .\cookbook\templates\account\email.html:13
msgid "Email"
msgstr ""
msgstr "Email"
#: .\cookbook\templates\account\email.html:19
msgid "The following e-mail addresses are associated with your account:"
msgstr ""
msgstr "Οι παρακάτω διευθύνσεις e-mail συνδέονται με τον λογαριασμό σας:"
#: .\cookbook\templates\account\email.html:36
msgid "Verified"
msgstr ""
msgstr "Πιστοποιημένο"
#: .\cookbook\templates\account\email.html:38
msgid "Unverified"
msgstr ""
msgstr "Μη πιστοποιημένο"
#: .\cookbook\templates\account\email.html:40
msgid "Primary"
msgstr ""
msgstr "Κύριο"
#: .\cookbook\templates\account\email.html:47
msgid "Make Primary"
msgstr ""
msgstr "Μετατροπή σε κύριο"
#: .\cookbook\templates\account\email.html:49
msgid "Re-send Verification"
msgstr ""
msgstr "Επαναποστολή της επαλήθευσης"
#: .\cookbook\templates\account\email.html:50
#: .\cookbook\templates\generic\delete_template.html:57
#: .\cookbook\templates\socialaccount\connections.html:44
msgid "Remove"
msgstr ""
msgstr "Αφαίρεση"
#: .\cookbook\templates\account\email.html:58
msgid "Warning:"
msgstr ""
msgstr "Προειδοποίηση:"
#: .\cookbook\templates\account\email.html:58
msgid ""
"You currently do not have any e-mail address set up. You should really add "
"an e-mail address so you can receive notifications, reset your password, etc."
msgstr ""
"Προς το παρόν, δεν έχετε καμία διεύθυνση e-mail καταχωρημένη. Θα πρέπει να "
"προσθέσετε μια διεύθυνση ηλεκτρονικού ταχυδρομείου, ώστε να μπορείτε να "
"λαμβάνετε ειδοποιήσεις, να επαναφέρετε τον κωδικό πρόσβασης, κ.λπ."
#: .\cookbook\templates\account\email.html:64
msgid "Add E-mail Address"
msgstr ""
msgstr "Προσθήκη διεύθυνσης e-mail"
#: .\cookbook\templates\account\email.html:69
msgid "Add E-mail"
msgstr ""
msgstr "Προσθήκη e-mail"
#: .\cookbook\templates\account\email.html:79
msgid "Do you really want to remove the selected e-mail address?"
msgstr ""
msgstr "Θέλετε πραγματικά να αφαιρέσετε την επιλεγμένη διεύθυνση e-mail;"
#: .\cookbook\templates\account\email_confirm.html:6
#: .\cookbook\templates\account\email_confirm.html:10
msgid "Confirm E-mail Address"
msgstr ""
msgstr "Επιβεβαίωση διεύθυνσης e-mail"
#: .\cookbook\templates\account\email_confirm.html:16
#, python-format
@@ -918,11 +933,15 @@ msgid ""
"for user %(user_display)s\n"
" ."
msgstr ""
"Παρακαλώ επιβεβαιώστε ότι το\n"
" <a href=\"mailto:%(email)s\">%(email)s</a> είναι μια διεύθυνση "
"e-mail για τον χρήστη %(user_display)s\n"
" ."
#: .\cookbook\templates\account\email_confirm.html:22
#: .\cookbook\templates\generic\delete_template.html:72
msgid "Confirm"
msgstr ""
msgstr "Επιβεβαίωση"
#: .\cookbook\templates\account\email_confirm.html:29
#, python-format
@@ -931,11 +950,15 @@ msgid ""
" <a href=\"%(email_url)s\">issue a new e-mail confirmation "
"request</a>."
msgstr ""
"Αυτός ο σύνδεσμος επιβεβαίωσης έχει λήξει είναι δεν είναι έγκυρος. Παρακαλώ "
"\n"
" <a href=\"%(email_url)s\">κάντε ένα νέο αίτημα για επιβεβαιωτικό "
"e-mail</a>."
#: .\cookbook\templates\account\login.html:8
#: .\cookbook\templates\base.html:340 .\cookbook\templates\openid\login.html:8
msgid "Login"
msgstr ""
msgstr "Σύνδεση"
#: .\cookbook\templates\account\login.html:15
#: .\cookbook\templates\account\login.html:31
@@ -945,41 +968,43 @@ msgstr ""
#: .\cookbook\templates\openid\login.html:26
#: .\cookbook\templates\socialaccount\authentication_error.html:15
msgid "Sign In"
msgstr ""
msgstr "Σύνδεση"
#: .\cookbook\templates\account\login.html:34
#: .\cookbook\templates\socialaccount\signup.html:8
#: .\cookbook\templates\socialaccount\signup.html:57
msgid "Sign Up"
msgstr ""
msgstr "Εγγραφή"
#: .\cookbook\templates\account\login.html:39
#: .\cookbook\templates\account\login.html:41
#: .\cookbook\templates\account\password_reset.html:29
msgid "Reset My Password"
msgstr ""
msgstr "Επαναφορά κωδικού πρόσβασης"
#: .\cookbook\templates\account\login.html:40
msgid "Lost your password?"
msgstr ""
msgstr "Χασάτε τον κωδικό πρόσβασης;"
#: .\cookbook\templates\account\login.html:52
msgid "Social Login"
msgstr ""
msgstr "Σύνδεση με social media"
#: .\cookbook\templates\account\login.html:53
msgid "You can use any of the following providers to sign in."
msgstr ""
"Μπορείτε να χρησιμοποιήσετε οποιονδήποτε από τους παρακάτω παρόχους για να "
"συνδεθείτε."
#: .\cookbook\templates\account\logout.html:5
#: .\cookbook\templates\account\logout.html:9
#: .\cookbook\templates\account\logout.html:18
msgid "Sign Out"
msgstr ""
msgstr "Αποσύνδεση"
#: .\cookbook\templates\account\logout.html:11
msgid "Are you sure you want to sign out?"
msgstr ""
msgstr "Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε;"
#: .\cookbook\templates\account\password_change.html:6
#: .\cookbook\templates\account\password_change.html:16
@@ -989,44 +1014,50 @@ msgstr ""
#: .\cookbook\templates\account\password_reset_from_key_done.html:7
#: .\cookbook\templates\account\password_reset_from_key_done.html:13
msgid "Change Password"
msgstr ""
msgstr "Αλλαγή κωδικού πρόσβασης"
#: .\cookbook\templates\account\password_change.html:12
#: .\cookbook\templates\account\password_set.html:12
#: .\cookbook\templates\settings.html:76
msgid "Password"
msgstr ""
msgstr "Κωδικός πρόσβασης"
#: .\cookbook\templates\account\password_change.html:22
msgid "Forgot Password?"
msgstr ""
msgstr "Ξεχάσατε τον κωδικό πρόσβασης;"
#: .\cookbook\templates\account\password_reset.html:7
#: .\cookbook\templates\account\password_reset.html:13
#: .\cookbook\templates\account\password_reset_done.html:7
#: .\cookbook\templates\account\password_reset_done.html:10
msgid "Password Reset"
msgstr ""
msgstr "Επαναφορά κωδικού πρόσβασης"
#: .\cookbook\templates\account\password_reset.html:24
msgid ""
"Forgotten your password? Enter your e-mail address below, and we'll send you "
"an e-mail allowing you to reset it."
msgstr ""
"Ξεχάσατε τον κωδικό πρόσβασης σας; Εισάγετε τη διεύθυνση ηλεκτρονικού "
"ταχυδρομείου σας παρακάτω και θα σας στείλουμε ένα email που θα σας "
"επιτρέψει να τον επαναφέρετε."
#: .\cookbook\templates\account\password_reset.html:32
msgid "Password reset is disabled on this instance."
msgstr ""
"Η επαναφορά κωδικού πρόσβασης είναι απενεργοποιημένη σε αυτήν την πλατφόρμα."
#: .\cookbook\templates\account\password_reset_done.html:16
msgid ""
"We have sent you an e-mail. Please contact us if you do not receive it "
"within a few minutes."
msgstr ""
"Σας έχουμε στείλει ένα email. Παρακαλούμε επικοινωνήστε μαζί μας αν δεν το "
"λάβετε εντός λίγων λεπτών."
#: .\cookbook\templates\account\password_reset_from_key.html:13
msgid "Bad Token"
msgstr ""
msgstr "Μη έγκυρο token"
#: .\cookbook\templates\account\password_reset_from_key.html:25
#, python-format
@@ -1036,168 +1067,172 @@ msgid ""
" Please request a <a href=\"%(passwd_reset_url)s\">new "
"password reset</a>."
msgstr ""
"Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης ήταν άκυρος, πιθανώς επειδή έχει "
"ήδη χρησιμοποιηθεί.\n"
" Παρακαλώ ζητήστε έναν <a href=\"%(passwd_reset_url)s\""
">νέο σύνδεσμο επαναφοράς κωδικού πρόσβασης</a>."
#: .\cookbook\templates\account\password_reset_from_key.html:33
msgid "change password"
msgstr ""
msgstr "Αλλαγή κωδικού πρόσβασης"
#: .\cookbook\templates\account\password_reset_from_key.html:36
#: .\cookbook\templates\account\password_reset_from_key_done.html:19
msgid "Your password is now changed."
msgstr ""
msgstr "Ο κωδικός πρόσβασης σας έχει αλλάξει."
#: .\cookbook\templates\account\password_set.html:6
#: .\cookbook\templates\account\password_set.html:16
#: .\cookbook\templates\account\password_set.html:21
msgid "Set Password"
msgstr ""
msgstr "Ορισμός Κωδικού Πρόσβασης"
#: .\cookbook\templates\account\signup.html:6
msgid "Register"
msgstr ""
msgstr "Εγγραφή"
#: .\cookbook\templates\account\signup.html:12
msgid "Create an Account"
msgstr ""
msgstr "Δημιουργία λογαριασμού"
#: .\cookbook\templates\account\signup.html:42
#: .\cookbook\templates\socialaccount\signup.html:33
msgid "I accept the follwoing"
msgstr ""
msgstr "Αποδέχομαι τα παρακάτω"
#: .\cookbook\templates\account\signup.html:45
#: .\cookbook\templates\socialaccount\signup.html:36
msgid "Terms and Conditions"
msgstr ""
msgstr "Όροι και προϋποθέσεις"
#: .\cookbook\templates\account\signup.html:48
#: .\cookbook\templates\socialaccount\signup.html:39
msgid "and"
msgstr ""
msgstr "και"
#: .\cookbook\templates\account\signup.html:52
#: .\cookbook\templates\socialaccount\signup.html:43
msgid "Privacy Policy"
msgstr ""
msgstr "Πολιτική απορρήτου"
#: .\cookbook\templates\account\signup.html:65
msgid "Create User"
msgstr ""
msgstr "Δημιουργία χρήστη"
#: .\cookbook\templates\account\signup.html:69
msgid "Already have an account?"
msgstr ""
msgstr "Έχετε ήδη λογαριασμό;"
#: .\cookbook\templates\account\signup_closed.html:5
#: .\cookbook\templates\account\signup_closed.html:11
msgid "Sign Up Closed"
msgstr ""
msgstr "Οι εγγραφές έκλεισαν"
#: .\cookbook\templates\account\signup_closed.html:13
msgid "We are sorry, but the sign up is currently closed."
msgstr ""
msgstr "Λυπούμαστε, αλλά οι εγγραφές έχουν ήδη κλείσει."
#: .\cookbook\templates\api_info.html:5 .\cookbook\templates\base.html:330
#: .\cookbook\templates\rest_framework\api.html:11
msgid "API Documentation"
msgstr ""
msgstr "Τεκμηρίωση API"
#: .\cookbook\templates\base.html:103 .\cookbook\templates\index.html:87
#: .\cookbook\templates\stats.html:22
msgid "Recipes"
msgstr ""
msgstr "Συνταγές"
#: .\cookbook\templates\base.html:111
msgid "Shopping"
msgstr ""
msgstr "Αγορές"
#: .\cookbook\templates\base.html:150 .\cookbook\views\lists.py:105
msgid "Foods"
msgstr ""
msgstr "Φαγητά"
#: .\cookbook\templates\base.html:162
#: .\cookbook\templates\forms\ingredients.html:24
#: .\cookbook\templates\stats.html:26 .\cookbook\views\lists.py:122
msgid "Units"
msgstr ""
msgstr "Μονάδες μέτρησης"
#: .\cookbook\templates\base.html:176 .\cookbook\templates\supermarket.html:7
msgid "Supermarket"
msgstr ""
msgstr "Supermarket"
#: .\cookbook\templates\base.html:188
msgid "Supermarket Category"
msgstr ""
msgstr "Κατηγορία Supermarket"
#: .\cookbook\templates\base.html:200 .\cookbook\views\lists.py:171
msgid "Automations"
msgstr ""
msgstr "Αυτοματισμοί"
#: .\cookbook\templates\base.html:214 .\cookbook\views\lists.py:207
msgid "Files"
msgstr ""
msgstr "Αρχεία"
#: .\cookbook\templates\base.html:226
msgid "Batch Edit"
msgstr ""
msgstr "Μαζική Επεξεργασία"
#: .\cookbook\templates\base.html:238 .\cookbook\templates\history.html:6
#: .\cookbook\templates\history.html:14
msgid "History"
msgstr ""
msgstr "Ιστορικό"
#: .\cookbook\templates\base.html:252
#: .\cookbook\templates\ingredient_editor.html:7
#: .\cookbook\templates\ingredient_editor.html:13
msgid "Ingredient Editor"
msgstr ""
msgstr "Επεξεργαστής Συστατικών"
#: .\cookbook\templates\base.html:264
#: .\cookbook\templates\export_response.html:7
#: .\cookbook\templates\test2.html:14 .\cookbook\templates\test2.html:20
msgid "Export"
msgstr ""
msgstr "Εξαγωγή"
#: .\cookbook\templates\base.html:280 .\cookbook\templates\index.html:47
msgid "Import Recipe"
msgstr ""
msgstr "Εισαγωγή συνταγής"
#: .\cookbook\templates\base.html:282
msgid "Create"
msgstr ""
msgstr "Δημιουργία"
#: .\cookbook\templates\base.html:295
#: .\cookbook\templates\generic\list_template.html:14
#: .\cookbook\templates\stats.html:43
msgid "External Recipes"
msgstr ""
msgstr "Εξωτερικές Συνταγές"
#: .\cookbook\templates\base.html:298
#: .\cookbook\templates\space_manage.html:15
msgid "Space Settings"
msgstr ""
msgstr "Ρυθμίσεις χώρου"
#: .\cookbook\templates\base.html:303 .\cookbook\templates\system.html:13
msgid "System"
msgstr ""
msgstr "Σύστημα"
#: .\cookbook\templates\base.html:305
msgid "Admin"
msgstr ""
msgstr "Διαχειριστής"
#: .\cookbook\templates\base.html:309
#: .\cookbook\templates\space_overview.html:25
msgid "Your Spaces"
msgstr ""
msgstr "Οι χώροι σας"
#: .\cookbook\templates\base.html:320
#: .\cookbook\templates\space_overview.html:6
msgid "Overview"
msgstr ""
msgstr "Σύνοψη"
#: .\cookbook\templates\base.html:324
msgid "Markdown Guide"
msgstr ""
msgstr "Οδηγός χρήσης του Markdown"
#: .\cookbook\templates\base.html:326
msgid "GitHub"
@@ -1205,53 +1240,57 @@ msgstr "GitHub"
#: .\cookbook\templates\base.html:328
msgid "Translate Tandoor"
msgstr ""
msgstr "Μεταφράστε το Tandoor"
#: .\cookbook\templates\base.html:332
msgid "API Browser"
msgstr ""
msgstr "Περιηγητής API"
#: .\cookbook\templates\base.html:335
msgid "Log out"
msgstr ""
msgstr "Αποσύνδεση"
#: .\cookbook\templates\base.html:357
msgid "You are using the free version of Tandor"
msgstr ""
msgstr "Χρησιμοποιείται την δωρεάν έκδοση του Tandoor"
#: .\cookbook\templates\base.html:358
msgid "Upgrade Now"
msgstr ""
msgstr "Αναβαθμιστείτε τώρα"
#: .\cookbook\templates\batch\edit.html:6
msgid "Batch edit Category"
msgstr ""
msgstr "Μαζική τροποποίηση κατηγοριών"
#: .\cookbook\templates\batch\edit.html:15
msgid "Batch edit Recipes"
msgstr ""
msgstr "Μαζική τροποποίηση Συνταγών"
#: .\cookbook\templates\batch\edit.html:20
msgid "Add the specified keywords to all recipes containing a word"
msgstr ""
"Προσθέστε τις καθορισμένες λέξεις-κλειδιά σε όλες τις συνταγές που περιέχουν "
"μια λέξη"
#: .\cookbook\templates\batch\monitor.html:6 .\cookbook\views\edit.py:73
msgid "Sync"
msgstr ""
msgstr "Συγχρονισμός"
#: .\cookbook\templates\batch\monitor.html:10
msgid "Manage watched Folders"
msgstr ""
msgstr "Διαχείριση φακέλων που έχουν προβληθεί"
#: .\cookbook\templates\batch\monitor.html:14
msgid ""
"On this Page you can manage all storage folder locations that should be "
"monitored and synced."
msgstr ""
"Σε αυτήν τη σελίδα μπορείτε να διαχειριστείτε όλες τις τοποθεσίες "
"αποθήκευσης φακέλων που πρέπει να παρακολουθούνται και να συγχρονίζονται."
#: .\cookbook\templates\batch\monitor.html:16
msgid "The path must be in the following format"
msgstr ""
msgstr "Η διαδρομή (path) πρέπει να είναι στην ακόλουθη μορφή"
#: .\cookbook\templates\batch\monitor.html:20
#: .\cookbook\templates\forms\edit_import_recipe.html:14
@@ -1263,55 +1302,57 @@ msgstr ""
#: .\cookbook\templates\settings.html:202
#: .\cookbook\templates\settings.html:213
msgid "Save"
msgstr ""
msgstr "Αποθήκευση"
#: .\cookbook\templates\batch\monitor.html:21
msgid "Manage External Storage"
msgstr ""
msgstr "Διαχείριση εξωτερικού χώρου αποθήκευσης"
#: .\cookbook\templates\batch\monitor.html:28
msgid "Sync Now!"
msgstr ""
msgstr "Συγχρονισμός τώρα!"
#: .\cookbook\templates\batch\monitor.html:29
msgid "Show Recipes"
msgstr ""
msgstr "Προβολή Συνταγών"
#: .\cookbook\templates\batch\monitor.html:30
msgid "Show Log"
msgstr ""
msgstr "Προβολή αρχείων καταγραφής"
#: .\cookbook\templates\batch\waiting.html:4
#: .\cookbook\templates\batch\waiting.html:10
msgid "Importing Recipes"
msgstr ""
msgstr "Οι συνταγές εισάγονται"
#: .\cookbook\templates\batch\waiting.html:28
msgid ""
"This can take a few minutes, depending on the number of recipes in sync, "
"please wait."
msgstr ""
"Αυτή η διαδικασία μπορεί να πάρει μερικά λεπτά, ανάλογα με τον αριθμό των "
"συνταγών που πρέπει να συγχρονιστούν, παρακαλώ περιμένετε."
#: .\cookbook\templates\books.html:7
msgid "Recipe Books"
msgstr ""
msgstr "Βιβλία Συνταγών"
#: .\cookbook\templates\export.html:8 .\cookbook\templates\test2.html:6
msgid "Export Recipes"
msgstr ""
msgstr "Εξαγωγή Συνταγών"
#: .\cookbook\templates\forms\edit_import_recipe.html:5
#: .\cookbook\templates\forms\edit_import_recipe.html:9
msgid "Import new Recipe"
msgstr ""
msgstr "Εισαγωγή μια νέας συνταγή"
#: .\cookbook\templates\forms\edit_internal_recipe.html:7
msgid "Edit Recipe"
msgstr ""
msgstr "Τροποποίηση συνταγής"
#: .\cookbook\templates\forms\ingredients.html:15
msgid "Edit Ingredients"
msgstr ""
msgstr "Τροποποίηση υλικών"
#: .\cookbook\templates\forms\ingredients.html:16
msgid ""
@@ -1323,32 +1364,41 @@ msgid ""
"them.\n"
" "
msgstr ""
"\n"
" Την παρακάτω φόρμα μπορεί να χρησιμοποιηθεί στην περίπτωση που, κατά "
"λάθος, δημιουργήθηκαν δύο (ή περισσότερες) μονάδες μέτρησης ή συστατικά που "
"θα έπρεπε να είναι\n"
" τα ίδια.\n"
" Αυτή η φόρμα συγχωνεύει δύο μονάδες ή συστατικά και ενημερώνει όλες "
"τις συνταγές που τα χρησιμοποιούν.\n"
" "
#: .\cookbook\templates\forms\ingredients.html:26
msgid "Are you sure that you want to merge these two units?"
msgstr ""
msgstr "Είστε βέβαιος ότι θέλετε να συγχωνεύσετε αυτές τις δύο μονάδες;"
#: .\cookbook\templates\forms\ingredients.html:31
#: .\cookbook\templates\forms\ingredients.html:40
msgid "Merge"
msgstr ""
msgstr "Συγχώνευση"
#: .\cookbook\templates\forms\ingredients.html:36
msgid "Are you sure that you want to merge these two ingredients?"
msgstr ""
msgstr "Είστε βέβαιος ότι θέλετε να συγχωνεύσετε αυτά τα δύο υλικά;"
#: .\cookbook\templates\generic\delete_template.html:21
#, python-format
msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
msgstr ""
"Είστε σίγουροι ότι θέλετε να διαγράψετε τα %(title)s: <b>%(object)s</b> "
#: .\cookbook\templates\generic\delete_template.html:22
msgid "This cannot be undone!"
msgstr ""
msgstr "Αυτό δεν μπορεί να αναιρεθεί!"
#: .\cookbook\templates\generic\delete_template.html:27
msgid "Protected"
msgstr ""
msgstr "Προστατευμένο"
#: .\cookbook\templates\generic\delete_template.html:42
msgid "Cascade"
@@ -1356,68 +1406,68 @@ msgstr ""
#: .\cookbook\templates\generic\delete_template.html:73
msgid "Cancel"
msgstr ""
msgstr "Ακύρωση"
#: .\cookbook\templates\generic\edit_template.html:32
msgid "View"
msgstr ""
msgstr "Προβολή"
#: .\cookbook\templates\generic\edit_template.html:36
msgid "Delete original file"
msgstr ""
msgstr "Διαγραφή πρωτότυπου αρχείου"
#: .\cookbook\templates\generic\list_template.html:6
#: .\cookbook\templates\generic\list_template.html:22
msgid "List"
msgstr ""
msgstr "Λίστα"
#: .\cookbook\templates\generic\list_template.html:36
msgid "Filter"
msgstr ""
msgstr "Φίλτρο"
#: .\cookbook\templates\generic\list_template.html:41
msgid "Import all"
msgstr ""
msgstr "Εισαγωγή όλων"
#: .\cookbook\templates\generic\table_template.html:76
#: .\cookbook\templates\recipes_table.html:121
msgid "previous"
msgstr ""
msgstr "προηγούμενο"
#: .\cookbook\templates\generic\table_template.html:98
#: .\cookbook\templates\recipes_table.html:143
msgid "next"
msgstr ""
msgstr "επόμενο"
#: .\cookbook\templates\history.html:20
msgid "View Log"
msgstr ""
msgstr "Προβολή αρχείων καταγραφής"
#: .\cookbook\templates\history.html:24
msgid "Cook Log"
msgstr ""
msgstr "Αρχείο καταγραφής μαγειρέματος"
#: .\cookbook\templates\import.html:6
msgid "Import Recipes"
msgstr ""
msgstr "Εισαγωγή Συνταγών"
#: .\cookbook\templates\import.html:14 .\cookbook\templates\import.html:20
#: .\cookbook\templates\import_response.html:7 .\cookbook\views\delete.py:86
#: .\cookbook\views\edit.py:191
msgid "Import"
msgstr ""
msgstr "Εισαγωγή"
#: .\cookbook\templates\include\recipe_open_modal.html:18
msgid "Close"
msgstr ""
msgstr "Κλείσιμο"
#: .\cookbook\templates\include\recipe_open_modal.html:32
msgid "Open Recipe"
msgstr ""
msgstr "Άνοιγμα Συνταγής"
#: .\cookbook\templates\include\storage_backend_warning.html:4
msgid "Security Warning"
msgstr ""
msgstr "Προειδοποίηση ασφαλείας"
#: .\cookbook\templates\include\storage_backend_warning.html:5
msgid ""
@@ -1434,32 +1484,32 @@ msgstr ""
#: .\cookbook\templates\index.html:29
msgid "Search recipe ..."
msgstr ""
msgstr "Αναζήτηση συνταγής ..."
#: .\cookbook\templates\index.html:44
msgid "New Recipe"
msgstr ""
msgstr "Νέα συνταγή"
#: .\cookbook\templates\index.html:53
msgid "Advanced Search"
msgstr ""
msgstr "Αναζήτηση για προχωρημένους"
#: .\cookbook\templates\index.html:57
msgid "Reset Search"
msgstr ""
msgstr "Επαναφορά αναζήτησης"
#: .\cookbook\templates\index.html:85
msgid "Last viewed"
msgstr ""
msgstr "Τελευταίες προβολές"
#: .\cookbook\templates\index.html:94
msgid "Log in to view recipes"
msgstr ""
msgstr "Συνδεθείτε για να δείτε τις συνταγές"
#: .\cookbook\templates\markdown_info.html:5
#: .\cookbook\templates\markdown_info.html:13
msgid "Markdown Info"
msgstr ""
msgstr "Πληροφορίες για το Markdown"
#: .\cookbook\templates\markdown_info.html:14
msgid ""
@@ -1479,31 +1529,33 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:25
msgid "Headers"
msgstr ""
msgstr "Επικεφαλίδες"
#: .\cookbook\templates\markdown_info.html:54
msgid "Formatting"
msgstr ""
msgstr "Μορφοποίηση"
#: .\cookbook\templates\markdown_info.html:56
#: .\cookbook\templates\markdown_info.html:72
msgid "Line breaks are inserted by adding two spaces after the end of a line"
msgstr ""
"Οι αλλαγές γραμμής εισάγονται προσθέτοντας δύο κενά μετά το τέλος μιας "
"γραμμής"
#: .\cookbook\templates\markdown_info.html:57
#: .\cookbook\templates\markdown_info.html:73
msgid "or by leaving a blank line in between."
msgstr ""
msgstr "ή αφήνοντας μια κενή γραμμή μεταξύ τους."
#: .\cookbook\templates\markdown_info.html:59
#: .\cookbook\templates\markdown_info.html:74
msgid "This text is bold"
msgstr ""
msgstr "Το κείμενο είναι έντονο (bold)"
#: .\cookbook\templates\markdown_info.html:60
#: .\cookbook\templates\markdown_info.html:75
msgid "This text is italic"
msgstr ""
msgstr "Αυτό το κείμενο είναι πλάγιο (italic)"
#: .\cookbook\templates\markdown_info.html:61
#: .\cookbook\templates\markdown_info.html:77
@@ -1512,7 +1564,7 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:84
msgid "Lists"
msgstr ""
msgstr "Λίστες"
#: .\cookbook\templates\markdown_info.html:85
msgid ""
@@ -1550,7 +1602,7 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:125
msgid "Images & Links"
msgstr ""
msgstr "Φωτογραφίες και σύνδεσμοι"
#: .\cookbook\templates\markdown_info.html:126
msgid ""

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-09-25 09:59+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-10-12 20:19+0000\n"
"Last-Translator: pharok <pharok@free.fr>\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"
@@ -310,7 +310,7 @@ msgid ""
msgstr ""
"Champs à rechercher en ignorant les accents. La sélection de cette option "
"peut améliorer ou dégrader la qualité de la recherche en fonction de la "
"langue."
"langue"
#: .\cookbook\forms.py:466
msgid ""
@@ -326,8 +326,8 @@ msgid ""
"will return 'salad' and 'sandwich')"
msgstr ""
"Champs permettant de rechercher les correspondances de début de mot (par "
"exemple, si vous recherchez « sa », vous obtiendrez « salade » et "
"« sandwich»)."
"exemple, si vous recherchez « sa », vous obtiendrez « salade » et « "
"sandwich»)"
#: .\cookbook\forms.py:470
msgid ""
@@ -546,10 +546,8 @@ msgid "One of queryset or hash_key must be provided"
msgstr "Il est nécessaire de fournir soit le queryset, soit la clé de hachage"
#: .\cookbook\helper\recipe_url_import.py:266
#, fuzzy
#| msgid "Use fractions"
msgid "reverse rotation"
msgstr "Utiliser les fractions"
msgstr "sens inverse"
#: .\cookbook\helper\recipe_url_import.py:267
msgid "careful rotation"
@@ -557,27 +555,27 @@ msgstr ""
#: .\cookbook\helper\recipe_url_import.py:268
msgid "knead"
msgstr ""
msgstr "pétrir"
#: .\cookbook\helper\recipe_url_import.py:269
msgid "thicken"
msgstr ""
msgstr "épaissir"
#: .\cookbook\helper\recipe_url_import.py:270
msgid "warm up"
msgstr ""
msgstr "réchauffer"
#: .\cookbook\helper\recipe_url_import.py:271
msgid "ferment"
msgstr ""
msgstr "fermenter"
#: .\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"
msgstr "Vous devez fournir une information de portion"
msgstr "Vous devez fournir un nombre de portions"
#: .\cookbook\helper\template_helper.py:79
#: .\cookbook\helper\template_helper.py:81
@@ -590,7 +588,6 @@ msgid "Favorite"
msgstr "Favori"
#: .\cookbook\integration\copymethat.py:50
#, fuzzy
msgid "I made this"
msgstr "J'ai fait ça"
@@ -620,10 +617,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"
@@ -648,7 +643,7 @@ msgstr "Portions"
#: .\cookbook\integration\saffron.py:25
msgid "Waiting time"
msgstr "temps dattente"
msgstr "Temps dattente"
#: .\cookbook\integration\saffron.py:27
msgid "Preparation Time"
@@ -851,7 +846,6 @@ msgid "ID of unit to use for the shopping list"
msgstr "ID de lunité à utiliser pour la liste de courses"
#: .\cookbook\serializer.py:1259
#, fuzzy
msgid "When set to true will delete all food from active shopping lists."
msgstr ""
"Lorsqu'il est défini sur \"true\", tous les aliments des listes de courses "
@@ -967,8 +961,9 @@ msgid ""
" ."
msgstr ""
"Confirmez SVP que\n"
" <a href=\"mailto:%(email)s\"></a> est une adresse mail de "
"lutilisateur %(user_display)s."
" <a href=\"mailto:%(email)s\"></a> est une adresse mail de l"
"utilisateur %(user_display)s\n"
" ."
#: .\cookbook\templates\account\email_confirm.html:22
#: .\cookbook\templates\generic\delete_template.html:72
@@ -1371,9 +1366,8 @@ msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
msgstr "Êtes-vous sûr(e) de vouloir supprimer %(title)s : <b>%(object)s</b> "
#: .\cookbook\templates\generic\delete_template.html:22
#, fuzzy
msgid "This cannot be undone!"
msgstr "Cela ne peut pas être annulé !"
msgstr "L'opération ne peut pas être annulée !"
#: .\cookbook\templates\generic\delete_template.html:27
msgid "Protected"
@@ -1456,12 +1450,12 @@ msgid ""
" "
msgstr ""
"\n"
" Les champs <b>Mot de passe et Token</b> sont stockés <b>en texte "
"brut</b>dans la base de données.\n"
" Les champs <b>Mot de passe et Token</b> sont stockés <b>en clair</"
"b>dans la base de données.\n"
" C'est nécessaire car ils sont utilisés pour faire des requêtes API, "
"mais cela accroît le risque que quelqu'un les vole.<br/>\n"
" Pour limiter les risques, des tokens ou comptes avec un accès limité "
"devraient être utilisés.\n"
" Pour limiter les risques, il est possible d'utiliser des tokens ou "
"des comptes avec un accès limité.\n"
" "
#: .\cookbook\templates\index.html:29
@@ -1771,15 +1765,6 @@ msgstr ""
" "
#: .\cookbook\templates\search_info.html:29
#, fuzzy
#| msgid ""
#| " \n"
#| " Simple searches ignore punctuation and common words such as "
#| "'the', 'a', 'and'. And will treat seperate words as required.\n"
#| " Searching for 'apple or flour' will return any recipe that "
#| "includes both 'apple' and 'flour' anywhere in the fields that have been "
#| "selected for a full text search.\n"
#| " "
msgid ""
" \n"
" Simple searches ignore punctuation and common words such as "
@@ -1791,7 +1776,7 @@ msgid ""
msgstr ""
" \n"
" Les recherches simples ignorent la ponctuation et les mots "
"courants tels que \"le\", \"a\", \"et\", et traiteront les mots séparés "
"courants tels que \"le\", \"et\", \"a\", et traiteront les mots séparés "
"comme il se doit.\n"
" Si vous recherchez \"pomme ou farine\", vous obtiendrez toutes "
"les recettes qui contiennent à la fois \"pomme\" et \"farine\" dans les "
@@ -2219,7 +2204,7 @@ msgstr "Gérer labonnement"
#: .\cookbook\templates\space_overview.html:13 .\cookbook\views\delete.py:216
msgid "Space"
msgstr "Groupe :"
msgstr "Groupe"
#: .\cookbook\templates\space_overview.html:17
msgid ""
@@ -2659,7 +2644,7 @@ msgstr ""
#: .\cookbook\views\api.py:1394
msgid "Sync successful!"
msgstr "Synchro réussie !"
msgstr "Synchronisation réussie !"
#: .\cookbook\views\api.py:1399
msgid "Error synchronizing with Storage"
@@ -2732,6 +2717,8 @@ msgid ""
"The PDF Exporter is not enabled on this instance as it is still in an "
"experimental state."
msgstr ""
"L'export PDF n'est pas activé sur cette instance car il est toujours au "
"statut expérimental."
#: .\cookbook\views\lists.py:24
msgid "Import Log"

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,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-10-20 14:05+0000\n"
"Last-Translator: Ferenc <ugyes@freemail.hu>\n"
"Language-Team: Hungarian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/hu/>\n"
"Language: hu_HU\n"
@@ -99,7 +99,7 @@ msgstr ""
#: .\cookbook\forms.py:74
msgid "Users with whom newly created meal plans should be shared by default."
msgstr ""
"Azok a felhasználók, akikkel az újonnan létrehozott étkezési terveket "
"Azok a felhasználók, akikkel az újonnan létrehozott menüterveket "
"alapértelmezés szerint meg kell osztani."
#: .\cookbook\forms.py:75
@@ -135,8 +135,7 @@ msgstr "A navigációs sávot az oldal tetejére rögzíti."
#: .\cookbook\forms.py:83 .\cookbook\forms.py:512
msgid "Automatically add meal plan ingredients to shopping list."
msgstr ""
"Automatikusan hozzáadja az étkezési terv hozzávalóit a bevásárlólistához."
msgstr "Automatikusan hozzáadja a menüterv hozzávalóit a bevásárlólistához."
#: .\cookbook\forms.py:84
msgid "Exclude ingredients that are on hand."
@@ -360,10 +359,8 @@ msgid "Partial Match"
msgstr "Részleges találat"
#: .\cookbook\forms.py:480
#, fuzzy
#| msgid "Starts Wtih"
msgid "Starts With"
msgstr "Kezdődik a következővel"
msgstr "Ezzel kezdődik"
#: .\cookbook\forms.py:481
msgid "Fuzzy Search"
@@ -387,16 +384,16 @@ msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"include all related recipes."
msgstr ""
"Amikor étkezési tervet ad hozzá a bevásárlólistához (kézzel vagy "
"automatikusan), vegye fel az összes kapcsolódó receptet."
"Amikor menütervet ad hozzá a bevásárlólistához (kézzel vagy automatikusan), "
"vegye fel az összes kapcsolódó receptet."
#: .\cookbook\forms.py:514
msgid ""
"When adding a meal plan to the shopping list (manually or automatically), "
"exclude ingredients that are on hand."
msgstr ""
"Amikor étkezési tervet ad hozzá a bevásárlólistához (kézzel vagy "
"automatikusan), zárja ki a kéznél lévő összetevőket."
"Amikor menütervet ad hozzá a bevásárlólistához (kézzel vagy automatikusan), "
"zárja ki a kéznél lévő összetevőket."
#: .\cookbook\forms.py:515
msgid "Default number of hours to delay a shopping list entry."
@@ -436,7 +433,7 @@ msgstr "Automatikus szinkronizálás"
#: .\cookbook\forms.py:526
msgid "Auto Add Meal Plan"
msgstr "Automatikus étkezési terv hozzáadása"
msgstr "Menüterv automatikus hozzáadása"
#: .\cookbook\forms.py:527
msgid "Exclude On Hand"
@@ -490,6 +487,7 @@ msgstr "A receptek számának megjelenítése a keresési szűrőkön"
#: .\cookbook\forms.py:559
msgid "Use the plural form for units and food inside this space."
msgstr ""
"Használja a többes számot az egységek és az ételek esetében ezen a helyen."
#: .\cookbook\helper\AllAuthCustomAdapter.py:39
msgid ""
@@ -549,29 +547,27 @@ msgstr ""
#: .\cookbook\helper\recipe_url_import.py:268
msgid "knead"
msgstr ""
msgstr "dagasztás"
#: .\cookbook\helper\recipe_url_import.py:269
msgid "thicken"
msgstr ""
msgstr "sűrítés"
#: .\cookbook\helper\recipe_url_import.py:270
msgid "warm up"
msgstr ""
msgstr "melegítés"
#: .\cookbook\helper\recipe_url_import.py:271
msgid "ferment"
msgstr ""
msgstr "fermentálás"
#: .\cookbook\helper\recipe_url_import.py:272
msgid "sous-vide"
msgstr ""
msgstr "sous-vide"
#: .\cookbook\helper\shopping_helper.py:157
#, fuzzy
#| msgid "You must supply a created_by"
msgid "You must supply a servings size"
msgstr "Meg kell adnia egy created_by"
msgstr "Meg kell adnia az adagok nagyságát"
#: .\cookbook\helper\template_helper.py:79
#: .\cookbook\helper\template_helper.py:81
@@ -581,11 +577,11 @@ msgstr "Nem sikerült elemezni a sablon kódját."
#: .\cookbook\integration\copymethat.py:44
#: .\cookbook\integration\melarecipes.py:37
msgid "Favorite"
msgstr ""
msgstr "Kedvenc"
#: .\cookbook\integration\copymethat.py:50
msgid "I made this"
msgstr ""
msgstr "Elkészítettem"
#: .\cookbook\integration\integration.py:218
msgid ""
@@ -613,10 +609,8 @@ msgid "Imported %s recipes."
msgstr "Importálva %s recept."
#: .\cookbook\integration\openeats.py:26
#, fuzzy
#| msgid "Recipe Home"
msgid "Recipe source:"
msgstr "Recipe Home"
msgstr "Recept forrása:"
#: .\cookbook\integration\paprika.py:49
msgid "Notes"
@@ -632,10 +626,8 @@ msgstr "Forrás"
#: .\cookbook\integration\recettetek.py:54
#: .\cookbook\integration\recipekeeper.py:70
#, fuzzy
#| msgid "Import Log"
msgid "Imported from"
msgstr "Import napló"
msgstr "Importálva a"
#: .\cookbook\integration\saffron.py:23
msgid "Servings"
@@ -662,12 +654,10 @@ msgid "Rebuilds full text search index on Recipe"
msgstr "Újraépíti a teljes szöveges keresési indexet a Recept oldalon"
#: .\cookbook\management\commands\rebuildindex.py:18
#, fuzzy
#| msgid "Only Postgress databases use full text search, no index to rebuild"
msgid "Only Postgresql databases use full text search, no index to rebuild"
msgstr ""
"Csak a Postgress adatbázisok használnak teljes szöveges keresést, nincs "
"újjáépítendő index"
"Csak a Postgresql adatbázisok használják a teljes szöveges keresést, nem "
"kell indexet újjáépíteni"
#: .\cookbook\management\commands\rebuildindex.py:29
msgid "Recipe index rebuild complete."
@@ -711,7 +701,7 @@ msgstr "Keresés"
#: .\cookbook\templates\meal_plan.html:7 .\cookbook\views\delete.py:178
#: .\cookbook\views\edit.py:211 .\cookbook\views\new.py:179
msgid "Meal-Plan"
msgstr "Étkezési terv"
msgstr "Menüterv"
#: .\cookbook\models.py:367 .\cookbook\templates\base.html:118
msgid "Books"
@@ -750,16 +740,12 @@ msgid "Keyword Alias"
msgstr "Kulcsszó álneve"
#: .\cookbook\models.py:1232
#, fuzzy
#| msgid "Description"
msgid "Description Replace"
msgstr "Leírás"
msgstr "Leírás csere"
#: .\cookbook\models.py:1232
#, fuzzy
#| msgid "Instructions"
msgid "Instruction Replace"
msgstr "Elkészítés"
msgstr "Leírás cseréje"
#: .\cookbook\models.py:1258 .\cookbook\views\delete.py:36
#: .\cookbook\views\edit.py:251 .\cookbook\views\new.py:48
@@ -767,10 +753,8 @@ msgid "Recipe"
msgstr "Recept"
#: .\cookbook\models.py:1259
#, fuzzy
#| msgid "Foods"
msgid "Food"
msgstr "Ételek"
msgstr "Étel"
#: .\cookbook\models.py:1260 .\cookbook\templates\base.html:141
msgid "Keyword"
@@ -1176,7 +1160,7 @@ msgstr "Ételek"
#: .\cookbook\templates\base.html:165 .\cookbook\views\lists.py:122
msgid "Units"
msgstr "Egységek"
msgstr "Mértékegységek"
#: .\cookbook\templates\base.html:179 .\cookbook\templates\supermarket.html:7
msgid "Supermarket"
@@ -1206,10 +1190,8 @@ msgstr "Előzmények"
#: .\cookbook\templates\base.html:255
#: .\cookbook\templates\ingredient_editor.html:7
#: .\cookbook\templates\ingredient_editor.html:13
#, fuzzy
#| msgid "Ingredients"
msgid "Ingredient Editor"
msgstr "Hozzávalók"
msgstr "Hozzávaló szerkesztő"
#: .\cookbook\templates\base.html:267
#: .\cookbook\templates\export_response.html:7
@@ -1252,7 +1234,7 @@ msgstr "Nincs hely"
#: .\cookbook\templates\base.html:323
#: .\cookbook\templates\space_overview.html:6
msgid "Overview"
msgstr ""
msgstr "Áttekintés"
#: .\cookbook\templates\base.html:327
msgid "Markdown Guide"
@@ -1276,11 +1258,11 @@ msgstr "Kijelentkezés"
#: .\cookbook\templates\base.html:360
msgid "You are using the free version of Tandor"
msgstr ""
msgstr "Ön a Tandoor ingyenes verzióját használja"
#: .\cookbook\templates\base.html:361
msgid "Upgrade Now"
msgstr ""
msgstr "Frissítés most"
#: .\cookbook\templates\batch\edit.html:6
msgid "Batch edit Category"
@@ -1377,7 +1359,7 @@ msgstr "Biztos, hogy törölni akarod a %(title)s: <b>%(object)s</b> "
#: .\cookbook\templates\generic\delete_template.html:22
msgid "This cannot be undone!"
msgstr ""
msgstr "Ezt nem lehet visszafordítani!"
#: .\cookbook\templates\generic\delete_template.html:27
msgid "Protected"
@@ -1541,8 +1523,6 @@ msgstr "A sortörés a sor vége után két szóköz hozzáadásával történik
#: .\cookbook\templates\markdown_info.html:57
#: .\cookbook\templates\markdown_info.html:73
#, fuzzy
#| msgid "or by leaving a blank line inbetween."
msgid "or by leaving a blank line in between."
msgstr "vagy egy üres sort hagyva közöttük."
@@ -1566,10 +1546,6 @@ msgid "Lists"
msgstr "Listák"
#: .\cookbook\templates\markdown_info.html:85
#, fuzzy
#| msgid ""
#| "Lists can ordered or unorderd. It is <b>important to leave a blank line "
#| "before the list!</b>"
msgid ""
"Lists can ordered or unordered. It is <b>important to leave a blank line "
"before the list!</b>"
@@ -1701,11 +1677,11 @@ msgstr ""
#: .\cookbook\templates\openid\login.html:27
#: .\cookbook\templates\socialaccount\authentication_error.html:27
msgid "Back"
msgstr ""
msgstr "Vissza"
#: .\cookbook\templates\profile.html:7
msgid "Profile"
msgstr ""
msgstr "Profil"
#: .\cookbook\templates\recipe_view.html:41
msgid "by"
@@ -1718,7 +1694,7 @@ msgstr "Megjegyzés"
#: .\cookbook\templates\rest_framework\api.html:5
msgid "Recipe Home"
msgstr "Recipe Home"
msgstr "Recept főoldal"
#: .\cookbook\templates\search_info.html:5
#: .\cookbook\templates\search_info.html:9
@@ -2104,17 +2080,15 @@ msgstr "Szuperfelhasználói fiók létrehozása"
#: .\cookbook\templates\socialaccount\authentication_error.html:7
#: .\cookbook\templates\socialaccount\authentication_error.html:23
#, fuzzy
#| msgid "Social Login"
msgid "Social Network Login Failure"
msgstr "Közösségi bejelentkezés"
msgstr "Közösségi hálózat bejelentkezési hiba"
#: .\cookbook\templates\socialaccount\authentication_error.html:25
#, fuzzy
#| msgid "An error occurred attempting to move "
msgid ""
"An error occurred while attempting to login via your social network account."
msgstr "Hiba történt az áthelyezés közben "
msgstr ""
"Hiba történt, miközben megpróbált bejelentkezni a közösségi hálózati fiókján "
"keresztül."
#: .\cookbook\templates\socialaccount\connections.html:4
#: .\cookbook\templates\socialaccount\connections.html:15
@@ -2152,7 +2126,7 @@ msgstr "Regisztráció"
#: .\cookbook\templates\socialaccount\login.html:9
#, python-format
msgid "Connect %(provider)s"
msgstr ""
msgstr "Csatlakozás %(provider)s"
#: .\cookbook\templates\socialaccount\login.html:11
#, python-format
@@ -2162,7 +2136,7 @@ msgstr ""
#: .\cookbook\templates\socialaccount\login.html:13
#, python-format
msgid "Sign In Via %(provider)s"
msgstr ""
msgstr "Bejelentkezve %(provider)s keresztül"
#: .\cookbook\templates\socialaccount\login.html:15
#, python-format
@@ -2171,7 +2145,7 @@ msgstr ""
#: .\cookbook\templates\socialaccount\login.html:20
msgid "Continue"
msgstr ""
msgstr "Folytatás"
#: .\cookbook\templates\socialaccount\signup.html:10
#, python-format
@@ -2210,10 +2184,8 @@ msgid "Manage Subscription"
msgstr "Feliratkozás kezelése"
#: .\cookbook\templates\space_overview.html:13 .\cookbook\views\delete.py:216
#, fuzzy
#| msgid "Space:"
msgid "Space"
msgstr "Tér:"
msgstr "Tér"
#: .\cookbook\templates\space_overview.html:17
msgid ""
@@ -2230,13 +2202,11 @@ msgstr "Meghívást kaphatsz egy meglévő térbe, vagy létrehozhatod a sajáto
#: .\cookbook\templates\space_overview.html:53
msgid "Owner"
msgstr ""
msgstr "Tulajdonos"
#: .\cookbook\templates\space_overview.html:57
#, fuzzy
#| msgid "Create Space"
msgid "Leave Space"
msgstr "Tér létrehozása"
msgstr "Kilépés a Térből"
#: .\cookbook\templates\space_overview.html:78
#: .\cookbook\templates\space_overview.html:88
@@ -2485,87 +2455,111 @@ msgstr ""
"teljes szöveges keresés is."
#: .\cookbook\views\api.py:733
#, fuzzy
#| msgid "ID of keyword a recipe should have. For multiple repeat parameter."
msgid ""
"ID of keyword a recipe should have. For multiple repeat parameter. "
"Equivalent to keywords_or"
msgstr ""
"A recept kulcsszavának azonosítója. Többszörös ismétlődő paraméter esetén."
"A recept kulcsszavának azonosítója. Többszörös ismétlődő paraméter esetén. "
"Egyenértékű a keywords_or kulcsszavakkal"
#: .\cookbook\views\api.py:736
msgid ""
"Keyword IDs, repeat for multiple. Return recipes with any of the keywords"
msgstr ""
"Kulcsszó azonosítók. Többször is megadható. A megadott kulcsszavak "
"mindegyikéhez tartozó receptek listázza"
#: .\cookbook\views\api.py:739
msgid ""
"Keyword IDs, repeat for multiple. Return recipes with all of the keywords."
msgstr ""
"Kulcsszó azonosítók. Többször is megadható. Az összes megadott kulcsszót "
"tartalmazó receptek listázása."
#: .\cookbook\views\api.py:742
msgid ""
"Keyword IDs, repeat for multiple. Exclude recipes with any of the keywords."
msgstr ""
"Kulcsszó azonosító. Többször is megadható. Kizárja a recepteket a megadott "
"kulcsszavak egyikéből."
#: .\cookbook\views\api.py:745
msgid ""
"Keyword IDs, repeat for multiple. Exclude recipes with all of the keywords."
msgstr ""
"Kulcsszó azonosítók. Többször is megadható. Kizárja az összes megadott "
"kulcsszóval rendelkező receptet."
#: .\cookbook\views\api.py:747
msgid "ID of food a recipe should have. For multiple repeat parameter."
msgstr ""
"Az ételek azonosítója egy receptnek tartalmaznia kell. Többszörös ismétlődő "
"paraméter esetén."
"Annak az összetevőnek az azonosítója, amelynek receptjeit fel kell sorolni. "
"Többször is megadható."
#: .\cookbook\views\api.py:750
msgid "Food IDs, repeat for multiple. Return recipes with any of the foods"
msgstr ""
"Összetevő azonosító. Többször is megadható. Legalább egy összetevő "
"receptjeinek listája"
#: .\cookbook\views\api.py:752
msgid "Food IDs, repeat for multiple. Return recipes with all of the foods."
msgstr ""
"Összetevő azonosító. Többször is megadható. Az összes megadott összetevőt "
"tartalmazó receptek listája."
#: .\cookbook\views\api.py:754
msgid "Food IDs, repeat for multiple. Exclude recipes with any of the foods."
msgstr ""
"Összetevő azonosító. Többször is megadható. Kizárja azokat a recepteket, "
"amelyek a megadott összetevők bármelyikét tartalmazzák."
#: .\cookbook\views\api.py:756
msgid "Food IDs, repeat for multiple. Exclude recipes with all of the foods."
msgstr ""
"Összetevő azonosító. Többször is megadható. Kizárja az összes megadott "
"összetevőt tartalmazó recepteket."
#: .\cookbook\views\api.py:757
msgid "ID of unit a recipe should have."
msgstr "Az egység azonosítója, amellyel a receptnek rendelkeznie kell."
msgstr "A recepthez tartozó mértékegység azonosítója."
#: .\cookbook\views\api.py:759
msgid ""
"Rating a recipe should have or greater. [0 - 5] Negative value filters "
"rating less than."
msgstr ""
"Egy recept minimális értékelése (0-5). A negatív értékek a maximális "
"értékelés szerint szűrnek."
#: .\cookbook\views\api.py:760
msgid "ID of book a recipe should be in. For multiple repeat parameter."
msgstr ""
"A könyv azonosítója, amelyben a receptnek szerepelnie kell. Többszörös "
"ismétlés esetén paraméter."
"A könyv azonosítója, amelyben a recept található. Többször is megadható."
#: .\cookbook\views\api.py:762
msgid "Book IDs, repeat for multiple. Return recipes with any of the books"
msgstr ""
"A könyv azonosítója. Többször is megadható. A megadott könyvek összes "
"receptjének listája"
#: .\cookbook\views\api.py:764
msgid "Book IDs, repeat for multiple. Return recipes with all of the books."
msgstr ""
"A könyv azonosítója. Többször is megadható. Az összes könyvben szereplő "
"recept listája."
#: .\cookbook\views\api.py:766
msgid "Book IDs, repeat for multiple. Exclude recipes with any of the books."
msgstr ""
"A könyv azonosítói. Többször is megadható. Kizárja a megadott könyvek "
"receptjeit."
#: .\cookbook\views\api.py:768
msgid "Book IDs, repeat for multiple. Exclude recipes with all of the books."
msgstr ""
"A könyv azonosítói. Többször is megadható. Kizárja az összes megadott "
"könyvben szereplő receptet."
#: .\cookbook\views\api.py:770
msgid "If only internal recipes should be returned. [true/<b>false</b>]"
@@ -2587,36 +2581,50 @@ msgid ""
"Filter recipes cooked X times or more. Negative values returns cooked less "
"than X times"
msgstr ""
"X-szer vagy többször főzött receptek szűrése. A negatív értékek X "
"alkalomnál kevesebbet főzött recepteket jelenítik meg"
#: .\cookbook\views\api.py:778
msgid ""
"Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on "
"or before date."
msgstr ""
"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
"vagy később főztek meg utoljára. A - jelölve az adott dátumon vagy azt "
"megelőzően elkészítettek kerülnek be a receptek listájába."
#: .\cookbook\views\api.py:780
msgid ""
"Filter recipes created on or after YYYY-MM-DD. Prepending - filters on or "
"before date."
msgstr ""
"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
"vagy később hoztak létre. A - jelölve az adott dátumon vagy azt megelőzően "
"hozták létre."
#: .\cookbook\views\api.py:782
msgid ""
"Filter recipes updated on or after YYYY-MM-DD. Prepending - filters on or "
"before date."
msgstr ""
"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
"vagy később frissültek. A - jelölve az adott dátumon vagy azt megelőzően "
"frissültek."
#: .\cookbook\views\api.py:784
msgid ""
"Filter recipes lasts viewed on or after YYYY-MM-DD. Prepending - filters on "
"or before date."
msgstr ""
"Megjeleníti azokat a recepteket, amelyeket a megadott napon (ÉÉÉÉ-HH-NN) "
"vagy később néztek meg utoljára. A - jelölve az adott dátumon vagy azt "
"megelőzően néztek meg utoljára."
#: .\cookbook\views\api.py:786
#, fuzzy
#| msgid "If only internal recipes should be returned. [true/<b>false</b>]"
msgid "Filter recipes that can be made with OnHand food. [true/<b>false</b>]"
msgstr "Ha csak a belső recepteket kell visszaadni. [true/<b>false</b>]"
msgstr ""
"Felsorolja azokat a recepteket, amelyeket a rendelkezésre álló összetevőkből "
"el lehet készíteni. [true/<b>false</b>]"
#: .\cookbook\views\api.py:946
msgid ""
@@ -2647,7 +2655,7 @@ msgstr "Semmi feladat."
#: .\cookbook\views\api.py:1198
msgid "Invalid Url"
msgstr ""
msgstr "Érvénytelen URL"
#: .\cookbook\views\api.py:1205
msgid "Connection Refused."
@@ -2655,13 +2663,11 @@ msgstr "Kapcsolat megtagadva."
#: .\cookbook\views\api.py:1210
msgid "Bad URL Schema."
msgstr ""
msgstr "Rossz URL séma."
#: .\cookbook\views\api.py:1233
#, fuzzy
#| msgid "No useable data could be found."
msgid "No usable data could be found."
msgstr "Nem találtam használható adatokat."
msgstr "Nem sikerült használható adatokat találni."
#: .\cookbook\views\api.py:1326 .\cookbook\views\import_export.py:117
msgid "Importing is not implemented for this provider"
@@ -2774,10 +2780,8 @@ msgid "Shopping Categories"
msgstr "Bevásárlási kategóriák"
#: .\cookbook\views\lists.py:187
#, fuzzy
#| msgid "Filter"
msgid "Custom Filters"
msgstr "Szűrő"
msgstr "Egyedi szűrők"
#: .\cookbook\views\lists.py:224
msgid "Steps"

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

@@ -12,8 +12,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-01-08 17:55+0000\n"
"Last-Translator: Joachim Weber <joachim.weber@gmx.de>\n"
"PO-Revision-Date: 2023-10-07 18:02+0000\n"
"Last-Translator: Guilherme Roda <glealroda@gmail.com>\n"
"Language-Team: Portuguese <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/pt/>\n"
"Language: pt\n"
@@ -206,8 +206,8 @@ msgid ""
"Leave empty for dropbox and enter only base url for nextcloud (<code>/remote."
"php/webdav/</code> is added automatically)"
msgstr ""
"Deixar vazio para Dropbox e inserir apenas url base para Nextcloud (<code>/"
"remote.php/webdav/</code>é adicionado automaticamente). "
"Deixar vazio para Dropbox e inserir apenas url base para Nextcloud "
"(<code>/remote.php/webdav/</code>é adicionado automaticamente)"
#: .\cookbook\forms.py:282 .\cookbook\views\edit.py:157
msgid "Storage"
@@ -277,16 +277,12 @@ msgstr ""
"ignorados)."
#: .\cookbook\forms.py:461
#, fuzzy
#| msgid ""
#| "Select type method of search. Click <a href=\"/docs/search/\">here</a> "
#| "for full desciption of choices."
msgid ""
"Select type method of search. Click <a href=\"/docs/search/\">here</a> for "
"full description of choices."
msgstr ""
"Selecionar o método de pesquisa. Uma descrição completa das opções pode ser "
"encontrada <a href=\"/docs/search/\">aqui</a>."
"Selecionar o método de pesquisa. Uma descrição completa das opções pode "
"ser encontrada <a href=\"/docs/search/\">aqui</a>."
#: .\cookbook\forms.py:462
msgid ""
@@ -329,10 +325,8 @@ msgid ""
msgstr ""
#: .\cookbook\forms.py:476
#, fuzzy
#| msgid "Search"
msgid "Search Method"
msgstr "Procurar"
msgstr "Método de Pesquisa"
#: .\cookbook\forms.py:477
msgid "Fuzzy Lookups"
@@ -351,16 +345,12 @@ msgid "Starts With"
msgstr ""
#: .\cookbook\forms.py:481
#, fuzzy
#| msgid "Search"
msgid "Fuzzy Search"
msgstr "Procurar"
msgstr "Pesquisa Fuzzy"
#: .\cookbook\forms.py:482
#, fuzzy
#| msgid "Text"
msgid "Full Text"
msgstr "Texto"
msgstr "Texto Completo"
#: .\cookbook\forms.py:507
msgid ""
@@ -405,10 +395,8 @@ msgid "Prefix to add when copying list to the clipboard."
msgstr ""
#: .\cookbook\forms.py:524
#, fuzzy
#| msgid "Shopping"
msgid "Share Shopping List"
msgstr "Compras"
msgstr "Compartilhar Lista de Compras"
#: .\cookbook\forms.py:525
msgid "Autosync"
@@ -459,10 +447,8 @@ msgid "Reset all food to inherit the fields configured."
msgstr ""
#: .\cookbook\forms.py:557
#, fuzzy
#| msgid "Food that should be replaced."
msgid "Fields on food that should be inherited by default."
msgstr "Prato a ser alterado."
msgstr "Campos do alimento que devem ser herdados por padrão."
#: .\cookbook\forms.py:558
msgid "Show recipe counts on search filters"
@@ -516,10 +502,8 @@ msgid "One of queryset or hash_key must be provided"
msgstr ""
#: .\cookbook\helper\recipe_url_import.py:266
#, fuzzy
#| msgid "Use fractions"
msgid "reverse rotation"
msgstr "Usar frações"
msgstr "rotação reversa"
#: .\cookbook\helper\recipe_url_import.py:267
msgid "careful rotation"
@@ -585,16 +569,12 @@ msgid "Imported %s recipes."
msgstr "%s receitas importadas."
#: .\cookbook\integration\openeats.py:26
#, fuzzy
#| msgid "Recipes"
msgid "Recipe source:"
msgstr "Receitas"
msgstr "Fonte da Receita:"
#: .\cookbook\integration\paprika.py:49
#, fuzzy
#| msgid "Note"
msgid "Notes"
msgstr "Nota"
msgstr "Notas"
#: .\cookbook\integration\paprika.py:52
msgid "Nutritional Information"
@@ -606,10 +586,8 @@ msgstr ""
#: .\cookbook\integration\recettetek.py:54
#: .\cookbook\integration\recipekeeper.py:70
#, fuzzy
#| msgid "Import"
msgid "Imported from"
msgstr "Importar"
msgstr "Importado de"
#: .\cookbook\integration\saffron.py:23
msgid "Servings"
@@ -706,32 +684,24 @@ msgid "Raw"
msgstr ""
#: .\cookbook\models.py:1231
#, fuzzy
#| msgid "New Food"
msgid "Food Alias"
msgstr "Novo Prato"
msgstr "Apelido do Alimento"
#: .\cookbook\models.py:1231
#, fuzzy
#| msgid "Units"
msgid "Unit Alias"
msgstr "Unidades"
msgstr "Apelido da Unidade"
#: .\cookbook\models.py:1231
#, fuzzy
#| msgid "Keywords"
msgid "Keyword Alias"
msgstr "Palavras-chave"
msgstr "Apelido de Palavra-chave"
#: .\cookbook\models.py:1232
msgid "Description Replace"
msgstr ""
#: .\cookbook\models.py:1232
#, fuzzy
#| msgid "Instructions"
msgid "Instruction Replace"
msgstr "Instruções"
msgstr "Substituir Instruções"
#: .\cookbook\models.py:1258 .\cookbook\views\delete.py:36
#: .\cookbook\views\edit.py:251 .\cookbook\views\new.py:48
@@ -739,10 +709,8 @@ msgid "Recipe"
msgstr "Receita"
#: .\cookbook\models.py:1259
#, fuzzy
#| msgid "New Food"
msgid "Food"
msgstr "Novo Prato"
msgstr "Alimento"
#: .\cookbook\models.py:1260 .\cookbook\templates\base.html:141
msgid "Keyword"
@@ -880,10 +848,8 @@ msgid "Primary"
msgstr ""
#: .\cookbook\templates\account\email.html:47
#, fuzzy
#| msgid "Make Header"
msgid "Make Primary"
msgstr "Adicionar Cabeçalho"
msgstr "Tornar Primeiro"
#: .\cookbook\templates\account\email.html:49
msgid "Re-send Verification"
@@ -1004,10 +970,8 @@ msgstr ""
#: .\cookbook\templates\account\password_change.html:12
#: .\cookbook\templates\account\password_set.html:12
#, fuzzy
#| msgid "Settings"
msgid "Password"
msgstr "Definições"
msgstr "Senha"
#: .\cookbook\templates\account\password_change.html:22
msgid "Forgot Password?"
@@ -1050,10 +1014,8 @@ msgid ""
msgstr ""
#: .\cookbook\templates\account\password_reset_from_key.html:33
#, fuzzy
#| msgid "Settings"
msgid "change password"
msgstr "Definições"
msgstr "alterar senha"
#: .\cookbook\templates\account\password_reset_from_key.html:36
#: .\cookbook\templates\account\password_reset_from_key_done.html:19
@@ -1125,10 +1087,8 @@ msgid "Shopping"
msgstr "Compras"
#: .\cookbook\templates\base.html:153 .\cookbook\views\lists.py:105
#, fuzzy
#| msgid "New Food"
msgid "Foods"
msgstr "Novo Prato"
msgstr "Alimentos"
#: .\cookbook\templates\base.html:165 .\cookbook\views\lists.py:122
msgid "Units"
@@ -1139,20 +1099,16 @@ msgid "Supermarket"
msgstr ""
#: .\cookbook\templates\base.html:191
#, fuzzy
#| msgid "Batch edit Category"
msgid "Supermarket Category"
msgstr "Editar Categorias em massa"
msgstr "Categoria de Supermercado"
#: .\cookbook\templates\base.html:203 .\cookbook\views\lists.py:171
msgid "Automations"
msgstr ""
#: .\cookbook\templates\base.html:217 .\cookbook\views\lists.py:207
#, fuzzy
#| msgid "File ID"
msgid "Files"
msgstr "ID the ficheiro"
msgstr "Arquivos"
#: .\cookbook\templates\base.html:229
msgid "Batch Edit"
@@ -1166,10 +1122,8 @@ msgstr "Histórico"
#: .\cookbook\templates\base.html:255
#: .\cookbook\templates\ingredient_editor.html:7
#: .\cookbook\templates\ingredient_editor.html:13
#, fuzzy
#| msgid "Ingredients"
msgid "Ingredient Editor"
msgstr "Ingredientes"
msgstr "Editor de Ingrediente"
#: .\cookbook\templates\base.html:267
#: .\cookbook\templates\export_response.html:7
@@ -1191,10 +1145,8 @@ msgid "External Recipes"
msgstr ""
#: .\cookbook\templates\base.html:301 .\cookbook\templates\space_manage.html:15
#, fuzzy
#| msgid "Settings"
msgid "Space Settings"
msgstr "Definições"
msgstr "Configurar Espaço"
#: .\cookbook\templates\base.html:306 .\cookbook\templates\system.html:13
msgid "System"
@@ -1206,10 +1158,8 @@ msgstr "Administração"
#: .\cookbook\templates\base.html:312
#: .\cookbook\templates\space_overview.html:25
#, fuzzy
#| msgid "Create"
msgid "Your Spaces"
msgstr "Criar"
msgstr "Seus Espaços"
#: .\cookbook\templates\base.html:323
#: .\cookbook\templates\space_overview.html:6
@@ -1288,19 +1238,15 @@ msgstr ""
#: .\cookbook\templates\batch\monitor.html:28
msgid "Sync Now!"
msgstr "Sincronizar"
msgstr "Sincronizar Agora!"
#: .\cookbook\templates\batch\monitor.html:29
#, fuzzy
#| msgid "Recipes"
msgid "Show Recipes"
msgstr "Receitas"
msgstr "Mostrar Receitas"
#: .\cookbook\templates\batch\monitor.html:30
#, fuzzy
#| msgid "View Log"
msgid "Show Log"
msgstr "Ver Registro"
msgstr "Mostrar Log"
#: .\cookbook\templates\batch\waiting.html:4
#: .\cookbook\templates\batch\waiting.html:10
@@ -1335,7 +1281,7 @@ msgstr "Editar Receita"
#: .\cookbook\templates\generic\delete_template.html:21
#, python-format
msgid "Are you sure you want to delete the %(title)s: <b>%(object)s</b> "
msgstr "Tem a certeza que quer apagar %(title)s: <b>%(object)s</b>"
msgstr "Tem certeza que deseja apagar %(title)s: <b>%(object)s</b> "
#: .\cookbook\templates\generic\delete_template.html:22
msgid "This cannot be undone!"
@@ -1369,7 +1315,7 @@ msgstr "Apagar ficheiro original"
#: .\cookbook\templates\generic\list_template.html:6
#: .\cookbook\templates\generic\list_template.html:22
msgid "List"
msgstr "Listar "
msgstr "Listar"
#: .\cookbook\templates\generic\list_template.html:36
msgid "Filter"
@@ -1422,13 +1368,13 @@ msgid ""
" "
msgstr ""
"\n"
" Os </b>campos da senha e Token</b> são guardados dentro da base de "
"dados como <b>texto simples.</b>\n"
"Isto é necessário porque eles são usados para fazer pedidos á API, mas "
"também aumenta o risco de\n"
"de alguém os roubar. <br/>\n"
"Para limitar os possíveis danos, tokens e contas com acesso limitado podem "
"ser usadas.\n"
" Os campos de <b>senha e Token</b> são armazenados na base de dados "
"como <b>texto simples</b>.\n"
" Isto é necessário porque eles são usados para fazer pedidos à API, "
"mas também aumenta o risco\n"
" de alguém os roubar.<br/>\n"
" Para limitar os possíveis danos, tokens e contas com acesso limitado "
"podem ser usadas.\n"
" "
#: .\cookbook\templates\index.html:29
@@ -1441,7 +1387,7 @@ msgstr "Nova Receita"
#: .\cookbook\templates\index.html:53
msgid "Advanced Search"
msgstr "Procura avançada "
msgstr "Pesquisa avançada"
#: .\cookbook\templates\index.html:57
msgid "Reset Search"
@@ -1493,8 +1439,6 @@ msgstr ""
#: .\cookbook\templates\markdown_info.html:57
#: .\cookbook\templates\markdown_info.html:73
#, fuzzy
#| msgid "or by leaving a blank line inbetween."
msgid "or by leaving a blank line in between."
msgstr "ou deixando uma linha em branco no meio."
@@ -1518,10 +1462,6 @@ msgid "Lists"
msgstr "Listas"
#: .\cookbook\templates\markdown_info.html:85
#, fuzzy
#| msgid ""
#| "Lists can ordered or unorderd. It is <b>important to leave a blank line "
#| "before the list!</b>"
msgid ""
"Lists can ordered or unordered. It is <b>important to leave a blank line "
"before the list!</b>"
@@ -1598,7 +1538,7 @@ msgstr "Cabeçalho"
#: .\cookbook\templates\markdown_info.html:157
#: .\cookbook\templates\markdown_info.html:178
msgid "Cell"
msgstr "Célula "
msgstr "Célula"
#: .\cookbook\templates\no_groups_info.html:5
#: .\cookbook\templates\no_groups_info.html:12
@@ -1666,10 +1606,8 @@ msgstr ""
#: .\cookbook\templates\search_info.html:5
#: .\cookbook\templates\search_info.html:9
#: .\cookbook\templates\settings.html:24
#, fuzzy
#| msgid "Search String"
msgid "Search Settings"
msgstr "Procurar"
msgstr "Configurações de Pesquisa"
#: .\cookbook\templates\search_info.html:10
msgid ""
@@ -1684,10 +1622,8 @@ msgid ""
msgstr ""
#: .\cookbook\templates\search_info.html:19
#, fuzzy
#| msgid "Search"
msgid "Search Methods"
msgstr "Procurar"
msgstr "Métodos de Pesquisa"
#: .\cookbook\templates\search_info.html:23
msgid ""
@@ -1769,10 +1705,8 @@ msgid ""
msgstr ""
#: .\cookbook\templates\search_info.html:69
#, fuzzy
#| msgid "Search Recipe"
msgid "Search Fields"
msgstr "Procure Receita"
msgstr "Campos de Pesquisa"
#: .\cookbook\templates\search_info.html:73
msgid ""
@@ -1810,10 +1744,8 @@ msgid ""
msgstr ""
#: .\cookbook\templates\search_info.html:95
#, fuzzy
#| msgid "Search"
msgid "Search Index"
msgstr "Procurar"
msgstr "Índice de Pesquisa"
#: .\cookbook\templates\search_info.html:99
msgid ""
@@ -2012,10 +1944,8 @@ msgid "Owner"
msgstr ""
#: .\cookbook\templates\space_overview.html:57
#, fuzzy
#| msgid "Create"
msgid "Leave Space"
msgstr "Criar"
msgstr "Sair do Espaço"
#: .\cookbook\templates\space_overview.html:78
#: .\cookbook\templates\space_overview.html:88
@@ -2034,10 +1964,8 @@ msgstr ""
#: .\cookbook\templates\space_overview.html:96
#: .\cookbook\templates\space_overview.html:105
#, fuzzy
#| msgid "Create"
msgid "Create Space"
msgstr "Criar"
msgstr "Criar Espaço"
#: .\cookbook\templates\space_overview.html:99
msgid "Create your own recipe space."
@@ -2487,10 +2415,8 @@ msgid "Shopping Categories"
msgstr ""
#: .\cookbook\views\lists.py:187
#, fuzzy
#| msgid "Filter"
msgid "Custom Filters"
msgstr "Filtrar"
msgstr "Filtros Customizados"
#: .\cookbook\views\lists.py:224
msgid "Steps"

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
"PO-Revision-Date: 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

@@ -1,9 +1,9 @@
from django.conf import settings
from django.contrib.postgres.search import SearchVector
from django.core.management.base import BaseCommand
from django_scopes import scopes_disabled
from django.utils import translation
from django.utils.translation import gettext_lazy as _
from django_scopes import scopes_disabled
from cookbook.managers import DICTIONARY
from cookbook.models import Recipe, Step
@@ -14,7 +14,7 @@ class Command(BaseCommand):
help = _('Rebuilds full text search index on Recipe')
def handle(self, *args, **options):
if settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.postgresql':
self.stdout.write(self.style.WARNING(_('Only Postgresql databases use full text search, no index to rebuild')))
try:

View File

@@ -34,35 +34,14 @@ class RecipeSearchManager(models.Manager):
+ SearchVector(StringAgg('steps__ingredients__food__name__unaccent', delimiter=' '), weight='B', config=language)
+ SearchVector(StringAgg('keywords__name__unaccent', delimiter=' '), weight='B', config=language))
search_rank = SearchRank(search_vectors, search_query)
# USING TRIGRAM BREAKS WEB SEARCH
# ADDING MULTIPLE TRIGRAMS CREATES DUPLICATE RESULTS
# DISTINCT NOT COMPAITBLE WITH ANNOTATE
# trigram_name = (TrigramSimilarity('name', search_text))
# trigram_description = (TrigramSimilarity('description', search_text))
# trigram_food = (TrigramSimilarity('steps__ingredients__food__name', search_text))
# trigram_keyword = (TrigramSimilarity('keywords__name', search_text))
# adding additional trigrams created duplicates
# + TrigramSimilarity('description', search_text)
# + TrigramSimilarity('steps__ingredients__food__name', search_text)
# + TrigramSimilarity('keywords__name', search_text)
return (
self.get_queryset()
.annotate(
search=search_vectors,
rank=search_rank,
# trigram=trigram_name+trigram_description+trigram_food+trigram_keyword
# trigram_name=trigram_name,
# trigram_description=trigram_description,
# trigram_food=trigram_food,
# trigram_keyword=trigram_keyword
)
.filter(
Q(search=search_query)
# | Q(trigram_name__gt=0.1)
# | Q(name__icontains=search_text)
# | Q(trigram_name__gt=0.2)
# | Q(trigram_description__gt=0.2)
# | Q(trigram_food__gt=0.2)
# | Q(trigram_keyword__gt=0.2)
)
.order_by('-rank'))

View File

@@ -9,7 +9,7 @@ from django.utils import translation
from django_scopes import scopes_disabled
from cookbook.managers import DICTIONARY
from cookbook.models import (Index, PermissionModelMixin, Recipe, Step, SearchFields)
from cookbook.models import Index, PermissionModelMixin, Recipe, SearchFields, Step
def allSearchFields():
@@ -21,7 +21,7 @@ def nameSearchField():
def set_default_search_vector(apps, schema_editor):
if settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.postgresql':
return
language = DICTIONARY.get(translation.get_language(), 'simple')
with scopes_disabled():

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.9 on 2023-06-26 13:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0193_space_internal_note'),
]
operations = [
migrations.AlterField(
model_name='food',
name='properties_food_amount',
field=models.DecimalField(blank=True, decimal_places=2, default=100, max_digits=16),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.1.9 on 2023-06-30 20:34
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0194_alter_food_properties_food_amount'),
]
operations = [
migrations.AddField(
model_name='invitelink',
name='internal_note',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='userspace',
name='internal_note',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='userspace',
name='invite_link',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.invitelink'),
),
migrations.AlterField(
model_name='userpreference',
name='theme',
field=models.CharField(choices=[('TANDOOR', 'Tandoor'), ('BOOTSTRAP', 'Bootstrap'), ('DARKLY', 'Darkly'), ('FLATLY', 'Flatly'), ('SUPERHERO', 'Superhero'), ('TANDOOR_DARK', 'Tandoor Dark (INCOMPLETE)')], default='TANDOOR', max_length=128),
),
]

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,30 @@
# Generated by Django 4.1.10 on 2023-09-01 17:03
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'),
('FOOD_REPLACE', 'Food Replace'),
('UNIT_REPLACE', 'Unit Replace'),
('NAME_REPLACE', 'Name Replace')],
max_length=128),
),
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 4.1.10 on 2023-08-29 11:59
from django.db import migrations
from django.db.models import F, Value
from django.db.models.functions import Concat
from django_scopes import scopes_disabled
def migrate_icons(apps, schema_editor):
with scopes_disabled():
MealType = apps.get_model('cookbook', 'MealType')
Keyword = apps.get_model('cookbook', 'Keyword')
PropertyType = apps.get_model('cookbook', 'PropertyType')
RecipeBook = apps.get_model('cookbook', 'RecipeBook')
MealType.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
Keyword.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
PropertyType.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
RecipeBook.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0199_alter_propertytype_options_alter_automation_type_and_more'),
]
operations = [
migrations.RunPython(
migrate_icons
),
migrations.AlterModelOptions(
name='propertytype',
options={'ordering': ('order',)},
),
migrations.RemoveField(
model_name='keyword',
name='icon',
),
migrations.RemoveField(
model_name='mealtype',
name='icon',
),
migrations.RemoveField(
model_name='propertytype',
name='icon',
),
migrations.RemoveField(
model_name='recipebook',
name='icon',
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 4.1.10 on 2023-09-08 12:20
from django.db import migrations, models
from django.db.models import F
from django_scopes import scopes_disabled
def apply_migration(apps, schema_editor):
with scopes_disabled():
MealPlan = apps.get_model('cookbook', 'MealPlan')
MealPlan.objects.update(to_date=F('from_date'))
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0200_alter_propertytype_options_remove_keyword_icon_and_more'),
]
operations = [
migrations.RenameField(
model_name='mealplan',
old_name='date',
new_name='from_date',
),
migrations.AddField(
model_name='mealplan',
name='to_date',
field=models.DateField(blank=True, null=True),
),
migrations.RunPython(apply_migration),
migrations.AlterField(
model_name='mealplan',
name='to_date',
field=models.DateField(),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.1.10 on 2023-09-12 13:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0201_rename_date_mealplan_from_date_mealplan_to_date'),
]
operations = [
migrations.RemoveField(
model_name='space',
name='show_facet_count',
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.2.5 on 2023-09-14 12:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0202_remove_space_show_facet_count'),
]
operations = [
migrations.AddConstraint(
model_name='mealtype',
constraint=models.UniqueConstraint(fields=('space', 'name', 'created_by'), name='mt_unique_name_per_space'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.7 on 2023-11-27 21:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0203_alter_unique_contstraints'),
]
operations = [
migrations.AddField(
model_name='propertytype',
name='fdc_id',
field=models.CharField(blank=True, default=None, max_length=128, null=True),
),
]

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,
@@ -116,10 +116,7 @@ class TreeModel(MP_Node):
_full_name_separator = ' > '
def __str__(self):
if self.icon:
return f"{self.icon} {self.name}"
else:
return f"{self.name}"
return f"{self.name}"
@property
def parent(self):
@@ -188,7 +185,6 @@ class TreeModel(MP_Node):
:param filter: Filter (include) the descendants nodes with the provided Q filter
"""
descendants = Q()
# TODO filter the queryset nodes to exclude descendants of objects in the queryset
nodes = queryset.values('path', 'depth')
for node in nodes:
descendants |= Q(path__startswith=node['path'], depth__gt=node['depth'])
@@ -268,7 +264,6 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
no_sharing_limit = models.BooleanField(default=False)
demo = models.BooleanField(default=False)
food_inherit = models.ManyToManyField(FoodInheritField, blank=True)
show_facet_count = models.BooleanField(default=False)
internal_note = models.TextField(blank=True, null=True)
@@ -331,6 +326,7 @@ class UserPreference(models.Model, PermissionModelMixin):
FLATLY = 'FLATLY'
SUPERHERO = 'SUPERHERO'
TANDOOR = 'TANDOOR'
TANDOOR_DARK = 'TANDOOR_DARK'
THEMES = (
(TANDOOR, 'Tandoor'),
@@ -338,6 +334,7 @@ class UserPreference(models.Model, PermissionModelMixin):
(DARKLY, 'Darkly'),
(FLATLY, 'Flatly'),
(SUPERHERO, 'Superhero'),
(TANDOOR_DARK, 'Tandoor Dark (INCOMPLETE)'),
)
# Nav colors
@@ -392,6 +389,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=",")
@@ -413,6 +411,9 @@ class UserSpace(models.Model, PermissionModelMixin):
# that having more than one active space should just break certain parts of the application and not leak any data
active = models.BooleanField(default=False)
invite_link = models.ForeignKey("InviteLink", on_delete=models.PROTECT, null=True, blank=True)
internal_note = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -527,7 +528,6 @@ class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelM
if SORT_TREE_BY_NAME:
node_order_by = ['name']
name = models.CharField(max_length=64)
icon = models.CharField(max_length=16, blank=True, null=True)
description = models.TextField(default="", blank=True)
created_at = models.DateTimeField(auto_now_add=True) # TODO deprecate
updated_at = models.DateTimeField(auto_now=True) # TODO deprecate
@@ -574,6 +574,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)
@@ -585,7 +586,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
child_inherit_fields = models.ManyToManyField(FoodInheritField, blank=True, related_name='child_inherit')
properties = models.ManyToManyField("Property", blank=True, through='FoodProperty')
properties_food_amount = models.IntegerField(default=100, blank=True)
properties_food_amount = models.DecimalField(default=100, max_digits=16, decimal_places=2, blank=True)
properties_food_unit = models.ForeignKey(Unit, on_delete=models.PROTECT, blank=True, null=True)
preferred_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_unit')
@@ -717,25 +718,6 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
food = ""
unit = ""
if self.always_use_plural_food and self.food.plural_name not in (None, "") and not self.no_amount:
food = self.food.plural_name
else:
if self.amount > 1 and self.food.plural_name not in (None, "") and not self.no_amount:
food = self.food.plural_name
else:
food = str(self.food)
if self.always_use_plural_unit and self.unit.plural_name not in (None, "") and not self.no_amount:
unit = self.unit.plural_name
else:
if self.amount > 1 and self.unit is not None and self.unit.plural_name not in (None, "") and not self.no_amount:
unit = self.unit.plural_name
else:
unit = str(self.unit)
return str(self.amount) + ' ' + str(unit) + ' ' + str(food)
class Meta:
ordering = ['order', 'pk']
indexes = (
@@ -751,6 +733,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)
@@ -778,11 +761,13 @@ 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)
fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None)
# TODO show if empty property?
# TODO formatting property?
@@ -797,6 +782,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):
@@ -945,7 +931,6 @@ class RecipeImport(models.Model, PermissionModelMixin):
class RecipeBook(ExportModelOperationsMixin('book'), models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
description = models.TextField(blank=True)
icon = models.CharField(max_length=16, blank=True, null=True)
shared = models.ManyToManyField(User, blank=True, related_name='shared_with')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
filter = models.ForeignKey('cookbook.CustomFilter', null=True, blank=True, on_delete=models.SET_NULL)
@@ -988,7 +973,6 @@ class RecipeBookEntry(ExportModelOperationsMixin('book_entry'), models.Model, Pe
class MealType(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
order = models.IntegerField(default=0)
icon = models.CharField(max_length=16, blank=True, null=True)
color = models.CharField(max_length=7, blank=True, null=True)
default = models.BooleanField(default=False, blank=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
@@ -999,6 +983,11 @@ class MealType(models.Model, PermissionModelMixin):
def __str__(self):
return self.name
class Meta:
constraints = [
models.UniqueConstraint(fields=['space', 'name', 'created_by'], name='mt_unique_name_per_space'),
]
class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
@@ -1008,7 +997,8 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission
shared = models.ManyToManyField(User, blank=True, related_name='plan_share')
meal_type = models.ForeignKey(MealType, on_delete=models.CASCADE)
note = models.TextField(blank=True)
date = models.DateField()
from_date = models.DateField()
to_date = models.DateField()
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@@ -1022,7 +1012,7 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission
return self.meal_type.name
def __str__(self):
return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
return f'{self.get_label()} - {self.from_date} - {self.meal_type.name}'
class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), models.Model, PermissionModelMixin):
@@ -1142,6 +1132,8 @@ class InviteLink(ExportModelOperationsMixin('invite_link'), models.Model, Permis
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
internal_note = models.TextField(blank=True, null=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@@ -1308,7 +1300,7 @@ class UserFile(ExportModelOperationsMixin('user_files'), models.Model, Permissio
def is_image(self):
try:
img = Image.open(self.file.file.file)
Image.open(self.file.file.file)
return True
except Exception:
return False
@@ -1326,10 +1318,25 @@ 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'
FOOD_REPLACE = 'FOOD_REPLACE'
UNIT_REPLACE = 'UNIT_REPLACE'
NAME_REPLACE = 'NAME_REPLACE'
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')),))
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')),
(FOOD_REPLACE, _('Food Replace')),
(UNIT_REPLACE, _('Unit Replace')),
(NAME_REPLACE, _('Name Replace')),
))
name = models.CharField(max_length=128, default='')
description = models.TextField(blank=True, null=True)

View File

@@ -67,17 +67,3 @@ class FilterSchema(AutoSchema):
'schema': {'type': 'string', },
})
return parameters
# class QueryOnlySchema(AutoSchema):
# def get_path_parameters(self, path, method):
# if not is_list_view(path, method, self.view):
# return super(QueryOnlySchema, self).get_path_parameters(path, method)
# parameters = super().get_path_parameters(path, method)
# parameters.append({
# "name": 'query', "in": "query", "required": False,
# "description": 'Query string matched (fuzzy) against object name.',
# 'schema': {'type': 'string', },
# })
# return parameters

View File

@@ -6,34 +6,34 @@ from gettext import gettext as _
from html import escape
from smtplib import SMTPException
from django.contrib.auth.models import Group, User, AnonymousUser
from django.contrib.auth.models import AnonymousUser, Group, User
from django.core.cache import caches
from django.core.mail import send_mail
from django.db.models import Avg, Q, QuerySet, Sum
from django.db.models import Q, QuerySet, Sum
from django.http import BadHeaderError
from django.urls import reverse
from django.utils import timezone
from django_scopes import scopes_disabled
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
from PIL import Image
from oauth2_provider.models import AccessToken
from PIL import Image
from rest_framework import serializers
from rest_framework.exceptions import NotFound, ValidationError
from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.property_helper import FoodPropertyHelper
from cookbook.helper.permission_helper import above_space_limit
from cookbook.helper.property_helper import FoodPropertyHelper
from cookbook.helper.shopping_helper import RecipeShoppingEditor
from cookbook.helper.unit_conversion_helper import UnitConversionHelper
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, CustomFilter,
ExportLog, Food, FoodInheritField, ImportLog, Ingredient, InviteLink,
Keyword, MealPlan, MealType, NutritionInformation, Recipe, RecipeBook,
RecipeBookEntry, RecipeImport, ShareLink, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
SyncLog, Unit, UserFile, UserPreference, UserSpace, ViewLog, UnitConversion, Property,
PropertyType, Property)
Keyword, MealPlan, MealType, NutritionInformation, Property,
PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport,
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog)
from cookbook.templatetags.custom_tags import markdown
from recipes.settings import AWS_ENABLED, MEDIA_URL
@@ -56,10 +56,9 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
api_serializer = None
# extended values are computationally expensive and not needed in normal circumstances
try:
if str2bool(
self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
if str2bool(self.context['request'].query_params.get('extended', False)) and self.__class__ == api_serializer:
return fields
except (AttributeError, KeyError) as e:
except (AttributeError, KeyError):
pass
try:
del fields['image']
@@ -103,13 +102,13 @@ class CustomDecimalField(serializers.Field):
return round(value, 2).normalize()
def to_internal_value(self, data):
if type(data) == int or type(data) == float:
if isinstance(data, int) or isinstance(data, float):
return data
elif type(data) == str:
elif isinstance(data, str):
if data == '':
return 0
try:
return float(data.replace(',', ''))
return float(data.replace(',', '.'))
except ValueError:
raise ValidationError('A valid number is required')
@@ -145,11 +144,11 @@ class SpaceFilterSerializer(serializers.ListSerializer):
def to_representation(self, data):
if self.context.get('request', None) is None:
return
if (type(data) == QuerySet and data.query.is_sliced):
if (isinstance(data, QuerySet) and data.query.is_sliced):
# if query is sliced it came from api request not nested serializer
return super().to_representation(data)
if self.child.Meta.model == User:
if type(self.context['request'].user) == AnonymousUser:
if isinstance(self.context['request'].user, AnonymousUser):
data = []
else:
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
@@ -209,7 +208,7 @@ class UserFileSerializer(serializers.ModelSerializer):
def get_preview_link(self, obj):
try:
img = Image.open(obj.file.file.file)
Image.open(obj.file.file.file)
return self.context['request'].build_absolute_uri(obj.file.url)
except Exception:
traceback.print_exc()
@@ -257,7 +256,7 @@ class UserFileViewSerializer(serializers.ModelSerializer):
def get_preview_link(self, obj):
try:
img = Image.open(obj.file.file.file)
Image.open(obj.file.file.file)
return self.context['request'].build_absolute_uri(obj.file.url)
except Exception:
traceback.print_exc()
@@ -301,7 +300,7 @@ class SpaceSerializer(WritableNestedModelSerializer):
model = Space
fields = (
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb',
'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
'image', 'use_plural',)
read_only_fields = (
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
@@ -322,8 +321,8 @@ class UserSpaceSerializer(WritableNestedModelSerializer):
class Meta:
model = UserSpace
fields = ('id', 'user', 'space', 'groups', 'active', 'created_at', 'updated_at',)
read_only_fields = ('id', 'created_at', 'updated_at', 'space')
fields = ('id', 'user', 'space', 'groups', 'active', 'internal_note', 'invite_link', 'created_at', 'updated_at',)
read_only_fields = ('id', 'invite_link', 'created_at', 'updated_at', 'space')
class SpacedModelSerializer(serializers.ModelSerializer):
@@ -335,13 +334,16 @@ class SpacedModelSerializer(serializers.ModelSerializer):
class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
space = validated_data.pop('space', self.context['request'].space)
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
obj, created = MealType.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
return obj
class Meta:
list_serializer_class = SpaceFilterSerializer
model = MealType
fields = ('id', 'name', 'order', 'icon', 'color', 'default', 'created_by')
fields = ('id', 'name', 'order', 'color', 'default', 'created_by')
read_only_fields = ('created_by',)
@@ -375,7 +377,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'
)
@@ -448,7 +450,7 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
class Meta:
model = Keyword
fields = (
'id', 'name', 'icon', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
'id', 'name', 'label', 'description', 'image', 'parent', 'numchild', 'numrecipe', 'created_at',
'updated_at', 'full_name')
read_only_fields = ('id', 'label', 'numchild', 'parent', 'image')
@@ -457,17 +459,17 @@ class UnitSerializer(UniqueFieldsMixin, ExtendedRecipeMixin, OpenDataModelMixin)
recipe_filter = 'steps__ingredients__unit'
def create(self, validated_data):
name = validated_data.pop('name').strip()
# get_or_create drops any field that contains '__' when creating so values must be included in validated data
space = validated_data.pop('space', self.context['request'].space)
if x := validated_data.get('name', None):
validated_data['name'] = x.strip()
if x := validated_data.get('name', None):
validated_data['plural_name'] = x.strip()
if plural_name := validated_data.pop('plural_name', None):
plural_name = plural_name.strip()
if unit := Unit.objects.filter(Q(name=name) | Q(plural_name=name)).first():
if unit := Unit.objects.filter(Q(name__iexact=validated_data['name']) | Q(plural_name__iexact=validated_data['name']), space=space).first():
return unit
space = validated_data.pop('space', self.context['request'].space)
obj, created = Unit.objects.get_or_create(name=name, plural_name=plural_name, space=space,
defaults=validated_data)
obj, created = Unit.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
return obj
def update(self, instance, validated_data):
@@ -478,16 +480,16 @@ 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')
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer, OpenDataModelMixin):
def create(self, validated_data):
name = validated_data.pop('name').strip()
validated_data['name'] = validated_data['name'].strip()
space = validated_data.pop('space', self.context['request'].space)
obj, created = SupermarketCategory.objects.get_or_create(name=name, space=space)
obj, created = SupermarketCategory.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
return obj
def update(self, instance, validated_data):
@@ -509,31 +511,35 @@ class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataModelMixin):
category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
space = validated_data.pop('space', self.context['request'].space)
obj, created = Supermarket.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
return obj
class Meta:
model = Supermarket
fields = ('id', 'name', 'description', 'category_to_supermarket', 'open_data_slug')
class PropertyTypeSerializer(OpenDataModelMixin):
class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer, UniqueFieldsMixin):
id = serializers.IntegerField(required=False)
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
if property_type := PropertyType.objects.filter(Q(name=validated_data['name'])).first():
return property_type
return super().create(validated_data)
validated_data['name'] = validated_data['name'].strip()
space = validated_data.pop('space', self.context['request'].space)
obj, created = PropertyType.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
return obj
class Meta:
model = PropertyType
fields = ('id', 'name', 'icon', 'unit', 'description', 'open_data_slug')
fields = ('id', 'name', 'unit', 'description', 'order', 'open_data_slug', 'fdc_id',)
class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
property_type = PropertyTypeSerializer()
property_amount = CustomDecimalField()
# TODO prevent updates
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
@@ -541,7 +547,6 @@ class PropertySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
class Meta:
model = Property
fields = ('id', 'property_amount', 'property_type')
read_only_fields = ('id',)
class RecipeSimpleSerializer(WritableNestedModelSerializer):
@@ -572,7 +577,6 @@ class FoodSimpleSerializer(serializers.ModelSerializer):
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin, OpenDataModelMixin):
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
# shopping = serializers.SerializerMethodField('get_shopping_status')
shopping = serializers.ReadOnlyField(source='shopping_status')
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
child_inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
@@ -582,6 +586,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
properties = PropertySerializer(many=True, allow_null=True, required=False)
properties_food_unit = UnitSerializer(allow_null=True, required=False)
properties_food_amount = CustomDecimalField(required=False)
recipe_filter = 'steps__ingredients__food'
images = ['recipe__image']
@@ -610,9 +615,6 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
filter |= Q(path__startswith=obj.path, depth__gt=obj.depth)
return Food.objects.filter(filter).filter(onhand_users__id__in=shared_users).exists()
# def get_shopping_status(self, obj):
# return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0
def create(self, validated_data):
name = validated_data['name'].strip()
@@ -635,7 +637,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
validated_data['recipe'] = Recipe.objects.get(**recipe)
# assuming if on hand for user also onhand for shopping_share users
if not onhand is None:
if onhand is not None:
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
if self.instance:
onhand_users = self.instance.onhand_users.all()
@@ -649,8 +651,15 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
if properties_food_unit := validated_data.pop('properties_food_unit', None):
properties_food_unit = Unit.objects.filter(name=properties_food_unit['name']).first()
properties = validated_data.pop('properties', None)
obj, created = Food.objects.get_or_create(name=name, plural_name=plural_name, space=space, properties_food_unit=properties_food_unit,
defaults=validated_data)
if properties and len(properties) > 0:
for p in properties:
obj.properties.add(Property.objects.create(property_type_id=p['property_type']['id'], property_amount=p['property_amount'], space=space))
return obj
def update(self, instance, validated_data):
@@ -661,7 +670,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
# assuming if on hand for user also onhand for shopping_share users
onhand = validated_data.get('food_onhand', None)
reset_inherit = self.initial_data.get('reset_inherit', False)
if not onhand is None:
if onhand is not None:
shared_users = [user := self.context['request'].user] + list(user.userpreference.shopping_share.all())
if onhand:
validated_data['onhand_users'] = list(self.instance.onhand_users.all()) + shared_users
@@ -677,8 +686,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
class Meta:
model = Food
fields = (
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe',
'properties', 'properties_food_amount', 'properties_food_unit',
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url',
'properties', 'properties_food_amount', 'properties_food_unit', 'fdc_id',
'food_onhand', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug',
@@ -756,14 +765,15 @@ class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
def get_step_recipe_data(self, obj):
# check if root type is recipe to prevent infinite recursion
# can be improved later to allow multi level embedding
if obj.step_recipe and type(self.parent.root) == RecipeSerializer:
if obj.step_recipe and isinstance(self.parent.root, RecipeSerializer):
return StepRecipeSerializer(obj.step_recipe, context={'request': self.context['request']}).data
class Meta:
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'
)
@@ -792,9 +802,17 @@ class UnitConversionSerializer(WritableNestedModelSerializer, OpenDataModelMixin
return text + f' = {round(obj.converted_amount)} {obj.converted_unit}'
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
validated_data['space'] = validated_data.pop('space', self.context['request'].space)
try:
return UnitConversion.objects.get(
food__name__iexact=validated_data.get('food', {}).get('name', None),
base_unit__name__iexact=validated_data.get('base_unit', {}).get('name', None),
converted_unit__name__iexact=validated_data.get('converted_unit', {}).get('name', None),
space=validated_data['space']
)
except UnitConversion.DoesNotExist:
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
class Meta:
model = UnitConversion
@@ -930,7 +948,7 @@ class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSerializer)
class Meta:
model = RecipeBook
fields = ('id', 'name', 'description', 'icon', 'shared', 'created_by', 'filter')
fields = ('id', 'name', 'description', 'shared', 'created_by', 'filter')
read_only_fields = ('created_by',)
@@ -947,8 +965,7 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
def create(self, validated_data):
book = validated_data['book']
recipe = validated_data['recipe']
if not book.get_owner() == self.context['request'].user and not self.context[
'request'].user in book.get_shared():
if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
raise NotFound(detail=None, code=None)
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
return obj
@@ -986,12 +1003,22 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
model = MealPlan
fields = (
'id', 'title', 'recipe', 'servings', 'note', 'note_markdown',
'date', 'meal_type', 'created_by', 'shared', 'recipe_name',
'from_date', 'to_date', 'meal_type', 'created_by', 'shared', 'recipe_name',
'meal_type_name', 'shopping'
)
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')
@@ -1238,7 +1265,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
class Meta:
model = InviteLink
fields = (
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'reusable', 'created_by', 'created_at',)
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'reusable', 'internal_note', 'created_by', 'created_at',)
read_only_fields = ('id', 'uuid', 'created_by', 'created_at',)
@@ -1290,7 +1317,7 @@ class AccessTokenSerializer(serializers.ModelSerializer):
class KeywordExportSerializer(KeywordSerializer):
class Meta:
model = Keyword
fields = ('name', 'icon', 'description', 'created_at', 'updated_at')
fields = ('name', 'description', 'created_at', 'updated_at')
class NutritionInformationExportSerializer(NutritionInformationSerializer):
@@ -1343,7 +1370,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):
@@ -1355,7 +1382,7 @@ class RecipeExportSerializer(WritableNestedModelSerializer):
model = Recipe
fields = (
'name', 'description', 'keywords', 'steps', 'working_time',
'waiting_time', 'internal', 'nutrition', 'servings', 'servings_text',
'waiting_time', 'internal', 'nutrition', 'servings', 'servings_text', 'source_url',
)
def create(self, validated_data):

View File

@@ -1,4 +1,3 @@
from decimal import Decimal
from functools import wraps
from django.conf import settings
@@ -13,12 +12,11 @@ from django_scopes import scope, scopes_disabled
from cookbook.helper.cache_helper import CacheHelper
from cookbook.helper.shopping_helper import RecipeShoppingEditor
from cookbook.managers import DICTIONARY
from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe,
ShoppingListEntry, Step, UserPreference, SearchPreference, SearchFields, Unit, PropertyType)
from cookbook.models import (Food, MealPlan, PropertyType, Recipe, SearchFields, SearchPreference,
Step, Unit, UserPreference)
SQLITE = True
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql':
SQLITE = False

View File

@@ -51,13 +51,6 @@
inkscape:window-y="54"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<rect
style="fill:#f5f5f6;fill-opacity:1;stroke:#d8dde0;stroke-width:3.77952766;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect817"
width="1717.1526"
height="1092.339"
x="-602.57629"
y="-290.16949" />
<path
d="m 235.62851,202.1526 c -3.38906,-0.31992 -6.54323,1.7722 -7.40937,5.07666 l -3.10593,11.84344 c 39.34747,1.15551 65.965,27.49017 67.63017,66.72064 l 11.9414,-3.32129 c 3.29677,-0.91767 5.34573,-4.14216 4.95356,-7.55606 -4.37894,-38.04972 -35.85985,-69.14953 -74.00983,-72.76339 z m -12.26226,23.57322 -20.94044,79.87325 a 3.3995443,3.4118034 0 0 0 4.19438,4.15688 L 286.10367,287.635 c -0.8955,-36.81001 -25.8122,-61.48823 -62.73742,-61.90076 z m 5.78824,63.9529 a 6.7110067,6.7352075 0 1 1 6.711,-6.73521 6.7110067,6.7352075 0 0 1 -6.711,6.73521 z M 239.221,257.68648 a 6.7110067,6.7352075 0 1 1 6.71101,-6.7352 6.7110067,6.7352075 0 0 1 -6.71101,6.7352 z m 21.81077,21.88943 a 6.7110067,6.7352075 0 1 1 6.71101,-6.73521 6.7110067,6.7352075 0 0 1 -6.71101,6.73521 z"
id="path2"

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

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

@@ -4634,13 +4634,15 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
.card-img-top {
width: 100%;
border-top-left-radius: calc(.25rem - 1px);
border-top-right-radius: calc(.25rem - 1px)
border-top-right-radius: calc(.25rem - 1px);
background-color: #f5f5f6;
}
.card-img-bottom {
width: 100%;
border-bottom-right-radius: calc(.25rem - 1px);
border-bottom-left-radius: calc(.25rem - 1px)
border-bottom-left-radius: calc(.25rem - 1px);
background-color: #f5f5f6;
}
.card-deck {
@@ -10479,4 +10481,4 @@ textarea, input:not([type="submit"]):not([class="multiselect__input"]):not([clas
.ghost {
opacity: 0.5 !important;
background: #b98766 !important;
}
}

10564
cookbook/static/themes/tandoor_dark.min.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,7 @@ from django.utils.html import format_html
from django.utils.translation import gettext as _
from django_tables2.utils import A
from .models import (CookLog, InviteLink, Recipe, RecipeImport,
Storage, Sync, SyncLog, ViewLog)
from .models import CookLog, InviteLink, RecipeImport, Storage, Sync, SyncLog, ViewLog
class StorageTable(tables.Table):

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">
@@ -73,7 +74,7 @@
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-{% nav_color request %}"
<nav class="navbar navbar-expand-lg {% nav_color request %}"
id="id_main_nav"
style="{% sticky_nav request %}">
@@ -81,7 +82,7 @@
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
aria-label="Tandoor">
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="Logo">
<img class="brand-icon" src="{% logo_url request %}" alt="Logo">
</a>
{% endif %}
{% endif %}
@@ -95,7 +96,7 @@
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
aria-label="Tandoor">
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="Logo">
<img class="brand-icon" src="{% logo_url request %}" alt="Logo">
</a>
{% endif %}
{% endif %}
@@ -135,10 +136,10 @@
<i class="fas fa-fw fa-toolbox fa-lg"></i>
</a>
<div class="dropdown-menu dropdown-menu-center dropdown-menu-center-large">
<div class="row m-0 mt-2 mt-md-0">
<div class="row m-0 mt-1 mt-md-0">
<div class="col-4">
<a href="{% url 'list_keyword' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-tags fa-2x"></i>
</div>
@@ -150,7 +151,7 @@
</div>
<div class="col-4">
<a href="{% url 'list_food' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-leaf fa-2x"></i>
</div>
@@ -162,7 +163,7 @@
</div>
<div class="col-4">
<a href="{% url 'list_unit' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-balance-scale fa-2x"></i>
</div>
@@ -173,10 +174,10 @@
</a>
</div>
</div>
<div class="row m-0 mt-2 mt-md-0">
<div class="row m-0 mt-1 mt-md-0">
<div class="col-4">
<a href="{% url 'list_supermarket' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-store-alt fa-2x"></i>
</div>
@@ -188,7 +189,7 @@
</div>
<div class="col-4">
<a href="{% url 'list_supermarket_category' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-cubes fa-2x"></i>
</div>
@@ -200,7 +201,7 @@
</div>
<div class="col-4">
<a href="{% url 'list_automation' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-robot fa-2x"></i>
</div>
@@ -211,10 +212,10 @@
</a>
</div>
</div>
<div class="row m-0 mt-2 mt-md-0">
<div class="row m-0 mt-1 mt-md-0">
<div class="col-4">
<a href="{% url 'list_user_file' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-file fa-2x"></i>
</div>
@@ -226,7 +227,7 @@
</div>
<div class="col-4">
<a href="{% url 'data_batch_edit' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-edit fa-2x"></i>
</div>
@@ -238,7 +239,7 @@
</div>
<div class="col-4">
<a href="{% url 'view_history' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-history fa-2x"></i>
</div>
@@ -249,10 +250,10 @@
</a>
</div>
</div>
<div class="row m-0 mt-2 mt-md-0">
<div class="row m-0 mt-1 mt-md-0">
<div class="col-4">
<a href="{% url 'view_ingredient_editor' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-th-list fa-2x"></i>
</div>
@@ -264,7 +265,7 @@
</div>
<div class="col-4">
<a href="{% url 'view_export' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-file-export fa-2x"></i>
</div>
@@ -276,7 +277,7 @@
</div>
<div class="col-4">
<a href="{% url 'list_property_type' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-database fa-2x"></i>
</div>
@@ -287,10 +288,10 @@
</a>
</div>
</div>
<div class="row m-0 mt-2 mt-md-0">
<div class="row m-0 mt-1 mt-md-0">
<div class="col-4">
<a href="{% url 'list_unit_conversion' %}" class="p-0 p-md-1">
<div class="card p-0 no-gutters border-0">
<div class="card p-1 pt-2 no-gutters border-0">
<div class="card-body text-center p-0 no-gutters">
<i class="fas fa-exchange-alt fa-2x"></i>
</div>

View File

@@ -4,17 +4,14 @@
{% load i18n %}
{% load l10n %}
{% block title %}{% trans 'Export Recipes' %}{% endblock %}
{% block content %}
<div id="app">
<export-view></export-view>
</div>
{% endblock %}
{% block script %}
{% if debug %}
<script src="{% url 'js_reverse' %}"></script>
@@ -23,11 +20,13 @@
{% endif %}
<script type="application/javascript">
window.EXPORT_ID = {{pk}};
{% if pk %}
window.EXPORT_ID = {{ pk }}
{% else %}
window.EXPORT_ID = null
{% endif %}
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}';
</script>
{% render_bundle 'export_view' %}
{% endblock %}

View File

@@ -7,15 +7,12 @@
{% block title %}{% trans 'Export' %}{% endblock %}
{% block content %}
<div id="app">
<export-response-view></export-response-view>
</div>
{% endblock %}
{% block script %}
{% if debug %}
<script src="{% url 'js_reverse' %}"></script>
@@ -24,7 +21,11 @@
{% endif %}
<script type="application/javascript">
window.EXPORT_ID = {{pk}};
{% if pk %}
window.EXPORT_ID = {{ pk }}
{% else %}
window.EXPORT_ID = null
{% endif %}
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
</script>

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

@@ -3,20 +3,19 @@ from gettext import gettext as _
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
from django.urls import NoReverseMatch, reverse
from django_scopes import ScopeError
from markdown.extensions.tables import TableExtension
from rest_framework.authtoken.models import Token
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.helper.mdx_urlize import UrlizeExtension
from cookbook.models import Space, get_model_name
from cookbook.models import get_model_name
from recipes import settings
from recipes.settings import STATIC_URL, PLUGINS
from recipes.settings import PLUGINS, STATIC_URL
register = template.Library()
@@ -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,9 +63,14 @@ def markdown(value):
UrlizeExtension(), MarkdownFormatExtension()
]
)
markdown_attrs['*'] = markdown_attrs['*'] + ['class']
parsed_md = parsed_md[3:] # remove outer paragraph
parsed_md = parsed_md[:len(parsed_md)-4]
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)
@@ -132,6 +144,7 @@ def is_debug():
def markdown_link():
return f"{_('You can use markdown to format this field. See the ')}<a target='_blank' href='{reverse('docs_markdown')}'>{_('docs here')}</a>"
@register.simple_tag
def plugin_dropdown_nav_templates():
templates = []
@@ -140,6 +153,7 @@ def plugin_dropdown_nav_templates():
templates.append(p['nav_dropdown'])
return templates
@register.simple_tag
def plugin_main_nav_templates():
templates = []
@@ -187,7 +201,8 @@ def base_path(request, path_type):
@register.simple_tag
def user_prefs(request):
from cookbook.serializer import UserPreferenceSerializer # putting it with imports caused circular execution
from cookbook.serializer import \
UserPreferenceSerializer # putting it with imports caused circular execution
try:
return UserPreferenceSerializer(request.user.userpreference, context={'request': request}).data
except AttributeError:

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