Compare commits

..

184 Commits

Author SHA1 Message Date
vabene1111
6faabe3759 compiled messages 2021-11-23 18:18:07 +01:00
vabene1111
69acca7de1 Merge branch 'develop' 2021-11-23 18:17:35 +01:00
vabene1111
9d8c08341f fixed doc links 2021-11-23 18:09:04 +01:00
vabene1111
d488559e42 added setting to disable tree fix 2021-11-23 18:02:44 +01:00
vabene1111
85f7740e9b Merge pull request #1081 from RickeyShideler/patch-2
EN Text: Various grammar corrections
2021-11-23 17:04:12 +01:00
vabene1111
72e831afcf Merge pull request #1082 from TomHutter/k8s-install
K8s install
2021-11-23 17:03:01 +01:00
Tom Hutter
cb59f046c0 added comments to 15-secrets.yaml 2021-11-21 20:24:07 +01:00
Tom Hutter
25d505161f Changed k8s installation files for a bit more sophisticated setup. 2021-11-21 20:14:02 +01:00
RickeyShideler
62aa62b90f EN Text: Various grammar corrections 2021-11-21 12:06:54 -06:00
vabene1111
3fe5340592 updated middleware 2021-11-18 16:43:44 +01:00
vabene1111
9233cb9cf9 fixed token auth request space variable 2021-11-18 13:08:29 +01:00
Luka
fd9f6f6dca Translated using Weblate (Slovenian)
Currently translated at 5.5% (28 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/sl/
2021-11-16 06:06:32 +00:00
vabene1111
ecd4ce603c Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2021-11-14 15:33:44 +01:00
vabene1111
695cab29a1 fixed book sharing and added step page 2021-11-14 15:33:25 +01:00
Luka
7b6ca94d49 Translated using Weblate (Slovenian)
Currently translated at 11.5% (24 of 208 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/sl/
2021-11-14 14:06:36 +00:00
Job Putters
35e04f94c6 Translated using Weblate (Dutch)
Currently translated at 100.0% (509 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/nl/
2021-11-14 14:06:36 +00:00
vabene1111
7c4cd02dfa fixed startup failing if fix tree fails 2021-11-14 10:33:33 +01:00
vabene1111
5ae440d5c9 fixed image on url importer 2021-11-13 12:30:23 +01:00
vabene1111
e88010310c Merge pull request #1061 from tuxuser/docs/inline_docker_yaml_proper
Docs: Inline docker yaml files (revises #1058)
2021-11-13 10:55:19 +01:00
tuxuser
b6eba9c5e7 Docs: Fix inline include of yaml files 2021-11-13 10:36:14 +01:00
tuxuser
9d827ac174 Docs: Inline docker-compose.yaml files in the documentation 2021-11-13 10:35:55 +01:00
vabene1111
27679ae8a5 Merge pull request #1060 from TandoorRecipes/revert-1058-docs/inline_docker_yaml
Revert "Docs: Inline docker-compose.yaml files in the documentation"
2021-11-13 09:19:45 +01:00
vabene1111
6cb9a7068e Revert "Docs: Inline docker-compose.yaml files in the documentation" 2021-11-13 09:19:34 +01:00
vabene1111
f41c2ee7bb Merge pull request #1058 from tuxuser/docs/inline_docker_yaml
Docs: Inline docker-compose.yaml files in the documentation
2021-11-13 09:13:56 +01:00
vabene1111
af581bb27c Added translation using Weblate (Slovenian) 2021-11-13 08:08:25 +00:00
vabene1111
885c8982c1 Added translation using Weblate (Slovenian) 2021-11-13 08:07:56 +00:00
Oliver Cervera
64a9f67802 Translated using Weblate (Italian)
Currently translated at 100.0% (208 of 208 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/it/
2021-11-12 20:06:33 +00:00
Henrique Silva
df45e1d523 Translated using Weblate (Portuguese)
Currently translated at 31.4% (160 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/pt/
2021-11-12 20:06:33 +00:00
Oliver Cervera
03d7aa37da Translated using Weblate (Italian)
Currently translated at 95.6% (487 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/it/
2021-11-12 20:06:33 +00:00
A. L
dd3d28ec75 Translated using Weblate (German)
Currently translated at 100.0% (509 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2021-11-12 20:06:29 +00:00
tuxuser
ea377c2f3b Docs: Inline docker-compose.yaml files in the documentation 2021-11-12 14:41:33 +01:00
Flavius Stan
6f0bf886f6 Translated using Weblate (Romanian)
Currently translated at 8.1% (17 of 208 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/ro/
2021-11-11 16:06:30 +00:00
SMunos
16fbd9fe48 Translated using Weblate (French)
Currently translated at 93.2% (194 of 208 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2021-11-11 16:06:30 +00:00
vabene1111
79cdb56f9a added create supersuser to faq 2021-11-10 14:25:51 +01:00
vabene1111
1e6ba924ab fixed plan to eat and mealmaster imports 2021-11-10 08:45:13 +01:00
vabene1111
9ae076e426 fixed png scaling algortihm 2021-11-09 17:59:31 +01:00
vabene1111
f346022d8b improved image compression and added it to import 2021-11-09 17:38:32 +01:00
vabene1111
c9cd5325c4 improved nextcloud importer 2021-11-09 16:38:50 +01:00
vabene1111
ba6c80e04a Added translation using Weblate (Romanian) 2021-11-09 14:27:40 +00:00
vabene1111
be3f860ba1 Added translation using Weblate (Romanian) 2021-11-09 14:27:23 +00:00
Oliver Cervera
1ac4020b3d Translated using Weblate (Italian)
Currently translated at 96.6% (201 of 208 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/it/
2021-11-09 12:06:30 +00:00
Jesse
5da535b8ac Translated using Weblate (Dutch)
Currently translated at 100.0% (208 of 208 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nl/
2021-11-09 12:06:29 +00:00
Maximilian J
b8ed99a59a Translated using Weblate (German)
Currently translated at 100.0% (208 of 208 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2021-11-09 12:06:29 +00:00
Jesse
c199536fca Translated using Weblate (Dutch)
Currently translated at 100.0% (509 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/nl/
2021-11-09 12:06:29 +00:00
Oliver Cervera
d60a9f0379 Translated using Weblate (Italian)
Currently translated at 95.8% (488 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/it/
2021-11-09 12:06:29 +00:00
vabene1111
c5da006f4a Merge branch 'master' into develop 2021-11-08 18:44:29 +01:00
vabene1111
b72919dd42 compiled translations 2021-11-08 18:44:24 +01:00
vabene1111
020b102c5f Merge branch 'develop' 2021-11-08 18:44:03 +01:00
vabene1111
c02ea744ac testing with release bot again 2021-11-08 18:43:48 +01:00
vabene1111
df76791e1e fixed setp headers without ingredients loading in central list 2021-11-08 17:15:37 +01:00
vabene1111
086f8b4d62 made messages 2021-11-08 17:10:03 +01:00
vabene1111
409594a73a fixed rezkonv importer 2021-11-08 17:09:57 +01:00
vabene1111
5af687364f Merge pull request #1016 from StephenBrown2/patch-1
Add time unit hint to recipe edit view
2021-11-08 16:32:44 +01:00
vabene1111
1431be5cad Merge pull request #1020 from PastuDan/develop
Add KubeSail install method
2021-11-08 16:31:29 +01:00
vabene1111
769b53a309 Merge pull request #1038 from rose-a/fix-templating-docs
Fix Templating documentation
2021-11-08 16:28:08 +01:00
vabene1111
fd8752b298 fixed nextcloud image import 2021-11-07 22:19:46 +01:00
Kaibu
301f1cede4 updated translations 2021-11-07 18:17:49 +01:00
Kaibu
3f7aed995a Translated using Weblate (German)
Currently translated at 100.0% (509 of 509 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2021-11-07 17:14:14 +00:00
Kaibu
44d535301d Merge branch 'develop-weblate' into develop
# Conflicts:
#	cookbook/locale/de/LC_MESSAGES/django.po
2021-11-07 17:38:49 +01:00
Kaibu
15e36cc03f Translated using Weblate (German)
Currently translated at 100.0% (208 of 208 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2021-11-07 16:34:53 +00:00
Jens
f6cb5128f5 Translated using Weblate (German)
Currently translated at 97.9% (483 of 493 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2021-11-07 16:34:53 +00:00
Kaibu
83567c25aa Translated using Weblate (German)
Currently translated at 97.9% (483 of 493 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2021-11-07 16:34:53 +00:00
Kaibu
4288d76afd added more translations 2021-11-07 17:32:43 +01:00
Kaibu
195a13f825 hiding action buttons in plan settings view 2021-11-07 14:42:28 +01:00
vabene1111
f0335ebe40 updated translations 2021-11-07 13:48:18 +01:00
vabene1111
7fd2817014 fixed nextcloiud importer 2021-11-07 13:47:54 +01:00
Alexander Rose
0ab21f9941 Fix Templating documentation
Fix bad variable naming in docs
2021-11-06 17:39:23 +01:00
Nicklas Yli-Länttä
699edb6579 Translated using Weblate (Finnish)
Currently translated at 100.0% (208 of 208 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fi/
2021-11-06 14:06:29 +00:00
Nicklas Yli-Länttä
372a2e480e Translated using Weblate (Finnish)
Currently translated at 2.8% (14 of 493 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/fi/
2021-11-06 14:06:29 +00:00
retmas gh
06e54aed4b Translated using Weblate (Polish)
Currently translated at 100.0% (208 of 208 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/
2021-11-06 14:06:29 +00:00
retmas gh
e426cae091 Translated using Weblate (Polish)
Currently translated at 100.0% (362 of 362 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/pl/
2021-11-06 14:06:29 +00:00
Jan Brach
56e44ee3ff Translated using Weblate (German)
Currently translated at 94.7% (467 of 493 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2021-11-06 14:06:29 +00:00
Kaibu
e18737d254 Merge pull request #1025 from staeff/develop
Fix some typos in docs
2021-11-06 00:13:00 +01:00
vabene1111
dda2529f6f fixed plan share space filter 2021-11-05 19:42:41 +01:00
Stephan Klinger
5fdbedc924 Update manual.md 2021-11-05 14:45:54 +01:00
Stephan Klinger
a2e06a3099 Update manual.md 2021-11-05 14:43:33 +01:00
Stephan Klinger
6fadad1a5f Fix typo in manual.md 2021-11-05 14:43:08 +01:00
Stephan Klinger
cbc517b5da Fix typo in contribute.md 2021-11-05 14:41:32 +01:00
vabene1111
fb018ef9e2 Added translation using Weblate (Finnish) 2021-11-05 11:20:18 +00:00
vabene1111
1eac80942c Added translation using Weblate (Finnish) 2021-11-05 11:19:59 +00:00
Dan Pastusek
e611c095bb Update kubesail.md 2021-11-04 17:23:16 -06:00
Dan Pastusek
c418b7bbff Update kubesail.md 2021-11-04 17:23:00 -06:00
Dan Pastusek
c0417f0b5d Update kubesail.md 2021-11-04 17:22:44 -06:00
Dan Pastusek
aef73bc104 Update kubesail.md 2021-11-04 17:03:56 -06:00
Dan Pastusek
ca1ce40048 Create kubesail.md 2021-11-04 16:57:39 -06:00
Dan Pastusek
b2eef1ee30 Update mkdocs.yml 2021-11-04 16:42:53 -06:00
Stephen Brown II
b4f754e7d3 Add time unit hint to recipe edit view 2021-11-04 10:17:58 -06:00
vabene1111
d681e3ced3 disable external recipes in hosted version 2021-11-04 15:56:22 +01:00
vabene1111
afe46b3f67 Merge branch 'develop' 2021-11-04 15:30:39 +01:00
vabene1111
4b5edb4230 use safe version of coa package 2021-11-04 15:20:11 +01:00
vabene1111
11ea61094f remove package lock 2021-11-04 15:18:36 +01:00
vabene1111
e11d24b8c4 updated translations again 2021-11-04 14:57:00 +01:00
Kaibu
04822103c3 Merge branch 'develop_translate_backend' into develop
# Conflicts:
#	cookbook/locale/de/LC_MESSAGES/django.po
2021-11-04 14:55:09 +01:00
Kaibu
8041e092f7 Translated using Weblate (German)
Currently translated at 100.0% (496 of 496 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2021-11-04 13:53:39 +00:00
vabene1111
4c87440691 updated translations 2021-11-04 12:31:32 +01:00
vabene1111
e1056f9bbe Translated using Weblate (German)
Currently translated at 97.5% (484 of 496 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2021-11-04 11:29:56 +00:00
vabene1111
1fad1d2b8f improved deletion of recipes 2021-11-04 12:27:21 +01:00
Kaibu
980dc48eb8 Merge branch 'develop_translate_backend' into develop
# Conflicts:
#	vue/src/locales/de.json
2021-11-04 11:24:18 +01:00
FrenchAnon
835a34708d Translated using Weblate (French)
Currently translated at 94.5% (192 of 203 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2021-11-04 09:06:30 +00:00
Maximilian J
8247c8d2ca Translated using Weblate (German)
Currently translated at 100.0% (203 of 203 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2021-11-04 09:06:29 +00:00
FrenchAnon
3b612f5c8a Translated using Weblate (French)
Currently translated at 88.7% (440 of 496 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/fr/
2021-11-04 09:06:29 +00:00
Kaibu
dd5509f5b4 translating new mealtypes 2021-11-03 23:55:39 +01:00
Kaibu
2215e06506 translation fixes 2021-11-03 21:34:18 +01:00
Kaibu
a40c8c3f83 added password reset from key templates 2021-11-03 20:44:36 +01:00
vabene1111
c2c51d5e1a Merge pull request #1012 from krial057/patch-2
prepTime and workTime of CookbookApp not imported correctly
2021-11-02 13:09:45 +01:00
krial057
c40f6986f1 prepTime and workTime of CookbookApp not imported correctly 2021-11-01 18:01:05 +01:00
vabene1111
4f1f1239c8 Merge branch 'develop' 2021-11-01 16:54:09 +01:00
vabene1111
486e2fca3e release trigger update 2021-11-01 16:53:59 +01:00
vabene1111
5aaf0fb237 Merge branch 'master' of https://github.com/vabene1111/recipes 2021-11-01 15:46:18 +01:00
vabene1111
a12b2ffb21 updated actions repo for organization 2021-11-01 14:58:04 +01:00
vabene1111
2bda4c85b7 Merge pull request #1004 from TandoorRecipes/dependabot/pip/django-3.2.9
Bump django from 3.2.8 to 3.2.9
2021-11-01 14:57:09 +01:00
vabene1111
70774bdb32 Merge pull request #1005 from TandoorRecipes/dependabot/npm_and_yarn/vue/vue/cli-plugin-eslint-4.5.15
Bump @vue/cli-plugin-eslint from 4.5.13 to 4.5.15 in /vue
2021-11-01 14:57:04 +01:00
vabene1111
7e8a3c2b43 Merge pull request #1006 from TandoorRecipes/dependabot/npm_and_yarn/vue/core-js-3.19.0
Bump core-js from 3.18.1 to 3.19.0 in /vue
2021-11-01 14:56:58 +01:00
vabene1111
8450cddc1e Merge pull request #1007 from TandoorRecipes/dependabot/npm_and_yarn/vue/vue/cli-plugin-typescript-4.5.15
Bump @vue/cli-plugin-typescript from 4.5.13 to 4.5.15 in /vue
2021-11-01 14:56:54 +01:00
vabene1111
a036f2d323 Merge pull request #1008 from TandoorRecipes/dependabot/npm_and_yarn/vue/babel/eslint-parser-7.16.0
Bump @babel/eslint-parser from 7.14.4 to 7.16.0 in /vue
2021-11-01 14:56:49 +01:00
vabene1111
4af5e9d18a Merge pull request #1009 from TandoorRecipes/dependabot/npm_and_yarn/vue/typescript-eslint/eslint-plugin-4.33.0
Bump @typescript-eslint/eslint-plugin from 4.26.1 to 4.33.0 in /vue
2021-11-01 14:56:43 +01:00
dependabot[bot]
6bb2b04986 Bump @typescript-eslint/eslint-plugin from 4.26.1 to 4.33.0 in /vue
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.26.1 to 4.33.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.33.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 13:52:11 +00:00
dependabot[bot]
c0bc0bd6c3 Bump @babel/eslint-parser from 7.14.4 to 7.16.0 in /vue
Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.14.4 to 7.16.0.
- [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.16.0/eslint/babel-eslint-parser)

---
updated-dependencies:
- dependency-name: "@babel/eslint-parser"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 13:51:26 +00:00
dependabot[bot]
262238c96c Bump @vue/cli-plugin-typescript from 4.5.13 to 4.5.15 in /vue
Bumps [@vue/cli-plugin-typescript](https://github.com/vuejs/vue-cli/tree/HEAD/packages/@vue/cli-plugin-typescript) from 4.5.13 to 4.5.15.
- [Release notes](https://github.com/vuejs/vue-cli/releases)
- [Changelog](https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue-cli/commits/v4.5.15/packages/@vue/cli-plugin-typescript)

---
updated-dependencies:
- dependency-name: "@vue/cli-plugin-typescript"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 13:51:07 +00:00
dependabot[bot]
3d41065f3b Bump core-js from 3.18.1 to 3.19.0 in /vue
Bumps [core-js](https://github.com/zloirock/core-js) from 3.18.1 to 3.19.0.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/compare/v3.18.1...v3.19.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 13:50:52 +00:00
dependabot[bot]
ff764cbbec Bump @vue/cli-plugin-eslint from 4.5.13 to 4.5.15 in /vue
Bumps [@vue/cli-plugin-eslint](https://github.com/vuejs/vue-cli/tree/HEAD/packages/@vue/cli-plugin-eslint) from 4.5.13 to 4.5.15.
- [Release notes](https://github.com/vuejs/vue-cli/releases)
- [Changelog](https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue-cli/commits/v4.5.15/packages/@vue/cli-plugin-eslint)

---
updated-dependencies:
- dependency-name: "@vue/cli-plugin-eslint"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 13:50:37 +00:00
dependabot[bot]
7d3c7dce75 Bump django from 3.2.8 to 3.2.9
Bumps [django](https://github.com/django/django) from 3.2.8 to 3.2.9.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.8...3.2.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 13:50:19 +00:00
vabene1111
f66bf13ec5 Create FUNDING.yml 2021-11-01 12:51:14 +01:00
vabene1111
3e8bc0a42b another try at fixing the discord release bot 2021-11-01 12:25:53 +01:00
vabene1111
0e55fe162c renamed components to match recommended naming 2021-11-01 11:39:10 +01:00
vabene1111
355f2d30a6 -a 2021-11-01 11:37:55 +01:00
vabene1111
4efed9a1d2 improved html importer instruction line break behavior 2021-11-01 11:27:19 +01:00
vabene1111
e4924f9d27 improved recipe card time pill 2021-11-01 11:24:28 +01:00
vabene1111
77f0bf3628 Merge pull request #1003 from krial057/patch-1
Also show working/waiting time if only one of both is set
2021-11-01 11:21:20 +01:00
vabene1111
9a10f866e1 Merge remote-tracking branch 'origin/develop' into develop 2021-11-01 11:10:45 +01:00
vabene1111
66794d619f improved cookbook app importer 2021-11-01 11:10:35 +01:00
krial057
1a814aa9d1 Also show working/waiting time if only one of both is set 2021-11-01 10:40:03 +01:00
Oliver Cervera
6b4200acc3 Translated using Weblate (Italian)
Currently translated at 99.5% (201 of 202 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/it/
2021-11-01 08:47:19 +00:00
Jesse
7749ad1441 Translated using Weblate (Dutch)
Currently translated at 100.0% (202 of 202 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nl/
2021-11-01 08:47:19 +00:00
Kaibu
3d6d998774 Translated using Weblate (German)
Currently translated at 100.0% (202 of 202 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2021-11-01 08:47:19 +00:00
Kaibu
bcbd5c1103 Translated using Weblate (German)
Currently translated at 95.9% (476 of 496 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/
2021-11-01 08:47:19 +00:00
vabene1111
2d9b21b0d7 Merge pull request #974 from xeals/feature/kilojoules
Add user preference to display/edit using kilojoules
2021-11-01 09:47:14 +01:00
vabene1111
c9bd3ccae8 Merge branch 'develop' into feature/kilojoules 2021-11-01 09:46:04 +01:00
vabene1111
08aa5cc36e Merge pull request #998 from vabene1111/dependabot/pip/django-cors-headers-3.10.0
Bump django-cors-headers from 3.9.0 to 3.10.0
2021-11-01 08:54:53 +01:00
vabene1111
c3cbc2799b Merge pull request #999 from vabene1111/dependabot/pip/icalendar-4.0.9
Bump icalendar from 4.0.7 to 4.0.9
2021-11-01 08:54:48 +01:00
vabene1111
e0af020210 Merge pull request #1000 from vabene1111/dependabot/pip/boto3-1.19.7
Bump boto3 from 1.18.52 to 1.19.7
2021-11-01 08:54:42 +01:00
vabene1111
97c9b304f3 Merge pull request #1001 from vabene1111/dependabot/pip/uritemplate-4.1.1
Bump uritemplate from 3.0.1 to 4.1.1
2021-11-01 08:54:37 +01:00
vabene1111
2aa1df1c92 Merge pull request #1002 from vabene1111/dependabot/pip/pillow-8.4.0
Bump pillow from 8.3.2 to 8.4.0
2021-11-01 08:54:32 +01:00
dependabot[bot]
490e86a346 Bump boto3 from 1.18.52 to 1.19.7
Bumps [boto3](https://github.com/boto/boto3) from 1.18.52 to 1.19.7.
- [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.18.52...1.19.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 07:44:39 +00:00
dependabot[bot]
a495b853ea Bump uritemplate from 3.0.1 to 4.1.1
Bumps [uritemplate](https://github.com/python-hyper/uritemplate) from 3.0.1 to 4.1.1.
- [Release notes](https://github.com/python-hyper/uritemplate/releases)
- [Changelog](https://github.com/python-hyper/uritemplate/blob/main/HISTORY.rst)
- [Commits](https://github.com/python-hyper/uritemplate/compare/3.0.1...4.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 07:44:38 +00:00
vabene1111
0bd6ddae71 Merge remote-tracking branch 'origin/develop' into develop 2021-11-01 08:43:50 +01:00
dependabot[bot]
ef44ab1562 Bump icalendar from 4.0.7 to 4.0.9
Bumps [icalendar](https://github.com/collective/icalendar) from 4.0.7 to 4.0.9.
- [Release notes](https://github.com/collective/icalendar/releases)
- [Changelog](https://github.com/collective/icalendar/blob/master/CHANGES.rst)
- [Commits](https://github.com/collective/icalendar/compare/4.0.7...4.0.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 07:42:51 +00:00
dependabot[bot]
acd6a7fa76 Bump django-cors-headers from 3.9.0 to 3.10.0
Bumps [django-cors-headers](https://github.com/adamchainz/django-cors-headers) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/adamchainz/django-cors-headers/releases)
- [Changelog](https://github.com/adamchainz/django-cors-headers/blob/main/HISTORY.rst)
- [Commits](https://github.com/adamchainz/django-cors-headers/compare/3.9.0...3.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 07:42:35 +00:00
dependabot[bot]
e92c4d7b80 Bump pillow from 8.3.2 to 8.4.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.3.2 to 8.4.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.3.2...8.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 07:42:33 +00:00
vabene1111
1ca50c5932 Merge pull request #990 from vabene1111/dependabot/pip/django-storages-1.12.3
Bump django-storages from 1.11.1 to 1.12.3
2021-11-01 08:42:16 +01:00
vabene1111
61bb8abe0e Merge pull request #996 from vabene1111/dependabot/pip/pyyaml-6.0
Bump pyyaml from 5.4.1 to 6.0
2021-11-01 08:42:02 +01:00
vabene1111
7c4ecc7048 Merge pull request #986 from vabene1111/dependabot/pip/python-dotenv-0.19.1
Bump python-dotenv from 0.19.0 to 0.19.1
2021-11-01 08:29:57 +01:00
dependabot[bot]
8e09645f6c Bump pyyaml from 5.4.1 to 6.0
Bumps [pyyaml](https://github.com/yaml/pyyaml) from 5.4.1 to 6.0.
- [Release notes](https://github.com/yaml/pyyaml/releases)
- [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES)
- [Commits](https://github.com/yaml/pyyaml/compare/5.4.1...6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 07:29:53 +00:00
vabene1111
ebf99c0889 Merge pull request #987 from vabene1111/dependabot/pip/jinja2-3.0.2
Bump jinja2 from 3.0.1 to 3.0.2
2021-11-01 08:29:48 +01:00
vabene1111
eecf646bdd Merge pull request #988 from vabene1111/dependabot/pip/django-tables2-2.4.1
Bump django-tables2 from 2.4.0 to 2.4.1
2021-11-01 08:29:37 +01:00
vabene1111
cacb9dd447 Merge pull request #989 from vabene1111/dependabot/pip/django-3.2.8
Bump django from 3.2.7 to 3.2.8
2021-11-01 08:29:27 +01:00
vabene1111
28612e910a update recipe scrapers 2021-11-01 08:29:11 +01:00
vabene1111
9f5f689c45 Merge pull request #991 from vabene1111/dependabot/npm_and_yarn/vue/typescript-4.4.4
Bump typescript from 4.4.3 to 4.4.4 in /vue
2021-11-01 08:28:51 +01:00
dependabot[bot]
2e046b1bcc Bump typescript from 4.4.3 to 4.4.4 in /vue
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.4.3 to 4.4.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.4.3...v4.4.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 07:26:43 +00:00
vabene1111
07785fae6b Merge pull request #992 from vabene1111/dependabot/npm_and_yarn/vue/eslint-plugin-vue-8.0.3
Bump eslint-plugin-vue from 7.10.0 to 8.0.3 in /vue
2021-11-01 08:26:14 +01:00
vabene1111
dc9cc95860 Merge pull request #993 from vabene1111/dependabot/npm_and_yarn/vue/workbox-webpack-plugin-6.3.0
Bump workbox-webpack-plugin from 6.1.5 to 6.3.0 in /vue
2021-11-01 08:26:03 +01:00
vabene1111
26bcfdda96 Merge pull request #994 from vabene1111/dependabot/npm_and_yarn/vue/vue/compiler-sfc-3.2.20
Bump @vue/compiler-sfc from 3.1.1 to 3.2.20 in /vue
2021-11-01 08:25:53 +01:00
vabene1111
f32a0a092c Merge pull request #995 from vabene1111/dependabot/npm_and_yarn/vue/vue-i18n-8.26.5
Bump vue-i18n from 8.24.4 to 8.26.5 in /vue
2021-11-01 08:25:42 +01:00
dependabot[bot]
74935b22b7 Bump vue-i18n from 8.24.4 to 8.26.5 in /vue
Bumps [vue-i18n](https://github.com/kazupon/vue-i18n) from 8.24.4 to 8.26.5.
- [Release notes](https://github.com/kazupon/vue-i18n/releases)
- [Changelog](https://github.com/kazupon/vue-i18n/blob/v8.x/CHANGELOG.md)
- [Commits](https://github.com/kazupon/vue-i18n/compare/v8.24.4...v8.26.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 00:16:08 +00:00
dependabot[bot]
e84a38a98e Bump @vue/compiler-sfc from 3.1.1 to 3.2.20 in /vue
Bumps [@vue/compiler-sfc](https://github.com/vuejs/vue-next/tree/HEAD/packages/compiler-sfc) from 3.1.1 to 3.2.20.
- [Release notes](https://github.com/vuejs/vue-next/releases)
- [Changelog](https://github.com/vuejs/vue-next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue-next/commits/v3.2.20/packages/compiler-sfc)

---
updated-dependencies:
- dependency-name: "@vue/compiler-sfc"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 00:15:46 +00:00
dependabot[bot]
890def60d3 Bump workbox-webpack-plugin from 6.1.5 to 6.3.0 in /vue
Bumps [workbox-webpack-plugin](https://github.com/googlechrome/workbox) from 6.1.5 to 6.3.0.
- [Release notes](https://github.com/googlechrome/workbox/releases)
- [Commits](https://github.com/googlechrome/workbox/compare/v6.1.5...v6.3.0)

---
updated-dependencies:
- dependency-name: workbox-webpack-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 00:15:29 +00:00
dependabot[bot]
0ef69ada91 Bump eslint-plugin-vue from 7.10.0 to 8.0.3 in /vue
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 7.10.0 to 8.0.3.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v7.10.0...v8.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 00:14:53 +00:00
dependabot[bot]
4ea77882fc Bump django-storages from 1.11.1 to 1.12.3
Bumps [django-storages](https://github.com/jschneier/django-storages) from 1.11.1 to 1.12.3.
- [Release notes](https://github.com/jschneier/django-storages/releases)
- [Changelog](https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jschneier/django-storages/compare/1.11.1...1.12.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 00:03:42 +00:00
dependabot[bot]
590e56f003 Bump django from 3.2.7 to 3.2.8
Bumps [django](https://github.com/django/django) from 3.2.7 to 3.2.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.7...3.2.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 00:03:37 +00:00
dependabot[bot]
47135e929d Bump django-tables2 from 2.4.0 to 2.4.1
Bumps [django-tables2](https://github.com/jieter/django-tables2) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/jieter/django-tables2/releases)
- [Changelog](https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jieter/django-tables2/compare/v2.4.0...v2.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 00:03:32 +00:00
dependabot[bot]
8d102727ff Bump jinja2 from 3.0.1 to 3.0.2
Bumps [jinja2](https://github.com/pallets/jinja) from 3.0.1 to 3.0.2.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.0.1...3.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 00:03:28 +00:00
dependabot[bot]
4a85081d18 Bump python-dotenv from 0.19.0 to 0.19.1
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 0.19.0 to 0.19.1.
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v0.19.0...v0.19.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-01 00:03:23 +00:00
Kaibu
e7ce582959 readme update 2021-10-31 18:11:48 +01:00
Kaibu
31720927b1 deprecated old meal plan 2021-10-31 18:08:37 +01:00
vabene1111
b6845e06a5 Merge pull request #983 from lostlont/develop
Added LDAP usage in documentation
2021-10-31 15:23:36 +01:00
lostlont
831b7c391d Merge branch 'vabene1111:develop' into develop 2021-10-31 14:16:16 +01:00
Perjéssy, Lóránt
4eaf0df9a3 Added LDAP usage description in authentication page of documentation. 2021-10-31 14:10:57 +01:00
vabene1111
f720c5c094 fixed development server webpack path 2021-10-31 13:21:25 +01:00
xeals
34d9f5a1d8 Correct capitalization for KJ in preference form 2021-10-25 17:46:05 +11:00
xeals
6dfd3ae1d7 Localize nutritional energy headings 2021-10-25 17:39:04 +11:00
xeals
15eec9d373 Set calories heading based on user preference 2021-10-25 17:38:41 +11:00
xeals
c9ff0543e3 Convert nutrition input from KJ to calories with preference 2021-10-25 17:37:31 +11:00
xeals
3d830a4449 Display nutritional energy in KJ with preference 2021-10-25 17:28:52 +11:00
xeals
b1b770c9e5 Add KJ user preference 2021-10-25 17:25:35 +11:00
169 changed files with 18686 additions and 35994 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [vabene1111]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -4,7 +4,7 @@ on: [push]
jobs:
build:
if: github.repository_owner == 'vabene1111'
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
strategy:
max-parallel: 4

View File

@@ -8,7 +8,7 @@ on:
jobs:
CodeQL-Build:
if: github.repository_owner == 'vabene1111'
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- name: Checkout repository

View File

@@ -5,7 +5,7 @@ on:
- 'beta'
jobs:
build:
if: github.repository_owner == 'vabene1111'
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master

View File

@@ -7,7 +7,7 @@ on:
- '!master'
jobs:
build:
if: github.repository_owner == 'vabene1111'
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master

View File

@@ -6,7 +6,7 @@ on:
jobs:
build:
if: github.repository_owner == 'vabene1111'
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master

View File

@@ -1,13 +1,12 @@
name: publish tagged release docker
on:
push:
tags:
- '*'
release:
types: [published]
jobs:
build:
if: github.repository_owner == 'vabene1111'
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
name: Build image job
steps:
@@ -50,4 +49,4 @@ jobs:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2
with:
args: '🚀 A new Version of tandoor has been released 🥳 \n https://github.com/vabene1111/recipes/releases/tag/{{GITHUB_REF/refs\/tags\//}}'
args: '🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of tandoor has been released 🥳 \nCheck it out https://github.com/vabene1111/recipes/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}'

View File

@@ -7,12 +7,12 @@ on:
jobs:
deploy:
if: github.repository_owner == 'vabene1111'
if: github.repository_owner == 'TandoorRecipes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: pip install mkdocs-material mkdocs-include-markdown-plugin
- run: mkdocs gh-deploy --force

View File

@@ -1,6 +1,6 @@
<h1 align="center">
<br>
<a href="https://app.tandoor.dev"><img src="https://github.com/vabene1111/recipes/raw/develop/docs/logo_color.svg" height="256px" width="256px"></a>
<a href="https://tandoor.dev"><img src="https://github.com/vabene1111/recipes/raw/develop/docs/logo_color.svg" height="256px" width="256px"></a>
<br>
Tandoor Recipes
<br>
@@ -15,46 +15,74 @@
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
<a href="https://app.tandoor.dev/accounts/login/?demo" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
</p>
<p align="center">
<a href="https://tandoor.dev" target="_blank" rel="noopener noreferrer">Website</a> •
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Documentation</a> •
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Docs</a> •
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a> •
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord server</a>
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord</a>
</p>
![Preview](docs/preview.png)
# Your Feedback
## Core Features
- 🥗 **Manage your recipes** - Manage your ever growing recipe collection
- 📆 **Plan** - multiple meals for each day
- 🛒 **Shopping lists** - via the meal plan or straight from recipes
- 📚 **Cookbooks** - collect recipes into books
- 👪 **Share and collaborate** on recipes with friends and family
Share some information on how you use Tandoor to help me improve the application [Google Survey](https://forms.gle/qNfLK2tWTeWHe9Qd7)
## Made by and for power users
## Features
- 📦 **Sync** files with Dropbox and Nextcloud (more can easily be added)
- 🔍 Powerful **search** with Djangos [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
- 🔍 Powerful & customizable **search** with fulltext support and [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
- 🏷️ Create and search for **tags**, assign them in batch to all files matching certain filters
- 📄 **Create recipes** locally within a nice, standardized web interface
- **Import recipes** from thousands of websites supporting [ld+json or microdata](https://schema.org/Recipe)
- 📱 Optimized for use on **mobile** devices like phones and tablets
- 🛒 Generate **shopping** lists from recipes
- 📆 Create a **Plan** on what to eat when
- 👪 **Share** recipes with friends and comment on them to suggest or remember changes you made
- ➗ automatically convert decimal units to **fractions** for those who like this
- 🐳 Easy setup with **Docker** and included examples for Kubernetes, Unraid and Synology
- ↔️ Quickly merge and rename ingredients, tags and units
- 📥 **Import recipes** from thousands of websites supporting [ld+json or microdata](https://schema.org/Recipe)
- ➗ Support for **fractions** or decimals
- 🐳 Easy setup with **Docker** and included examples for **Kubernetes**, **Unraid** and **Synology**
- 🎨 Customize your interface with **themes**
- ✉️ Export and import recipes from other users
- 📦 **Sync** files with Dropbox and Nextcloud
## All the must haves
- 📱Optimized for use on **mobile** devices
- 🌍 localized in many languages thanks to the awesome community
- Many more like recipe scaling, image compression, cookbooks, printing views, ...
- 📥️ **Import your collection** from many other [recipe managers](https://docs.tandoor.dev/features/import_export/)
- Many more like recipe scaling, image compression, printing views and supermarkets
This application is meant for people with a collection of recipes they want to share with family and friends or simply
store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as
a public page.
## Docs
Documentation can be found [here](https://docs.tandoor.dev/).
While this application has been around for a while and is actively used by many (including myself), it is still considered
**beta** software that has a lot of rough edges and unpolished parts.
## Contributing
You can help out with the ongoing development by looking for potential bugs in our code base, or by contributing new features. We are always welcoming new pull requests containing bug fixes, refactors and new features. We have a list of tasks and bugs on our issue tracker on Github. Please comment on issues if you want to contribute with, to avoid duplicating effort.
## Your Feedback
Share some information on how you use Tandoor to help me improve the application [Google Survey](https://forms.gle/qNfLK2tWTeWHe9Qd7)
## Get in touch
<table>
<tr>
<td><a href="https://discord.gg/RhzBrfWgtp">Discord</a></td>
<td>We have a public Discord server that anyone can join. This is where all our developers and contributors hang out and where we make announcements</td>
</tr>
<tr>
<td><a href="https://twitter.com/TandoorRecipes">Twitter</a></td>
<td>You can follow our Twitter account to get updates on new features or releases</td>
</tr>
</table>
## License
Beginning with version 0.10.0 the code in this repository is licensed under the [GNU AGPL v3](https://www.gnu.org/licenses/agpl-3.0.de.html) license with a

View File

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

View File

@@ -36,15 +36,36 @@ class DateWidget(forms.DateInput):
class UserPreferenceForm(forms.ModelForm):
prefix = 'preference'
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['plan_share'].queryset = User.objects.filter(userpreference__space=space).all()
class Meta:
model = UserPreference
fields = (
'default_unit', 'use_fractions', 'theme', 'nav_color',
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
'sticky_navbar', 'default_page', 'show_recent', 'search_style',
'plan_share', 'ingredient_decimals', 'shopping_auto_sync',
'comments'
)
labels = {
'default_unit': _('Default unit'),
'use_fractions': _('Use fractions'),
'use_kj': _('Use KJ'),
'theme': _('Theme'),
'nav_color': _('Navbar color'),
'sticky_navbar': _('Sticky navbar'),
'default_page': _('Default page'),
'show_recent': _('Show recent recipes'),
'search_style': _('Search style'),
'plan_share': _('Plan sharing'),
'ingredient_decimals': _('Ingredient decimal places'),
'shopping_auto_sync': _('Shopping list auto sync period'),
'comments': _('Comments')
}
help_texts = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
# noqa: E501
@@ -52,6 +73,7 @@ class UserPreferenceForm(forms.ModelForm):
'use_fractions': _(
'Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'),
# noqa: E501
'use_kj': _('Display nutritional energy amounts in joules instead of calories'), # noqa: E501
'plan_share': _(
'Users with whom newly created meal plan/shopping list entries should be shared by default.'),
# noqa: E501
@@ -232,6 +254,12 @@ class SyncForm(forms.ModelForm):
'storage': SafeModelChoiceField,
}
labels = {
'storage': _('Storage'),
'path': _('Path'),
'active': _('Active')
}
class BatchEditForm(forms.Form):
search = forms.CharField(label=_('Search String'))
@@ -320,7 +348,8 @@ class InviteLinkForm(forms.ModelForm):
def clean(self):
space = self.cleaned_data['space']
if space.max_users != 0 and (UserPreference.objects.filter(space=space).count() + InviteLink.objects.filter(space=space).count()) >= space.max_users:
if space.max_users != 0 and (UserPreference.objects.filter(space=space).count() + InviteLink.objects.filter(
space=space).count()) >= space.max_users:
raise ValidationError(_('Maximum number of users for this space reached.'))
def clean_email(self):
@@ -335,7 +364,7 @@ class InviteLinkForm(forms.ModelForm):
model = InviteLink
fields = ('email', 'group', 'valid_until', 'space')
help_texts = {
'email': _('An email address is not required but if present the invite link will be send to the user.'),
'email': _('An email address is not required but if present the invite link will be sent to the user.'),
}
field_classes = {
'space': SafeModelChoiceField,
@@ -390,22 +419,31 @@ class UserCreateForm(forms.Form):
class SearchPreferenceForm(forms.ModelForm):
prefix = 'search'
trigram_threshold = forms.DecimalField(min_value=0.01, max_value=1, decimal_places=2, widget=NumberInput(attrs={'class': "form-control-range", 'type': 'range'}),
help_text=_('Determines how fuzzy a search is if it uses trigram similarity matching (e.g. low values mean more typos are ignored).'))
preset = forms.CharField(widget=forms.HiddenInput(),required=False)
trigram_threshold = forms.DecimalField(min_value=0.01, max_value=1, decimal_places=2,
widget=NumberInput(attrs={'class': "form-control-range", 'type': 'range'}),
help_text=_(
'Determines how fuzzy a search is if it uses trigram similarity matching (e.g. low values mean more typos are ignored).'))
preset = forms.CharField(widget=forms.HiddenInput(), required=False)
class Meta:
model = SearchPreference
fields = ('search', 'lookup', 'unaccent', 'icontains', 'istartswith', 'trigram', 'fulltext', 'trigram_threshold')
fields = (
'search', 'lookup', 'unaccent', 'icontains', 'istartswith', 'trigram', 'fulltext', 'trigram_threshold')
help_texts = {
'search': _('Select type method of search. Click <a href="/docs/search/">here</a> for full desciption of choices.'),
'search': _(
'Select type method of search. Click <a href="/docs/search/">here</a> for full desciption of choices.'),
'lookup': _('Use fuzzy matching on units, keywords and ingredients when editing and importing recipes.'),
'unaccent': _('Fields to search ignoring accents. Selecting this option can improve or degrade search quality depending on language'),
'icontains': _("Fields to search for partial matches. (e.g. searching for 'Pie' will return 'pie' and 'piece' and 'soapie')"),
'istartswith': _("Fields to search for beginning of word matches. (e.g. searching for 'sa' will return 'salad' and 'sandwich')"),
'trigram': _("Fields to 'fuzzy' search. (e.g. searching for 'recpie' will find 'recipe'.) Note: this option will conflict with 'web' and 'raw' methods of search."),
'fulltext': _("Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods only function with fulltext fields."),
'unaccent': _(
'Fields to search ignoring accents. Selecting this option can improve or degrade search quality depending on language'),
'icontains': _(
"Fields to search for partial matches. (e.g. searching for 'Pie' will return 'pie' and 'piece' and 'soapie')"),
'istartswith': _(
"Fields to search for beginning of word matches. (e.g. searching for 'sa' will return 'salad' and 'sandwich')"),
'trigram': _(
"Fields to 'fuzzy' search. (e.g. searching for 'recpie' will find 'recipe'.) Note: this option will conflict with 'web' and 'raw' methods of search."),
'fulltext': _(
"Fields to full text search. Note: 'web', 'phrase', and 'raw' search methods only function with fulltext fields."),
}
labels = {

View File

@@ -5,7 +5,7 @@ from PIL import Image
from io import BytesIO
def rescale_image_jpeg(image_object, base_width=720):
def rescale_image_jpeg(image_object, base_width=1020):
img = Image.open(image_object)
icc_profile = img.info.get('icc_profile') # remember color profile to not mess up colors
width_percent = (base_width / float(img.size[0]))
@@ -13,20 +13,20 @@ def rescale_image_jpeg(image_object, base_width=720):
img = img.resize((base_width, height), Image.ANTIALIAS)
img_bytes = BytesIO()
img.save(img_bytes, 'JPEG', quality=75, optimize=True, icc_profile=icc_profile)
img.save(img_bytes, 'JPEG', quality=90, optimize=True, icc_profile=icc_profile)
return img_bytes
def rescale_image_png(image_object, base_width=720):
basewidth = 720
wpercent = (basewidth / float(image_object.size[0]))
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((basewidth, hsize), Image.ANTIALIAS)
img = image_object.resize((base_width, hsize), Image.ANTIALIAS)
im_io = BytesIO()
img.save(im_io, 'PNG', quality=70)
return img
img.save(im_io, 'PNG', quality=90)
return im_io
def get_filetype(name):
@@ -36,9 +36,11 @@ def get_filetype(name):
return '.jpeg'
# TODO this whole file needs proper documentation, refactoring, and testing
# TODO also add env variable to define which images sizes should be compressed
def handle_image(request, image_object, filetype='.jpeg'):
if sys.getsizeof(image_object) / 8 > 500:
if filetype == '.jpeg':
if (image_object.size / 1000) > 500: # if larger than 500 kb compress
if filetype == '.jpeg' or filetype == '.jpg':
return rescale_image_jpeg(image_object), filetype
if filetype == '.png':
return rescale_image_png(image_object), filetype

View File

@@ -38,6 +38,7 @@ def search_recipes(request, queryset, params):
search_keywords = params.getlist('keywords', [])
search_foods = params.getlist('foods', [])
search_books = params.getlist('books', [])
search_steps = params.getlist('steps', [])
search_units = params.get('units', None)
# TODO I think default behavior should be 'AND' which is how most sites operate with facet/filters based on results
@@ -191,6 +192,10 @@ def search_recipes(request, queryset, params):
if search_units:
queryset = queryset.filter(steps__ingredients__unit__id=search_units)
# probably only useful in Unit list view, so keeping it simple
if search_steps:
queryset = queryset.filter(steps__id__in=search_steps)
if search_internal:
queryset = queryset.filter(internal=True)

View File

@@ -246,7 +246,10 @@ def parse_instructions(instructions):
instruction_text += str(i)
instructions = instruction_text
return normalize_string(instructions)
normalized_string = normalize_string(instructions)
normalized_string = normalized_string.replace('\n', ' \n')
normalized_string = normalized_string.replace(' \n \n', '\n\n')
return normalized_string
def parse_image(image):

View File

@@ -1,5 +1,8 @@
from django.urls import reverse
from django_scopes import scope, scopes_disabled
from rest_framework.authentication import TokenAuthentication
from rest_framework.authtoken.models import Token
from rest_framework.exceptions import AuthenticationFailed
from cookbook.views import views
@@ -33,6 +36,15 @@ class ScopeMiddleware:
with scope(space=request.space):
return self.get_response(request)
else:
if request.path.startswith('/api/'):
try:
if auth := TokenAuthentication().authenticate(request):
request.space = auth[0].userpreference.space
with scope(space=request.space):
return self.get_response(request)
except AuthenticationFailed:
pass
with scopes_disabled():
request.space = None
return self.get_response(request)

View File

@@ -4,9 +4,12 @@ import json
import re
from io import BytesIO
import requests
import yaml
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_url_import import iso_duration_to_minutes
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from gettext import gettext as _
@@ -15,53 +18,51 @@ from gettext import gettext as _
class CookBookApp(Integration):
def import_file_name_filter(self, zip_info_object):
return zip_info_object.filename.endswith('.yml')
return zip_info_object.filename.endswith('.html')
def get_recipe_from_file(self, file):
recipe_yml = yaml.safe_load(file.getvalue().decode("utf-8"))
recipe_html = file.getvalue().decode("utf-8")
recipe_json, recipe_tree, html_data, images = get_recipe_from_source(recipe_html, 'CookBookApp', self.request)
recipe = Recipe.objects.create(
name=recipe_yml['name'].strip(),
name=recipe_json['name'].strip(),
created_by=self.request.user, internal=True,
space=self.request.space)
try:
recipe.servings = re.findall('([0-9])+', recipe_yml['recipeYield'])[0]
recipe.servings = re.findall('([0-9])+', recipe_json['recipeYield'])[0]
except Exception as e:
pass
try:
recipe.working_time = recipe_yml['prep_time'].replace(' minutes', '')
recipe.waiting_time = recipe_yml['cook_time'].replace(' minutes', '')
recipe.working_time = iso_duration_to_minutes(recipe_json['prepTime'])
recipe.waiting_time = iso_duration_to_minutes(recipe_json['cookTime'])
except Exception:
pass
if recipe_yml['on_favorites']:
recipe.keywords.add(Keyword.objects.get_or_create(name=_('Favorites'), space=self.request.space))
step = Step.objects.create(instruction=recipe_json['recipeInstructions'], space=self.request.space, )
step = Step.objects.create(instruction=recipe_yml['directions'], space=self.request.space, )
if 'notes' in recipe_yml and recipe_yml['notes'].strip() != '':
step.instruction = step.instruction + '\n\n' + recipe_yml['notes']
if 'nutritional_info' in recipe_yml:
step.instruction = step.instruction + '\n\n' + recipe_yml['nutritional_info']
if 'source' in recipe_yml and recipe_yml['source'].strip() != '':
step.instruction = step.instruction + '\n\n' + recipe_yml['source']
if 'nutrition' in recipe_json:
step.instruction = step.instruction + '\n\n' + recipe_json['nutrition']
step.save()
recipe.steps.add(step)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in recipe_yml['ingredients'].split('\n'):
if ingredient.strip() != '':
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
for ingredient in recipe_json['recipeIngredient']:
f = ingredient_parser.get_food(ingredient['ingredient']['text'])
u = ingredient_parser.get_unit(ingredient['unit']['text'])
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=ingredient['amount'], note=ingredient['note'], space=self.request.space,
))
if len(images) > 0:
try:
response = requests.get(images[0])
self.import_recipe_image(recipe, BytesIO(response.content))
except Exception as e:
print('failed to import image ', str(e))
recipe.save()
return recipe

View File

@@ -14,7 +14,7 @@ from django.utils.translation import gettext as _
from django_scopes import scope
from cookbook.forms import ImportExportBase
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.image_processing import get_filetype, handle_image
from cookbook.models import Keyword, Recipe
from recipes.settings import DATABASES, DEBUG
@@ -52,7 +52,7 @@ class Integration:
icon=icon,
space=request.space
)
except IntegrityError: # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
except IntegrityError: # 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,
@@ -123,8 +123,6 @@ class Integration:
:return: HttpResponseRedirect to the recipe search showing all imported recipes
"""
with scope(space=self.request.space):
self.keyword.name = _('Import') + ' ' + str(il.pk)
self.keyword.save()
try:
self.files = files
@@ -231,15 +229,14 @@ class Integration:
self.ignored_recipes.append(recipe.name)
recipe.delete()
@staticmethod
def import_recipe_image(recipe, image_file, filetype='.jpeg'):
def import_recipe_image(self, recipe, image_file, filetype='.jpeg'):
"""
Adds an image to a recipe naming it correctly
:param recipe: Recipe object
:param image_file: ByteIO stream containing the image
:param filetype: type of file to write bytes to, default to .jpeg if unknown
"""
recipe.image = File(image_file, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}')
recipe.image = File(handle_image(self.request, File(image_file, name='image'), filetype=filetype)[0], name=f'{uuid.uuid4()}_{recipe.pk}{filetype}')
recipe.save()
def get_recipe_from_file(self, file):
@@ -269,7 +266,8 @@ class Integration:
"""
raise NotImplementedError('Method not implemented in integration')
def handle_exception(self, exception, log=None, message=''):
@staticmethod
def handle_exception(exception, log=None, message=''):
if log:
if message:
log.msg += message

View File

@@ -63,7 +63,7 @@ class MealMaster(Integration):
current_recipe = ''
for fl in file.readlines():
line = fl.decode("ANSI")
line = fl.decode("windows-1250")
if (line.startswith('MMMMM') or line.startswith('-----')) and 'meal-master' in line.lower():
if current_recipe != '':
recipe_list.append(current_recipe)

View File

@@ -5,8 +5,9 @@ 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 iso_duration_to_minutes
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
from cookbook.models import Recipe, Step, Ingredient, Keyword
class NextcloudCookbook(Integration):
@@ -24,9 +25,24 @@ class NextcloudCookbook(Integration):
created_by=self.request.user, internal=True,
servings=recipe_json['recipeYield'], space=self.request.space)
# TODO parse times (given in PT2H3M )
# @vabene check recipe_url_import.iso_duration_to_minutes I think it does what you are looking for
# TODO parse keywords
try:
recipe.working_time = iso_duration_to_minutes(recipe_json['prepTime'])
recipe.waiting_time = iso_duration_to_minutes(recipe_json['cookTime'])
except Exception:
pass
if 'recipeCategory' in recipe_json:
try:
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=recipe_json['recipeCategory'])[0])
except Exception:
pass
if 'keywords' in recipe_json:
try:
for x in recipe_json['keywords'].split(','):
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=x)[0])
except Exception:
pass
ingredients_added = False
for s in recipe_json['recipeInstructions']:
@@ -41,19 +57,28 @@ class NextcloudCookbook(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in recipe_json['recipeIngredient']:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
recipe.steps.add(step)
if 'nutrition' in recipe_json:
try:
recipe.nutrition.calories = recipe_json['nutrition']['calories'].replace(' kcal', '').replace(' ', '')
recipe.nutrition.proteins = recipe_json['nutrition']['calories'].replace(' g', '').replace(',', '.').replace(' ', '')
recipe.nutrition.fats = recipe_json['nutrition']['calories'].replace(' g', '').replace(',', '.').replace(' ', '')
recipe.nutrition.carbohydrates = recipe_json['nutrition']['calories'].replace(' g', '').replace(',', '.').replace(' ', '')
except Exception:
pass
for f in self.files:
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
for z in import_zip.filelist:
if re.match(f'^Recipes/{recipe.name}/full.jpg$', z.filename):
if re.match(f'^(.)+{recipe.name}/full.jpg$', z.filename):
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename))
return recipe

View File

@@ -78,7 +78,7 @@ class Plantoeat(Integration):
current_recipe = ''
for fl in file.readlines():
line = fl.decode("ANSI")
line = fl.decode("windows-1250")
if line.startswith('--------------'):
if current_recipe != '':
recipe_list.append(current_recipe)

View File

@@ -62,7 +62,7 @@ class RezKonv(Integration):
current_recipe = ''
for fl in file.readlines():
line = fl.decode("ANSI")
line = fl.decode("windows-1250")
if line.startswith('=====') and 'rezkonv' in line.lower():
if current_recipe != '':
recipe_list.append(current_recipe)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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: 2021-10-23 09:06+0000\n"
"Last-Translator: Tomasz Klimczak <klemensble@gmail.com>\n"
"PO-Revision-Date: 2021-11-06 14:06+0000\n"
"Last-Translator: retmas gh <tandoor@oppai.ovh>\n"
"Language-Team: Polish <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/pl/>\n"
"Language: pl\n"
@@ -385,7 +385,7 @@ msgstr "Zabierz mnie na stronę główną"
#: .\cookbook\templates\404.html:35
msgid "Report a Bug"
msgstr "Raprtuj błąd"
msgstr "Raportuj błąd"
#: .\cookbook\templates\account\login.html:7
#: .\cookbook\templates\base.html:166

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.7 on 2021-10-25 05:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0157_alter_searchpreference_trigram'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='use_kj',
field=models.BooleanField(default=False),
),
]

View File

@@ -19,7 +19,8 @@ from treebeard.mp_tree import MP_Node, MP_NodeManager
from django_scopes import ScopedManager, scopes_disabled
from django_prometheus.models import ExportModelOperationsMixin
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT,
STICKY_NAV_PREF_DEFAULT, SORT_TREE_BY_NAME)
KJ_PREF_DEFAULT, STICKY_NAV_PREF_DEFAULT,
SORT_TREE_BY_NAME)
def get_user_name(self):
@@ -217,6 +218,7 @@ class UserPreference(models.Model, PermissionModelMixin):
)
default_unit = models.CharField(max_length=32, default='g')
use_fractions = models.BooleanField(default=FRACTION_PREF_DEFAULT)
use_kj = models.BooleanField(default=KJ_PREF_DEFAULT)
default_page = models.CharField(
choices=PAGES, max_length=64, default=SEARCH
)
@@ -456,6 +458,9 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
from cookbook.helper.template_helper import render_instructions
return render_instructions(self)
def __str__(self):
return f'{self.pk} {self.name}'
class Meta:
ordering = ['order', 'pk']
indexes = (GinIndex(fields=["search_vector"]),)

View File

@@ -39,6 +39,11 @@ class RecipeSchema(AutoSchema):
"description": 'Id of book a recipe should have. For multiple repeat parameter.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'steps', "in": "query", "required": False,
"description": 'Id of a step a recipe should have. For multiple repeat parameter.',
'schema': {'type': 'string', },
})
parameters.append({
"name": 'keywords_or', "in": "query", "required": False,
"description": 'If recipe should have all (AND) or any (OR) of the provided keywords.',
@@ -86,7 +91,8 @@ class TreeSchema(AutoSchema):
})
parameters.append({
"name": 'root', "in": "query", "required": False,
"description": 'Return first level children of {obj} with ID [int]. Integer 0 will return root {obj}s.'.format(obj=api_name),
"description": 'Return first level children of {obj} with ID [int]. Integer 0 will return root {obj}s.'.format(
obj=api_name),
'schema': {'type': 'int', },
})
parameters.append({
@@ -110,3 +116,17 @@ 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

@@ -34,7 +34,8 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
api_serializer = None
# extended values are computationally expensive and not needed in normal circumstances
try:
if bool(int(self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
if bool(int(
self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
return fields
except AttributeError:
pass
@@ -49,11 +50,13 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
def get_image(self, obj):
# TODO add caching
recipes = Recipe.objects.filter(**{self.recipe_filter: obj}, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
recipes = Recipe.objects.filter(**{self.recipe_filter: obj}, space=obj.space).exclude(
image__isnull=True).exclude(image__exact='')
try:
if recipes.count() == 0 and obj.has_children():
obj__in = self.recipe_filter + '__in'
recipes = Recipe.objects.filter(**{obj__in: obj.get_descendants()}, space=obj.space).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
recipes = Recipe.objects.filter(**{obj__in: obj.get_descendants()}, space=obj.space).exclude(
image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
except AttributeError:
# probably not a tree
pass
@@ -404,7 +407,10 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
class Meta:
model = Food
fields = ('id', 'name', 'description', 'recipe', 'ignore_shopping', 'supermarket_category', 'image', 'parent', 'numchild', 'numrecipe')
fields = (
'id', 'name', 'description', 'recipe', 'ignore_shopping', 'supermarket_category', 'image', 'parent',
'numchild',
'numrecipe')
read_only_fields = ('id', 'numchild', 'parent', 'image')
@@ -425,12 +431,13 @@ class IngredientSerializer(WritableNestedModelSerializer):
)
class StepSerializer(WritableNestedModelSerializer):
class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
ingredients = IngredientSerializer(many=True)
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
ingredients_vue = serializers.SerializerMethodField('get_ingredients_vue')
file = UserFileViewSerializer(allow_null=True, required=False)
step_recipe_data = serializers.SerializerMethodField('get_step_recipe_data')
recipe_filter = 'steps'
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
@@ -442,6 +449,9 @@ class StepSerializer(WritableNestedModelSerializer):
def get_ingredients_markdown(self, obj):
return obj.get_instruction_render()
def get_step_recipes(self, obj):
return list(obj.recipe_set.values_list('id', flat=True).all())
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
@@ -452,7 +462,7 @@ class StepSerializer(WritableNestedModelSerializer):
model = Step
fields = (
'id', 'name', 'type', 'instruction', 'ingredients', 'ingredients_markdown',
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe', 'step_recipe_data'
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe', 'step_recipe_data', 'numrecipe'
)

View File

@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load i18n %}
{% load account %}
{% block head_title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-12" style="text-align: center">
<h3>{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}</h3>
{% if user.is_authenticated %}
{% include "account/snippets/already_logged_in.html" %}
{% endif %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
<hr>
{% if token_fail %}
{% url 'account_reset_password' as passwd_reset_url %}
<p>{% blocktrans %}The password reset link was invalid, possibly because it has already been used.
Please request a <a href="{{ passwd_reset_url }}">new password reset</a>.{% endblocktrans %}</p>
{% else %}
{% if form %}
<form method="POST" action="{{ action_url }}">
{% csrf_token %}
{{ form | crispy }}
<input type="submit" class="btn btn-warning float-right" name="action"
value="{% trans 'change password' %}"/>
</form>
{% else %}
<p>{% trans 'Your password is now changed.' %}</p>
{% endif %}
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% load i18n %}
{% load account %}
{% block title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12 col-lg-6 col-md-6 offset-lg-3 offset-md-3">
<hr>
<h3>{% trans "Change Password" %}</h3>
{% if user.is_authenticated %}
{% include "account/snippets/already_logged_in.html" %}
{% endif %}
<p>{% trans 'Your password is now changed.' %}</p>
</div>
</div>
{% endblock %}

View File

@@ -17,7 +17,7 @@
</p>
<form method="POST" class="post-form">{% csrf_token %}
{{ form|crispy }}
<input type="submit" value="Submit" class="btn btn-success">
<input type="submit" value="{% trans 'Save' %}" class="btn btn-success">
<a href="{% url 'list_storage' %}"><button type="button" class="btn btn-primary">{% trans 'Manage External Storage' %}</button></a>
</form>

View File

@@ -32,6 +32,9 @@
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
window.RECIPE_ID = {{ recipe.pk }}
window.DEFAULT_UNIT = '{{request.user.userpreference.default_unit}}'
window.USER_PREF = {
'use_kj': {% if request.user.userpreference.use_kj %} true {% else %} false {% endif %},
}
</script>

View File

@@ -22,13 +22,11 @@
</div>
{{ form|crispy }}
{% if related_objects %}
{% blocktrans %} <i>{{ object }}</i> could not be deleted because it is still referenced by the following instances: {% endblocktrans %}
<br/>
<br/>
{% for o in related_objects %}
{% if protected_objects %}
<h5>{% trans 'Protected' %} <small class="text-muted">The object you are trying to delete is <b>protected</b> by the following references to it.</small></h5>
{% for o in protected_objects %}
{% class_name o.model as name %}
<h5>{{ name }}</h5>
<u>{{ name }}</u>
<ul>
{% for e in o %}
<li>
@@ -36,14 +34,40 @@
</li>
{% endfor %}
</ul>
{% endfor %}
<br/>
{% endif %}
<button class="btn btn-success" type="submit" href="{{ success_url }}"><i
{% if cascading_objects %}
<h5>{% trans 'Cascade' %} <small class="text-muted">The object you are trying to delete is used by the following objects which will <b>also be deleted</b>.</small></h5>
{% for o in cascading_objects %}
{% class_name o.model as name %}
<u>{{ name }}</u>
<ul>
{% for e in o %}
<li>
<span class="badge badge-info">#{{ e.id }}</span> {{ e }}
</li>
{% endfor %}
</ul>
{% endfor %}
{% endif %}
{% if set_null_objects %}
<h5>{% trans 'Remove' %} <small class="text-muted">The object you are trying to delete is used by the following objects from which the reference will be removed.</small></h5>
{% for o in set_null_objects %}
{% class_name o.model as name %}
<u>{{ name }}</u>
<ul>
{% for e in o %}
<li>
<span class="badge badge-info">#{{ e.id }}</span> {{ e }}
</li>
{% endfor %}
</ul>
{% endfor %}
{% endif %}
<button class="btn btn-success" type="submit" href="{{ success_url }}" {% if protected_objects %}disabled{% endif %}><i
class="fas fa-trash-alt"></i> {% trans 'Confirm' %}</button>
<a href="javascript:history.back()" class="btn btn-danger"><i class="fas fa-undo-alt"></i> {% trans 'Cancel' %}
</a>

View File

@@ -28,6 +28,8 @@
<script type="application/javascript">
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
{% if current_file_size_mb %}
window.CURRENT_FILE_SIZE_MB = {{ current_file_size_mb|unlocalize }}
window.MAX_FILE_SIZE_MB = {{ max_file_size_mb|unlocalize }}

View File

@@ -1,742 +1,36 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% block title %}{% trans 'Meal-Plan' %}{% endblock %}
{% block extra_head %}
{% block content_fluid %}
{% include 'include/vue_base.html' %}
<div id="app">
<meal-plan-view></meal-plan-view>
</div>
<script src="{% static 'js/moment-with-locales.min.js' %}"></script>
<script src="{% static 'js/Sortable.min.js' %}"></script>
<script src="{% static 'js/vuedraggable.umd.min.js' %}"></script>
<script src="{% static 'js/vue-cookies.js' %}"></script>
<script src="{% static 'js/js.cookie.min.js' %}"></script>
{% endblock %}
{% block content %}
<div id="app">
<div class="row mt-2 mb-1">
<div class="col-md-4 offset-md-4">
<div class="input-group">
<div class="input-group-prepend">
<button class="btn btn-outline-secondary shadow-none"
@click="changeStartDate(number_of_days * -1)">
<i class="fas fa-arrow-left"></i>
</button>
</div>
<input name="date" id="id_date" class="form-control" type="date" v-model="start_date"
@change="updatePlan()">
<div class="input-group-append">
<button class="btn btn-outline-secondary shadow-none" @click="changeStartDate(number_of_days)">
<i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
</div>
<div class="col-md-4">
<a href="{% url 'view_plan_new' %}" class="float-right">
<button class="btn btn-outline-secondary shadow-none">
<i class="fas fa-star"></i> {% trans 'Try the new meal planner' %}
</button>
</a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-sm table-striped table-responsive-sm" style=" table-layout:fixed;">
<thead class="thead-dark" style="background-image: url({% static 'assets/header.svg' %});">
<tr>
<th class="thead-blank" v-for="d in dates" style="width: 14.2%; text-align: center;">
[[formatDateDayname(d)]]<br/>[[formatDateDay(d)]].
<button class="btn btn-sm btn-outline-secondary shadow-none" @click="addDayToShopping(d)"><i
class="fas fa-cart-plus fa-sm"></i></button>
</th>
</tr>
</thead>
<tbody v-for="t in meal_types">
<tr v-if="meal_plan[t.name] !== undefined">
<td :colspan="number_of_days" style="text-align: center">
[[ meal_plan[t.name].name]]
<template
v-if="t.created_by !== {{ request.user.pk }} && user_names[t.created_by] !== undefined">
([[ user_names[t.created_by] ]])
</template>
</td>
</tr>
<tr v-if="meal_plan[t.name] !== undefined">
<td v-for="d in meal_plan[t.name].days">
<draggable class="list-group" :list="d.items" group="plan" style="min-height: 40px"
@change="dragChanged(d.date, t, $event)"
:empty-insert-threshold="10" handle=".handle">
<div class="" v-for="(element, index) in d.items" :key="element.id">
<!-- small layout with handle -->
<div class="d-block d-md-none">
<div class="col-">
<i class="fas fa-arrows-alt handle input-group-text"
style="width: 100%"></i>
</div>
<div class="list-group-item" style="word-wrap: break-word;">
<a href="#" @click="plan_detail = element" data-toggle="modal"
data-target="#id_plan_detail_modal">[[ planElementName(element)]]</a>
</div>
</div>
<!-- big layout -->
<div class="list-group-item handle d-md-block d-none"
style="word-wrap: break-word; padding: 2;margin-bottom: 4">
<div class="col-md-12" style="padding: 0">
<a href="#" @click="plan_detail = element" data-toggle="modal"
data-target="#id_plan_detail_modal">[[ planElementName(element)]]</a>
</div>
</div>
</div>
</draggable>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<i class="fas fa-calendar-plus"></i> {% trans 'New Entry' %} <a href="#" data-toggle="modal"
data-target="#id_plan_help_modal"><i
class="far fa-question-circle"></i></a>
</div>
<div class="row">
<div class="col-md-6">
<div class="card-body">
<div class="row">
<div class="col-md-12">
<div class="input-group mb-3">
<input type="text" class="form-control" v-model="recipe_query" @keyup="getRecipes"
placeholder="{% trans 'Search Recipe' %}">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"
@click="getRandomRecipes">
<i class="fas fa-dice"></i>
</button>
</div>
</div>
</div>
</div>
<draggable class="list-group" :list="recipes"
:group="{ name: 'plan', pull: 'clone', put: false }" :clone="cloneRecipe">
<div class="list-group-item d-flex align-items-center justify-content-between"
v-for="(element, index) in recipes" :key="element.id">
<span>
<i class="fas fa-arrows-alt"></i> [[element.name]]
</span>
<span class="badge badge-light badge-pill">[[element.servings]]</span>
</div>
</draggable>
</div>
</div>
<div class="col-md-6">
<div>
<div class="card-body">
<input type="text" class="form-control" v-model="new_note_title"
placeholder="{% trans 'Title' %}" style="margin-bottom: 8px">
<textarea class="form-control" v-model="new_note_text"
placeholder="{% trans 'Note (optional)' %}"></textarea>
<small><span
class="text-muted">{% trans 'You can use markdown to format this field. See the <a href="/docs/markdown/" target="_blank" rel="noopener noreferrer">docs here</a>' %}</span></small>
<br/>
<br/>
<input type="number" class="form-control" v-model="new_note_servings"
placeholder="{% trans 'Serving Count' %}" style="margin-bottom: 8px">
<br/>
<draggable :list="pseudo_note_list"
:group="{ name: 'plan', pull: 'clone', put: false }" :clone="cloneNote">
<div class="list-group-item" v-for="(element, index) in pseudo_note_list"
:key="element.id">
<i class="fas fa-arrows-alt"></i> {% trans 'Create only note' %}
</div>
</draggable>
</div>
</div>
</div>
</div>
</div>
<br>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="fas fa-shopping-cart"></i> {% trans 'Shopping List' %}
</div>
<div class="card-body">
<template v-if="shopping_list.length < 1">{% trans 'Shopping list currently empty' %}</template>
<template v-else>
<a v-bind:href="getShoppingUrl()" class="btn btn-success"
target="_blank">{% trans 'Open Shopping List' %}</a>
<br/>
<br/>
{% trans 'Recipes' %}
<ul class="list-group" style="margin-top: 8px">
<li class="list-group-item" v-for="item in shopping_list"> [[ item.recipe_name ]]</li>
</ul>
</template>
</div>
</div>
</div>
<div class="col-md-6" style="margin-top: 8px">
<div class="card">
<div class="card-header">
<i class="fas fa-shopping-cart"></i> {% trans 'Plan' %}
</div>
<div class="card-body">
<div class="row">
<div class="col">
<label>
{% trans 'Number of Days' %}
<input class="form-control" type="number" v-model="number_of_days"
@change="updatePlan(); $cookies.set('number_of_days',number_of_days, -1)">
</label>
</div>
</div>
<div class="row">
<div class="col">
<label>
{% trans 'Weekday offset' %}
<input class="form-control" type="number" v-model="start_offset"
@change="updatePlan(); $cookies.set('start_offset',start_offset, -1)">
<small class="text-muted">{% trans 'Number of days starting from the first day of the week to offset the default view.' %}</small>
</label>
</div>
</div>
<a href="#" data-toggle="modal"
data-target="#id_plan_types_modal">{% trans 'Edit plan types' %}</a> <br/>
<a href="#" data-toggle="modal"
data-target="#id_plan_help_modal">{% trans 'Show help' %}</a><br/>
<a v-bind:href="getIcalUrl()">{% trans 'Week iCal export' %}</a>
</div>
</div>
</div>
</div>
<br/>
<br/>
<div class="modal fade" id="id_plan_detail_modal" tabindex="-1" role="dialog"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<template v-if="plan_detail.title !==''">[[ plan_detail.title ]]</template>
<template v-else>[[ plan_detail.recipe_name ]]</template>
<small
class="text-muted"><br/>[[ plan_detail.meal_type_name ]] [[
formatLocalDate(plan_detail.date) ]]</small>
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<template v-if="plan_detail.recipe_name !== undefined ">
<small class="text-muted">{% trans 'Recipe' %}</small><br/>
<a v-bind:href="planDetailRecipeUrl()" target="_blank">[[ plan_detail.recipe_name ]]</a>
<br/>
<br/>
<small class="text-muted">{% trans 'Serving Count' %}</small><br/>
<span>[[ plan_detail.servings ]]</span>
</template>
<template v-if="plan_detail.note !== ''">
<small class="text-muted">{% trans 'Note' %}</small><br/>
<span v-html="plan_detail.note_markdown"></span>
<br/>
</template>
<br/>
<br/>
<template v-if="plan_detail.created_by !== undefined ">
<small class="text-muted">{% trans 'Created by' %}</small><br/>
[[ user_names[plan_detail.created_by] ]]
<br/>
</template>
<template v-if="plan_detail.shared.length > 0">
<small class="text-muted">{% trans 'Shared with' %}</small><br/>
<span>[[ planDetailUserList() ]]</span>
<br/>
</template>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger"
@click="deleteEntry(plan_detail)">{% trans 'Delete' %}</button>
<button type="button" class="btn btn-success"
v-if="!shopping_list.includes(plan_detail) && plan_detail.recipe_name !== undefined"
@click="shopping_list.push(plan_detail)">{% trans 'Add to Shopping' %}</button>
<a class="btn btn-primary" v-bind:href="planDetailEditUrl()">{% trans 'Edit' %}</a>
<button type="button" class="btn btn-secondary"
data-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="id_plan_types_modal" tabindex="-1" role="dialog"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans 'Edit plan types' %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<draggable :list="meal_types_edit" handle=".handle"
:group="{ name: 'types'}">
<div v-for="(element, index) in meal_types_edit"
:key="element.id">
<template v-if="!element.delete">
<div class="input-group mb-3">
<div class="input-group-prepend handle">
<button tabindex="-1" class="btn btn-outline-secondary"><i
class="fas fa-arrows-alt-v"></i></button>
</div>
<input class="form-control" v-model="element.name">
<div class="input-group-append">
<button tabindex="-1" class="btn btn-outline-danger" type="button"
@click="markTypeDelete(element)"><i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
</template>
</div>
</draggable>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary"
@click="meal_types_edit.push({name:'{% trans 'New meal type' %}', delete:false})">{% trans 'New' %}</button>
<button type="button" class="btn btn-success"
@click="updatePlanTypes()">{% trans 'Save' %}</button>
<button type="button" class="btn btn-secondary"
data-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="id_plan_help_modal" tabindex="-1" role="dialog"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans 'Meal Plan Help' %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% blocktrans %}
<p>The meal plan module allows planning of meals both with recipes and notes.</p>
<p>Simply select a recipe from the list of recently viewed recipes or search the one you
want and drag it to the desired plan position. You can also add a note and a title and
then drag the recipe to create a plan entry with a custom title and note. Creating only
Notes is possible by dragging the create note box into the plan.</p>
<p>Click on a recipe in order to open the detailed view. There you can also add it to the
shopping list. You can also add all recipes of a day to the shopping list by
clicking the shopping cart at the top of the table.</p>
<p>Since a common use case is to plan meals together you can define
users you want to share your plan with in the settings.
</p>
<p>You can also edit the types of meals you want to plan. If you share your plan with
someone with
different meals, their meal types will appear in your list as well. To prevent
duplicates (e.g. Other and Misc.)
name your meal types the same as the users you share your meals with and they will be
merged.</p>
{% endblocktrans %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>
</div>
<script src="{% url 'javascript-catalog' %}"></script>
{% block script %}
{% if debug %}
<script src="{% url 'js_reverse' %}"></script>
{% else %}
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
{% endif %}
<script type="application/javascript">
moment.locale('{{request.LANGUAGE_CODE}}');
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
let csrftoken = Cookies.get('csrftoken');
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
let app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
start_date: undefined,
start_offset: 0,
dates: [],
number_of_days: $cookies.isKey('number_of_days') ? $cookies.get('number_of_days') : 7,
plan_entries: [],
meal_types: [],
meal_types_edit: [],
meal_plan: {},
plan_detail: {shared: []},
recipes: [],
recipe_query: '',
pseudo_note_list: [
{id: 0, title: '', text: ''}
],
new_note_title: '',
new_note_text: '',
new_note_servings: '',
default_shared_users: [],
user_id_update: [],
user_names: {},
shopping: false,
shopping_list: [],
},
mounted: function () {
this.default_shared_users = [{% for u in request.user.userpreference.plan_share.all %}
{{ u.pk }},
{% endfor %}]
this.$set(this.user_names, {{ request.user.pk }}, '{{ request.user.get_user_name }}')
this.user_id_update = Array.from(this.default_shared_users)
this.start_offset = $cookies.isKey('start_offset') ? $cookies.get('start_offset') : 0;
this.start_date = moment().weekday(0).add(this.start_offset, 'days').format('YYYY-MM-DD')
this.updatePlan();
this.getRecipes();
//this.makeToast('success', 'this actually works', 'success')
},
methods: {
makeToast: function (title, message, variant = null) {
this.$bvToast.toast(message, {
title: title,
variant: variant,
toaster: 'b-toaster-top-center',
solid: true
})
},
updatePlan: function () {
this.dates = [];
for (var i = 0; i <= (this.number_of_days - 1); i++) {
this.dates.push(moment(this.start_date).add(i, 'days'));
}
let planEntryPromise = this.getPlanEntries();
let planTypePromise = this.getPlanTypes();
Promise.allSettled([planEntryPromise, planTypePromise]).then(() => {
this.buildGrid()
})
},
getPlanEntries: function () {
return this.$http.get("{% url 'api:mealplan-list' %}?from_date=" + this.dates[0].format('YYYY-MM-DD') + "&to_date=" + this.dates[this.dates.length - 1].format('YYYY-MM-DD')).then((response) => {
this.plan_entries = response.data;
}).catch((err) => {
console.log("getPlanEntries error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
getPlanTypes: function () {
return this.$http.get("{% url 'api:mealtype-list' %}").then((response) => {
this.meal_types = response.data;
this.meal_types_edit = jQuery.extend(true, [], response.data);
for (let mte of this.meal_types_edit) {
this.$set(mte, 'delete', false)
}
if (this.meal_types.length === 0) {
this.makeToast(gettext('Information'), gettext('To use the meal plan please first create at least one meal plan type.'), 'warning')
}
}).catch((err) => {
console.log("getPlanTypes error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
buildGrid: function () {
this.meal_plan = {}
for (let e of this.plan_entries) {
let new_type = {id: e.meal_type, name: e.meal_type_name, created_by: e.created_by}
if (this.meal_types.filter(el => el.name === new_type.name).length === 0) {
this.meal_types.push(new_type)
}
}
for (let t of this.meal_types) {
this.$set(this.meal_plan, t.name, {
name: t.name,
meal_type: t.id,
days: {}
})
for (let d of this.dates) {
this.$set(this.meal_plan[t.name].days, d.format('YYYY-MM-DD'), {
name: this.formatDateDayname(d),
date: d.format('YYYY-MM-DD'),
items: []
})
}
}
for (let e of this.plan_entries) {
this.meal_plan[e.meal_type_name].days[e.date].items.push(e)
for (let u of e.shared) {
if (!this.user_id_update.includes(parseInt(u))) {
this.user_id_update.push(parseInt(u))
}
}
}
this.updateUserNames()
},
getRandomRecipes: function () {
this.$set(this, 'recipe_query', '');
this.getRecipes();
},
getRecipes: function () {
let url = "{% url 'api:recipe-list' %}?page_size=5"
if (this.recipe_query !== '') {
url += '&query=' + this.recipe_query;
} else {
url += '&random=true'
}
this.$http.get(url).then((response) => {
this.recipes = this.removeDuplicates(response.data.results, recipe => recipe.id);
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
getMdNote: function () {
let url = "{% url 'api:recipe-list' %}?page_size=5"
if (this.recipe_query !== '') {
url += '&query=' + this.recipe_query;
}
this.$http.get(url).then((response) => {
this.recipes = response.data.results;
}).catch((err) => {
console.log("getRecipes error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
updateUserNames: function () {
return this.$http.get("{% url 'api:username-list' %}?filter_list=[" + this.user_id_update + ']').then((response) => {
for (let u of response.data) {
this.$set(this.user_names, u.id, u.username);
}
}).catch((err) => {
console.log("updateUserNames error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
dragChanged: function (date, meal_type, evt) {
if (evt.added !== undefined) {
let plan_entry = evt.added.element
plan_entry.date = date
plan_entry.meal_type = meal_type
plan_entry.meal_type_name = meal_type.name
if (plan_entry.is_new) { // its not a meal plan object
plan_entry.created_by = {{ request.user.id }};
plan_entry.shared = this.default_shared_users
this.$http.post(`{% url 'api:mealplan-list' %}`, plan_entry).then((response) => {
let entry = response.data
this.meal_plan[entry.meal_type_name].days[entry.date].items = this.meal_plan[entry.meal_type_name].days[entry.date].items.filter(item => !item.is_new)
this.meal_plan[entry.meal_type_name].days[entry.date].items.push(entry)
}).catch((err) => {
console.log("dragChanged create error", err);
})
} else {
this.$http.put(`{% url 'api:mealplan-list' %}${plan_entry.id}/`, plan_entry).then((response) => {
}).catch((err) => {
console.log("dragChanged update error", err);
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
})
}
}
},
deleteEntry: function (entry) {
$('#id_plan_detail_modal').modal('hide')
this.$http.delete(`{% url 'api:mealplan-list' %}${entry.id}/`, entry).then((response) => {
this.meal_plan[entry.meal_type_name].days[entry.date].items = this.meal_plan[entry.meal_type_name].days[entry.date].items.filter(item => item !== entry)
}).catch((err) => {
console.log("deleteEntry error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
removeDuplicates: function (data, key) {
return [
...new Map(data.map(item => [key(item), item])).values()
]
},
updatePlanTypes: function () {
let promise_list = []
let i = 0
for (let x of this.meal_types_edit) {
x.order = i
i++
if (x.id === undefined && !x.delete) {
x.created_by = {{ request.user.id }}
promise_list.push(this.$http.post("{% url 'api:mealtype-list' %}", x).then((response) => {
}).catch((err) => {
console.log("updatePlanTypes create error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
}))
} else if (x.delete) {
if (x.id !== undefined) {
promise_list.push(this.$http.delete(`{% url 'api:mealtype-list' %}${x.id}/`, x).then((response) => {
}).catch((err) => {
console.log("updatePlanTypes delete error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
}))
}
} else {
promise_list.push(this.$http.put(`{% url 'api:mealtype-list' %}${x.id}/`, x).then((response) => {
}).catch((err) => {
console.log("updatePlanTypes update error: ", err);
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
}))
}
}
Promise.allSettled(promise_list).then(() => {
this.updatePlan()
$('#id_plan_types_modal').modal('hide')
})
},
markTypeDelete: function (element) {
if (confirm(gettext('When deleting a meal type all entries using that type will be deleted as well. Deletion will apply when configuration is saved. Do you want to proceed?'))) {
element.delete = true
}
},
cloneRecipe: function (recipe) {
let r = {
id: Math.round(Math.random() * 1000) + 10000,
recipe: recipe,
recipe_name: recipe.name,
servings: (this.new_note_servings > 1) ? this.new_note_servings : recipe.servings,
title: this.new_note_title,
note: this.new_note_text,
is_new: true
}
console.log(recipe)
this.new_note_title = ''
this.new_note_text = ''
this.new_note_servings = ''
return r
},
cloneNote: function () {
let new_entry = {
id: Math.round(Math.random() * 1000) + 10000,
title: this.new_note_title,
note: this.new_note_text,
servings: 1,
is_new: true,
}
if (new_entry.title === '') {
new_entry.title = gettext('Title')
}
this.new_note_title = ''
this.new_note_text = ''
this.new_note_servings = ''
return new_entry
},
planElementName: function (element) {
if (element.title) {
return element.title
} else if (element.recipe_name) {
return element.recipe_name
} else {
return element.name
}
},
planDetailRecipeUrl: function () {
return "{% url 'view_recipe' 12345 %}".replace(/12345/, this.plan_detail.recipe.id);
},
planDetailEditUrl: function () {
return "{% url 'edit_meal_plan' 12345 %}".replace(/12345/, this.plan_detail.id);
},
planDetailUserList: function () {
let users = []
for (let u of this.plan_detail.shared) {
users.push(this.user_names[u])
}
return users.join(', ')
},
formatLocalDate: function (date) {
return moment(date).format('LL')
},
formatDateDay: function (date) {
return moment(date).format('D')
},
formatDateDayname: function (date) {
return moment(date).format('dddd')
},
changeStartDate: function (change) {
this.start_date = moment(this.start_date).add(change, 'days').format('YYYY-MM-DD')
this.updatePlan();
},
getShoppingUrl: function () {
let url = "{% url 'view_shopping' %}"
let first = true
for (let se of this.shopping_list) {
if (first) {
url += `?r=[${se.recipe.id},${se.servings}]`
first = false
} else {
url += `&r=[${se.recipe.id},${se.servings}]`
}
}
return url
},
getIcalUrl: function () {
if (this.dates.length === 0) {
return ""
}
return "{% url 'api_get_plan_ical' 12345 6789 %}".replace(/12345/, this.dates[0].format('YYYY-MM-DD')).replace(/6789/, this.dates[this.dates.length - 1].format('YYYY-MM-DD'));
},
addDayToShopping: function (date) {
for (let t of this.meal_types) {
for (let i of this.meal_plan[t.name].days[date.format('YYYY-MM-DD')].items) {
if (!this.shopping_list.includes(i)) {
this.shopping_list.push(i)
}
}
}
}
}
});
window.ICAL_URL = '{% url 'api_get_plan_ical' 12345 6789 %}'
window.SHOPPING_URL = '{% url 'view_shopping' %}'
</script>
{% render_bundle 'meal_plan_view' %}
{% endblock %}

View File

@@ -69,6 +69,7 @@
window.USER_PREF = {
'use_fractions': {% if request.user.userpreference.use_fractions %} true {% else %} false {% endif %},
'ingredient_decimals': {% if request.user.userpreference.use_fractions %} {{ request.user.userpreference.ingredient_decimals }} {% else %} 2 {% endif %},
'use_kj': {% if request.user.userpreference.use_kj %} true {% else %} false {% endif %},
}
</script>

View File

@@ -29,26 +29,25 @@
style="height:50%"
href="{% bookmarklet request %}"
title="{% trans 'Drag me to your bookmarks to import recipes from anywhere' %}">
<img src="{% static 'assets/favicon-16x16.png' %}">{% trans 'Bookmark Me!' %} </a>
<img src="{% static 'assets/favicon-16x16.png' %}">{% trans 'Bookmark Me!' %} </a>
</div>
<nav class="nav nav-pills flex-sm-row" style="margin-bottom:10px">
<nav class="nav nav-pills flex-sm-row mb-2">
<a class="nav-link active" href="#nav-url" data-toggle="tab" role="tab" aria-controls="nav-url"
aria-selected="true" @click="mode='url'">URL</a>
aria-selected="true" @click="mode='url'">{% trans 'URL' %}</a>
<a class="nav-link" href="#nav-app" data-toggle="tab" role="tab" aria-controls="nav-app"
@click="mode='app'">App</a>
@click="mode='app'">{% trans 'App' %}</a>
<a class="nav-link" href="#nav-source" data-toggle="tab" role="tab" aria-controls="nav-source"
@click="mode='source'">Source</a>
@click="mode='source'">{% trans 'Source' %}</a>
<a class="nav-link disabled" href="#nav-text" data-toggle="tab" role="tab" aria-controls="nav-text"
@click="mode='text'">Text</a>
@click="mode='text'">{% trans 'Text' %}</a>
<a class="nav-link disabled" href="#nav-file" data-toggle="tab" role="tab" aria-controls="nav-file"
@click="mode='file'">File</a>
@click="mode='file'">{% trans 'File' %}</a>
</nav>
<div class="tab-content" id="nav-tabContent">
<!-- Import URL -->
<div class="tab-pane fade show active" id="nav-url" role="tabpanel">
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<div class="btn-group btn-group-toggle mt-2" data-toggle="buttons">
<label class="btn btn-outline-info btn-sm active" @click="automatic=true">
<input type="radio" autocomplete="off" checked> Automatic
</label>
@@ -57,10 +56,13 @@
<input type="radio" autocomplete="off"> Manual
</label>
</div>
<div class="input-group my-2">
<input class="form-control" v-model="remote_url" placeholder="{% trans 'Enter website URL' %}">
<div role="group" class="input-group mt-4">
<input type="text" v-model="remote_url"
class="form-control form-control-lg form-control-borderless form-control-search form-control"
placeholder="{% trans 'Enter website URL' %}">
<div class="input-group-append">
<button @click="loadRecipe()" class="btn btn-primary shadow-none" type="button"
<button @click="loadRecipe()" class="btn btn-primary shadow-none"
type="button"
id="id_btn_search"><i class="fas fa-search"></i>
</button>
</div>
@@ -106,7 +108,7 @@
<!-- Import JSON or HTML -->
<div class=" tab-pane fade show" id="nav-source" role="tabpanel">
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<div class="btn-group btn-group-toggle mt-2" data-toggle="buttons">
<label class="btn btn-outline-info btn-sm active" @click="automatic=true">
<input type="radio" autocomplete="off" checked> Automatic
</label>
@@ -115,7 +117,7 @@
<input type="radio" autocomplete="off"> Manual
</label>
</div>
<div class="input-group my-2">
<div class="input-group mt-4">
<textarea class="form-control input-group-append" v-model="source_data" rows=10
placeholder="{% trans 'Paste json or html source here to load recipe.' %}"
style="font-size: 12px">
@@ -455,7 +457,8 @@
<div class="form-group">
<label for="id_servings">{% trans 'Servings' %}</label>
<b-form-input id="id_servings" class="form-control" v-model="recipe_data.servings" @change="recipe_data.servings = Math.round($event.replace(',','.'))"></b-form-input>
<b-form-input id="id_servings" class="form-control" v-model="recipe_data.servings"
@change="recipe_data.servings = Math.round($event.replace(',','.'))"></b-form-input>
</div>
</div>

View File

@@ -92,10 +92,10 @@ def recipe_last(recipe, user):
@register.simple_tag
def page_help(page_name):
help_pages = {
'edit_storage': 'https://vabene1111.github.io/recipes/features/external_recipes/',
'view_shopping': 'https://vabene1111.github.io/recipes/features/shopping/',
'view_import': 'https://vabene1111.github.io/recipes/features/import_export/',
'view_export': 'https://vabene1111.github.io/recipes/features/import_export/',
'edit_storage': 'https://docs.tandoor.dev/features/external_recipes/',
'view_shopping': 'https://docs.tandoor.dev/features/shopping/',
'view_import': 'https://docs.tandoor.dev/features/import_export/',
'view_export': 'https://docs.tandoor.dev/features/import_export/',
}
link = help_pages.get(page_name, '')

View File

@@ -10,7 +10,8 @@ from cookbook.helper import dal
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList,
Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, get_model_name, Automation, UserFile)
Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, get_model_name, Automation,
UserFile, Step)
from .views import api, data, delete, edit, import_export, lists, new, views, telegram
router = routers.DefaultRouter()
@@ -58,7 +59,6 @@ urlpatterns = [
path('search/v2/', views.search_v2, name='view_search_v2'),
path('books/', views.books, name='view_books'),
path('plan/', views.meal_plan, name='view_plan'),
path('plan_new/', views.meal_plan_new, name='view_plan_new'),
path('plan/entry/<int:pk>', views.meal_plan_entry, name='view_plan_entry'),
path('shopping/', views.shopping_list, name='view_shopping'),
path('shopping/<int:pk>', views.shopping_list, name='view_shopping'),
@@ -178,7 +178,7 @@ for m in generic_models:
)
)
vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation, UserFile]
vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation, UserFile, Step]
for m in vue_models:
py_name = get_model_name(m)
url_name = py_name.replace('_', '-')

View File

@@ -48,7 +48,7 @@ from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
from cookbook.schemas import FilterSchema, RecipeSchema, TreeSchema
from cookbook.schemas import FilterSchema, RecipeSchema, TreeSchema, QueryOnlySchema
from cookbook.serializer import (FoodSerializer, IngredientSerializer,
KeywordSerializer, MealPlanSerializer,
MealTypeSerializer, RecipeBookSerializer,
@@ -63,6 +63,7 @@ from cookbook.serializer import (FoodSerializer, IngredientSerializer,
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer,
RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer,
BookmarkletImportSerializer, SupermarketCategorySerializer, UserFileSerializer, SupermarketCategoryRelationSerializer, AutomationSerializer)
from recipes import settings
class StandardFilterMixin(ViewSetMixin):
@@ -409,7 +410,7 @@ class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
permission_classes = [CustomIsOwner]
def get_queryset(self):
self.queryset = self.queryset.filter(created_by=self.request.user).filter(space=self.request.space)
self.queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(space=self.request.space)
return super().get_queryset()
@@ -497,9 +498,16 @@ class StepViewSet(viewsets.ModelViewSet):
queryset = Step.objects
serializer_class = StepSerializer
permission_classes = [CustomIsUser]
pagination_class = DefaultPagination
schema = QueryOnlySchema()
def get_queryset(self):
return self.queryset.filter(recipe__space=self.request.space)
queryset = self.queryset.filter(recipe__space=self.request.space)
query = self.request.query_params.get('query', None)
if query is not None:
queryset = queryset.filter(Q(name__icontains=query) | Q(recipe__name__icontains=query))
return queryset
class RecipePagination(PageNumberPagination):
@@ -718,10 +726,8 @@ def get_recipe_file(request, recipe_id):
@group_required('user')
def sync_all(request):
if request.space.demo:
messages.add_message(
request, messages.ERROR, _('This feature is not available in the demo version!')
)
if request.space.demo or settings.HOSTED:
messages.add_message(request, messages.ERROR, _('This feature is not yet available in the hosted version of tandoor!'))
return redirect('index')
monitors = Sync.objects.filter(active=True).filter(space=request.user.userpreference.space)

View File

@@ -25,6 +25,7 @@ from cookbook.helper.recipe_url_import import parse_cooktime
from cookbook.models import (Comment, Food, Ingredient, Keyword, Recipe,
RecipeImport, Step, Sync, Unit, UserPreference)
from cookbook.tables import SyncTable
from recipes import settings
@group_required('user')
@@ -37,6 +38,10 @@ def sync(request):
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
return HttpResponseRedirect(reverse('index'))
if request.space.demo or settings.HOSTED:
messages.add_message(request, messages.ERROR, _('This feature is not yet available in the hosted version of tandoor!'))
return redirect('index')
if request.method == "POST":
if not has_group_permission(request.user, ['admin']):
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
@@ -186,13 +191,12 @@ def import_url(request):
ingredient.save()
step.ingredients.add(ingredient)
print(ingredient)
if 'image' in data and data['image'] != '' and data['image'] is not None:
try:
response = requests.get(data['image'])
img, filetype = handle_image(request, BytesIO(response.content))
img, filetype = handle_image(request, File(BytesIO(response.content), name='image'))
recipe.image = File(
img, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}'
)

View File

@@ -1,4 +1,5 @@
from django.contrib import messages
from django.db import models
from django.db.models import ProtectedError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
@@ -24,30 +25,36 @@ class RecipeDelete(GroupRequiredMixin, DeleteView):
success_url = reverse_lazy('index')
def delete(self, request, *args, **kwargs):
obj = self.get_object()
related_objects = []
for x in obj._meta.get_fields():
try:
related = x.related_model.objects.filter(**{x.field.name: obj})
if related.exists():
related_objects.append(related)
except AttributeError:
pass
if related_objects:
self.object = obj
return render(request, template_name=self.template_name, context=self.get_context_data(related_objects=related_objects))
self.object = self.get_object()
# TODO make this more generic so that all delete functions benefit from this
if self.get_context_data()['protected_objects']:
return render(request, template_name=self.template_name, context=self.get_context_data())
success_url = self.get_success_url()
obj.delete()
self.object.delete()
return HttpResponseRedirect(success_url)
def get_context_data(self, **kwargs):
context = super(RecipeDelete, self).get_context_data(**kwargs)
context['title'] = _("Recipe")
if 'related_objects' in kwargs:
context['related_objects'] = kwargs.pop('related_objects')
# TODO make this more generic so that all delete functions benefit from this
self.object = self.get_object()
context['protected_objects'] = []
context['cascading_objects'] = []
context['set_null_objects'] = []
for x in self.object._meta.get_fields():
try:
related = x.related_model.objects.filter(**{x.field.name: self.object})
if related.exists() and x.on_delete == models.PROTECT:
context['protected_objects'].append(related)
if related.exists() and x.on_delete == models.CASCADE:
context['cascading_objects'].append(related)
if related.exists() and x.on_delete == models.SET_NULL:
context['set_null_objects'].append(related)
except AttributeError:
pass
return context

View File

@@ -1,14 +1,12 @@
import os
from django.contrib import messages
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
from django.utils.translation import gettext as _
from django.views.generic import UpdateView
from django.views.generic.edit import FormMixin
from django_scopes import scopes_disabled
from cookbook.forms import (CommentForm, ExternalRecipeForm,
MealPlanForm,
@@ -17,12 +15,13 @@ from cookbook.forms import (CommentForm, ExternalRecipeForm,
from cookbook.helper.permission_helper import (GroupRequiredMixin,
OwnerRequiredMixin,
group_required)
from cookbook.models import (Comment, Ingredient, MealPlan,
MealType, Recipe, RecipeBook, RecipeImport,
from cookbook.models import (Comment, MealPlan,
MealType, Recipe, RecipeImport,
Storage, Sync, UserPreference)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
from recipes import settings
@group_required('guest')
@@ -126,6 +125,10 @@ def edit_storage(request, pk):
messages.add_message(request, messages.ERROR, _('You cannot edit this storage!'))
return HttpResponseRedirect(reverse('list_storage'))
if request.space.demo or settings.HOSTED:
messages.add_message(request, messages.ERROR, _('This feature is not yet available in the hosted version of tandoor!'))
return redirect('index')
if request.method == "POST":
form = StorageForm(request.POST, instance=instance)
if form.is_valid():

View File

@@ -221,6 +221,23 @@ def user_file(request):
)
@group_required('user')
def step(request):
# recipe-param is the name of the parameters used when filtering recipes by this attribute
# model-name is the models.js name of the model, probably ALL-CAPS
return render(
request,
'generic/model_template.html',
{
"title": _("Steps"),
"config": {
'model': "STEP", # *REQUIRED* name of the model in models.js
'recipe_param': 'steps',
}
}
)
@group_required('user')
def shopping_list_new(request):
# recipe-param is the name of the parameters used when filtering recipes by this attribute

View File

@@ -19,6 +19,7 @@ from cookbook.helper.permission_helper import (GroupRequiredMixin,
from cookbook.models import (InviteLink, MealPlan, MealType, Recipe,
RecipeBook, RecipeImport, ShareLink, Step, UserPreference)
from cookbook.views.edit import SpaceFormMixing
from recipes import settings
class RecipeCreate(GroupRequiredMixin, CreateView):
@@ -90,6 +91,9 @@ class StorageCreate(GroupRequiredMixin, CreateView):
obj.created_by = self.request.user
obj.space = self.request.space
obj.save()
if self.request.space.demo or settings.HOSTED:
messages.add_message(self.request, messages.ERROR, _('This feature is not yet available in the hosted version of tandoor!'))
return redirect('index')
return HttpResponseRedirect(reverse('edit_storage', kwargs={'pk': obj.pk}))
def get_context_data(self, **kwargs):

View File

@@ -220,11 +220,6 @@ def meal_plan(request):
return render(request, 'meal_plan.html', {})
@group_required('user')
def meal_plan_new(request):
return render(request, 'meal_plan_new.html', {})
@group_required('user')
def supermarket(request):
return render(request, 'supermarket.html', {})
@@ -292,7 +287,7 @@ def user_settings(request):
if request.method == "POST":
if 'preference_form' in request.POST:
active_tab = 'preferences'
form = UserPreferenceForm(request.POST, prefix='preference')
form = UserPreferenceForm(request.POST, prefix='preference', space=request.space)
if form.is_valid():
if not up:
up = UserPreference(user=request.user)
@@ -307,6 +302,7 @@ def user_settings(request):
up.ingredient_decimals = form.cleaned_data['ingredient_decimals'] # noqa: E501
up.comments = form.cleaned_data['comments']
up.use_fractions = form.cleaned_data['use_fractions']
up.use_kj = form.cleaned_data['use_kj']
up.sticky_navbar = form.cleaned_data['sticky_navbar']
up.shopping_auto_sync = form.cleaned_data['shopping_auto_sync']
@@ -343,10 +339,13 @@ def user_settings(request):
if fields_searched == 0:
search_form.add_error(None, _('You must select at least one field to search!'))
search_error = True
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(search_form.cleaned_data['fulltext']) == 0:
search_form.add_error('search', _('To use this search method you must select at least one full text search field!'))
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(
search_form.cleaned_data['fulltext']) == 0:
search_form.add_error('search',
_('To use this search method you must select at least one full text search field!'))
search_error = True
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(search_form.cleaned_data['trigram']) > 0:
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(
search_form.cleaned_data['trigram']) > 0:
search_form.add_error(None, _('Fuzzy search is not compatible with this search method!'))
search_error = True
else:
@@ -381,11 +380,12 @@ def user_settings(request):
sp.save()
if up:
preference_form = UserPreferenceForm(instance=up)
preference_form = UserPreferenceForm(instance=up, space=request.space)
else:
preference_form = UserPreferenceForm()
preference_form = UserPreferenceForm( space=request.space)
fields_searched = len(sp.icontains.all()) + len(sp.istartswith.all()) + len(sp.trigram.all()) + len(sp.fulltext.all())
fields_searched = len(sp.icontains.all()) + len(sp.istartswith.all()) + len(sp.trigram.all()) + len(
sp.fulltext.all())
if sp and not search_error and fields_searched > 0:
search_form = SearchPreferenceForm(instance=sp)
elif not search_error:
@@ -395,7 +395,8 @@ def user_settings(request):
api_token = Token.objects.create(user=request.user)
# these fields require postgress - just disable them if postgress isn't available
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
search_form.fields['search'].disabled = True
search_form.fields['lookup'].disabled = True
search_form.fields['trigram'].disabled = True

View File

@@ -23,7 +23,7 @@ This application is developed using the Django framework for Python. They have e
1. Clone this repository wherever you like and install the Python language for your OS (at least version 3.8)
2. Open it in your favorite editor/IDE (e.g. PyCharm)
1. If you want, create a virutal environment for all your packages.
1. If you want, create a virtual environment for all your packages.
3. Install all required packages: `pip install -r requirements.txt`
4. Run the migrations: `python manage.py migrate`
5. Start the development server: `python manage.py runserver`
@@ -59,6 +59,8 @@ folder of the GitHub repository.
In order to contribute to the documentation you can fork the repository and edit the markdown files in the browser.
Now install mkdocs and dependencies: `pip install mkdocs-material mkdocs-include-markdown-plugin`.
If you want to test the documentation locally run `mkdocs serve` from the project root.
## Contribute Translations

View File

@@ -37,4 +37,13 @@ There is only one installation of the Dropbox system, but it handles multiple us
For Tandoor that means all people that work together on one recipe collection can be in one space.
If you want to host the collection of your friends family or your neighbor you can create a separate space for them (trough the admin interface).
Sharing between spaces is currently not possible but is planned for future releases.
Sharing between spaces is currently not possible but is planned for future releases.
## Create Admin user / reset passwords
To create a superuser or reset a lost password if access to the container is lost you need to
1. execute into the container using `docker-compose exec web_recipes sh`
2. activate the virtual environment `source venv/bin/activate`
3. run `python manage.py createsuperuser` and follow the steps shown.
To change a password enter `python manage.py changepassword <username>` in step 3.

View File

@@ -60,6 +60,25 @@ Use the superuser account to grant permissions to the newly created users.
To link an account to an already existing normal user go to the settings page of the user and link it.
Here you can also unlink your account if you no longer want to use a social login method.
## LDAP
LDAP authentication can be enabled in the `.env` file by setting `LDAP_AUTH=1`.
If set, users listed in the LDAP instance will be able to sign in without signing up.
These variables must be set to configure the connection to the LDAP instance:
```
AUTH_LDAP_SERVER_URI=ldap://ldap.example.org:389
AUTH_LDAP_BIND_DN=uid=admin,ou=users,dc=example,dc=org
AUTH_LDAP_BIND_PASSWORD=adminpassword
AUTH_LDAP_USER_SEARCH_BASE_DN=ou=users,dc=example,dc=org
```
Additional optional variables:
```
AUTH_LDAP_USER_SEARCH_FILTER_STR=(uid=%(user)s)
AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn', 'email': 'mail'}
AUTH_LDAP_ALWAYS_UPDATE_USER=1
AUTH_LDAP_CACHE_TIMEOUT=3600
```
## Reverse Proxy Authentication
!!! Info "Community Contributed Tutorial"

View File

@@ -2,13 +2,13 @@ This application features a very versatile import and export feature in order
to offer the best experience possible and allow you to freely choose where your data goes.
!!! warning "WIP"
The Module is relatively new. There is a know issue with [Timeouts](https://github.com/vabene1111/recipes/issues/417) on large exports.
The Module is relatively new. There is a known issue with [Timeouts](https://github.com/vabene1111/recipes/issues/417) on large exports.
A fix is being developed and will likely be released with the next version.
The Module is build with maximum flexibility and expandability in mind and allows to easily add new
The Module is built with maximum flexibility and expandability in mind and allows to easily add new
integrations to allow you to both import and export your recipes into whatever format you desire.
Feel like there is an important integration missing ? Just take a look at the [integration issues](https://github.com/vabene1111/recipes/issues?q=is%3Aissue+is%3Aopen+label%3Aintegration) or open a new one
Feel like there is an important integration missing? Just take a look at the [integration issues](https://github.com/vabene1111/recipes/issues?q=is%3Aissue+is%3Aopen+label%3Aintegration) or open a new one
if your favorite one is missing.
!!! info "Export"
@@ -36,12 +36,12 @@ Overview of the capabilities of the different integrations.
| RezKonv | ✔️ | ❌ | ❌ |
| OpenEats | ✔️ | ❌ | ⌚ |
| Plantoeat | ✔️ | ❌ | ✔ |
| CookBookApp | ✔️ | ⌚ | |
| CookBookApp | ✔️ | ⌚ | ✔️ |
✔ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented
## Default
The default integration is the build in (and preferred) way to import and export recipes.
The default integration is the built in (and preferred) way to import and export recipes.
It is maintained with new fields added and contains all data to transfer your recipes from one installation to another.
It is also one of the few recipe formats that is actually structured in a way that allows for
@@ -90,7 +90,7 @@ Mealie provides structured data similar to nextcloud.
To migrate your recipes
1. Go to you Mealie settings and create a new Backup
1. Go to your Mealie settings and create a new Backup
2. Download the backup by clicking on it and pressing download (this wasn't working for me, so I had to manually pull it from the server)
3. Upload the entire `.zip` file to the importer page and import everything
@@ -118,7 +118,7 @@ Recipes.zip/
```
## Safron
Go to you safron settings page and export your recipes.
Go to your safron settings page and export your recipes.
Then simply upload the entire `.zip` file to the importer.
!!! warning "Images"
@@ -131,8 +131,8 @@ The `.paprikarecipes` file is basically just a zip with gzipped contents. Simply
all your recipes.
## Pepperplate
Pepperplate provides a `.zip` files contain all your recipes as `.txt` files. These files are well-structured and allow
the import of all data without loosing anything.
Pepperplate provides a `.zip` file containing all of your recipes as `.txt` files. These files are well-structured and allow
the import of all data without losing anything.
Simply export the recipes from Pepperplate and upload the zip to Tandoor. Images are not included in the export and
thus cannot be imported.
@@ -145,7 +145,7 @@ This format is basically completely unstructured and every export looks differen
and leads to suboptimal results. Images are also not supported as they are not included in the export (at least
the tests I had).
Usually the import should recognize all ingredients and put everything else into the instructions. If you import fails
Usually the import should recognize all ingredients and put everything else into the instructions. If your import fails
or is worse than this feel free to provide me with more example data and I can try to improve the importer.
As ChefTap cannot import these files anyway there won't be an exporter implemented in Tandoor.
@@ -154,7 +154,7 @@ As ChefTap cannot import these files anyway there won't be an exporter implement
Meal master can be imported by uploading one or more meal master files.
The files should either be `.txt`, `.MMF` or `.MM` files.
The MealMaster spec allow for many variations. Currently, only the on column format for ingredients is supported.
The MealMaster spec allow for many variations. Currently, only the one column format for ingredients is supported.
Second line notes to ingredients are currently also not imported as a note but simply put into the instructions.
If you have MealMaster recipes that cannot be imported feel free to raise an issue.
@@ -166,7 +166,7 @@ The generated file can simply be imported into Tandoor.
As I only had limited sample data feel free to open an issue if your RezKonv export cannot be imported.
## Recipekeeper
Recipe keeper allows to export a zip file containing recipes and images using its apps.
Recipe keeper allows you to export a zip file containing recipes and images using its apps.
This zip file can simply be imported into Tandoor.
## OpenEats
@@ -213,8 +213,8 @@ Store the outputted json string in a `.json` file and simply import it using the
## Plantoeat
Plan to eat allow to export a text file containing all your recipes. Simply upload that text file to Tandoor to import all recipes
Plan to eat allows you to export a text file containing all your recipes. Simply upload that text file to Tandoor to import all recipes
## CookBookApp
CookBookApp can export .zip files containing .yml files. Upload the entire ZIP to Tandoor to import all conluded recipes.
CookBookApp can export .zip files containing .html files. Upload the entire ZIP to Tandoor to import all included recipes.

View File

@@ -17,7 +17,7 @@ from recipe scaling.
Currently the only available variable in the Templating context is `ingredients`.
`ingredients` is an array that contains all ingredients of the current recipe step. You can access an ingredient by using
`{{ ingredient[<index in list>] }}` where the index refers to the position in the list of ingredients starting with zero.
`{{ ingredients[<index in list>] }}` where the index refers to the position in the list of ingredients starting with zero.
You can also use the interaction menu of the ingredient to copy its reference.
!!! warning
@@ -28,10 +28,10 @@ You can also use the interaction menu of the ingredient to copy its reference.
You can also access only the amount, unit, note or food inside your instruction text using
```
{{ instructions[0].amount }}
{{ instructions[0].unit }}
{{ instructions[0].food }}
{{ instructions[0].note }}
{{ ingredients[0].amount }}
{{ ingredients[0].unit }}
{{ ingredients[0].food }}
{{ ingredients[0].note }}
```
## Technical Reasoning
@@ -43,4 +43,4 @@ The template could access them by ID, the food name or the position in the list.
2. **Name**: very nice to read and easy but does not work when a food occurs twice in a step. Could have workaround but would then be inconsistent.
3. **Position**: easy to write and understand but breaks when ordering is changed and not really nice to read when instructions are not rendered.
I decided to go for the position based system. If you know of any better way feel free to open an issue or PR.
I decided to go for the position based system. If you know of any better way feel free to open an issue or PR.

View File

@@ -1,72 +1,79 @@
<h1 align="center">
<br>
<a href="https://app.tandoor.dev"><img src="https://github.com/vabene1111/recipes/raw/develop/docs/logo_color.svg" height="256px" width="256px"></a>
<a href="https://tandoor.dev"><img src="https://github.com/vabene1111/recipes/raw/develop/docs/logo_color.svg" height="256px" width="256px"></a>
<br>
Tandoor Recipes
<br>
</h1>
<h4 align="center">This is my personal beta of vabene's excellent recipe app. It includes many of the new features I've developed and should be considered experimental.</h4>
## Experimental Features
- Manual import recipes from URL & Source (HTML/JSON)
- Bookmarklet to import recipes from any website
- Full Text Search
- Hierarchical Keywords
## Coming Next
- Heirarchical Ingredients
- Faceted Search
- Search filter by rating
- What Can I Make Now?
- Better ingredient/unit matching on import
- Custom word replacement on import (e.g. 'grams' automatically imported as 'g')
- improved ingredient parser (items in parens moved to notes)
- quick view ingredients
- quick view associated recipe
- favorite recipes
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
<p align="center">
<img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=develop" >
<img src="https://img.shields.io/github/stars/vabene1111/recipes" >
<img src="https://img.shields.io/github/forks/vabene1111/recipes" >
<img src="https://img.shields.io/docker/pulls/vabene1111/recipes" >
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=master" ></a>
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
<a href="https://app.tandoor.dev/accounts/login/?demo" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
</p>
<p align="center">
<a href="https://docs.tandoor.dev/install/docker.html" rel="noopener noreferrer">Installation</a> •
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Documentation</a> •
<a href="https://app.tandoor.dev/" target="_blank" rel="noopener noreferrer">Demo</a>
<a href="https://tandoor.dev" target="_blank" rel="noopener noreferrer">Website</a> •
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Docs</a>
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a> •
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord</a>
</p>
![Preview](preview.png)
!!! info "WIP"
The documentation is work in progress. New information will be added over time.
Feel free to open pull requests to enhance the documentation.
## Core Features
- 🥗 **Manage your recipes** with a fast and intuitive editor
- 📆 **Plan** multiple meals for each day
- 🛒 **Shopping lists** via the meal plan or straight from recipes
- 📚 **Cookbooks** collect recipes into books
- 👪 **Share and collaborate** on recipes with friends and family
## Features
## Made by and for power users
- 📦 **Sync** files with Dropbox and Nextcloud (more can easily be added)
- 🔍 Powerful **search** with Djangos [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
- 🔍 Powerful & customizable **search** with fulltext support and [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
- 🏷️ Create and search for **tags**, assign them in batch to all files matching certain filters
- 📄 **Create recipes** locally within a nice, standardized web interface
- **Import recipes** from thousands of websites supporting [ld+json or microdata](https://schema.org/Recipe)
- 📱 Optimized for use on **mobile** devices like phones and tablets
- 🛒 Generate **shopping** lists from recipes
- 📆 Create a **Plan** on what to eat when
- 👪 **Share** recipes with friends and comment on them to suggest or remember changes you made
- ➗ automatically convert decimal units to **fractions** for those who like this
- 🐳 Easy setup with **Docker** and included examples for Kubernetes, Unraid and Synology
- ↔️ Quickly merge and rename ingredients, tags and units
- 📥 **Import recipes** from thousands of websites supporting [ld+json or microdata](https://schema.org/Recipe)
- ➗ Support for **fractions** or decimals
- 🐳 Easy setup with **Docker** and included examples for **Kubernetes**, **Unraid** and **Synology**
- 🎨 Customize your interface with **themes**
- ✉️ Export and import recipes from other users
- 📦 **Sync** files with Dropbox and Nextcloud
## All the must haves
- 📱Optimized for use on **mobile** devices
- 🌍 localized in many languages thanks to the awesome community
- Many more like recipe scaling, image compression, cookbooks, printing views, ...
- 📥️ **Import your collection** from many other [recipe managers](https://docs.tandoor.dev/features/import_export/)
- Many more like recipe scaling, image compression, printing views and supermarkets
This application is meant for people with a collection of recipes they want to share with family and friends or simply
store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as
a public page.
## Your Feedback
Share some information on how you use Tandoor to help me improve the application [Google Survey](https://forms.gle/qNfLK2tWTeWHe9Qd7)
## Get in touch
<table>
<tr>
<td><a href="https://discord.gg/RhzBrfWgtp">Discord</a></td>
<td>We have a public Discord server that anyone can join. This is where all our developers and contributors hang out and where we make announcements</td>
</tr>
<tr>
<td><a href="https://twitter.com/TandoorRecipes">Twitter</a></td>
<td>You can follow our Twitter account to get updates on new features or releases</td>
</tr>
</table>
## Roadmap
This application has been under rapid development over the last year.

View File

@@ -65,47 +65,9 @@ This configuration exposes the application through an nginx web server on port 8
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/plain/docker-compose.yml
```
```yaml
version: "3"
services:
db_recipes:
restart: always
image: postgres:11-alpine
volumes:
- ./postgresql:/var/lib/postgresql/data
env_file:
- ./.env
web_recipes:
image: vabene1111/recipes
restart: always
env_file:
- ./.env
volumes:
- staticfiles:/opt/recipes/staticfiles
- nginx_config:/opt/recipes/nginx/conf.d
- ./mediafiles:/opt/recipes/mediafiles
depends_on:
- db_recipes
nginx_recipes:
image: nginx:mainline-alpine
restart: always
ports:
- 80:80
env_file:
- ./.env
depends_on:
- web_recipes
volumes:
- nginx_config:/etc/nginx/conf.d:ro
- staticfiles:/static
- ./mediafiles:/media
volumes:
nginx_config:
staticfiles:
```
~~~yaml
{% include "./docker/plain/docker-compose.yml" %}
~~~
### Reverse Proxy
@@ -123,62 +85,9 @@ If you use traefik, this configuration is the one for you.
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/traefik-nginx/docker-compose.yml
```
```yaml
version: "3"
services:
db_recipes:
restart: always
image: postgres:11-alpine
volumes:
- ./postgresql:/var/lib/postgresql/data
env_file:
- ./.env
networks:
- default
web_recipes:
image: vabene1111/recipes
restart: always
env_file:
- ./.env
volumes:
- staticfiles:/opt/recipes/staticfiles
- nginx_config:/opt/recipes/nginx/conf.d
- ./mediafiles:/opt/recipes/mediafiles
depends_on:
- db_recipes
networks:
- default
nginx_recipes:
image: nginx:mainline-alpine
restart: always
env_file:
- ./.env
volumes:
- nginx_config:/etc/nginx/conf.d:ro
- staticfiles:/static
- ./mediafiles:/media
labels: # traefik example labels
- "traefik.enable=true"
- "traefik.http.routers.recipes.rule=Host(`recipes.mydomain.com`, `recipes.myotherdomain.com`)"
- "traefik.http.routers.recipes.entrypoints=web_secure" # your https endpoint
- "traefik.http.routers.recipes.tls.certresolver=le_resolver" # your cert resolver
depends_on:
- web_recipes
networks:
- default
- traefik
networks:
default:
traefik: # This is you external traefik network
external: true
volumes:
nginx_config:
staticfiles:
```
~~~yaml
{% include "./docker/traefik-nginx/docker-compose.yml" %}
~~~
#### nginx-proxy
@@ -198,58 +107,9 @@ LETSENCRYPT_EMAIL=
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/nginx-proxy/docker-compose.yml
```
```yaml
version: "3"
services:
db_recipes:
restart: always
image: postgres:11-alpine
volumes:
- ./postgresql:/var/lib/postgresql/data
env_file:
- ./.env
networks:
- default
web_recipes:
image: vabene1111/recipes
restart: always
env_file:
- ./.env
volumes:
- staticfiles:/opt/recipes/staticfiles
- nginx_config:/opt/recipes/nginx/conf.d
- ./mediafiles:/opt/recipes/mediafiles
depends_on:
- db_recipes
networks:
- default
nginx_recipes:
image: nginx:mainline-alpine
restart: always
env_file:
- ./.env
depends_on:
- web_recipes
volumes:
- nginx_config:/etc/nginx/conf.d:ro
- staticfiles:/static
- ./mediafiles:/media
networks:
- default
- nginx-proxy
networks:
default:
nginx-proxy:
external:
name: nginx-proxy
volumes:
nginx_config:
staticfiles:
```
~~~yaml
{% include "./docker/nginx-proxy/docker-compose.yml" %}
~~~
## Additional Information

View File

@@ -4,12 +4,14 @@ metadata:
labels:
app: recipes
name: recipes-nginx-config
namespace: default
data:
nginx-config: |-
events {
worker_connections 1024;
}
http {
include mime.types;
server {
listen 80;
server_name _;
@@ -24,10 +26,5 @@ data:
location /media/ {
alias /media/;
}
# pass requests for dynamic content to gunicorn
location / {
proxy_set_header Host $host;
proxy_pass http://localhost:8080;
}
}
}

View File

@@ -0,0 +1,13 @@
kind: Secret
apiVersion: v1
metadata:
name: recipes
namespace: default
type: Opaque
data:
# echo -n 'db-password' | base64
postgresql-password: ZGItcGFzc3dvcmQ=
# echo -n 'postgres-user-password' | base64
postgresql-postgres-password: cG9zdGdyZXMtdXNlci1wYXNzd29yZA==
# echo -n 'secret-key' | sha256sum | awk '{ printf $1 }' | base64
secret-key: ODVkYmUxNWQ3NWVmOTMwOGM3YWUwZjMzYzdhMzI0Y2M2ZjRiZjUxOWEyZWQyZjMwMjdiZDMzYzE0MGE0ZjlhYQ==

View File

@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: recipes
namespace: default

View File

@@ -1,50 +0,0 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: recipes-db
labels:
app: recipes
type: local
tier: db
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
hostPath:
path: "/data/recipes/db"
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: recipes-media
labels:
app: recipes
type: local
tier: media
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
hostPath:
path: "/data/recipes/media"
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: recipes-static
labels:
app: recipes
type: local
tier: static
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
hostPath:
path: "/data/recipes/static"

View File

@@ -1,34 +1,13 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: recipes-db
labels:
app: recipes
spec:
selector:
matchLabels:
tier: db
storageClassName: manual
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: recipes-media
namespace: default
labels:
app: recipes
spec:
selector:
matchLabels:
tier: media
app: recipes
storageClassName: manual
accessModes:
- ReadWriteMany
- ReadWriteOnce
resources:
requests:
storage: 1Gi
@@ -37,16 +16,12 @@ apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: recipes-static
namespace: default
labels:
app: recipes
spec:
selector:
matchLabels:
tier: static
app: recipes
storageClassName: manual
accessModes:
- ReadWriteMany
- ReadWriteOnce
resources:
requests:
storage: 1Gi

View File

@@ -0,0 +1,142 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app: recipes
tier: database
name: recipes-postgresql
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: recipes
serviceName: recipes-postgresql
updateStrategy:
type: RollingUpdate
template:
metadata:
annotations:
backup.velero.io/backup-volumes: data
labels:
app: recipes
tier: database
name: recipes-postgresql
namespace: default
spec:
restartPolicy: Always
securityContext:
fsGroup: 999
serviceAccount: recipes
serviceAccountName: recipes
terminationGracePeriodSeconds: 30
containers:
- name: recipes-db
env:
- name: BITNAMI_DEBUG
value: "false"
- name: POSTGRESQL_PORT_NUMBER
value: "5432"
- name: POSTGRESQL_VOLUME_DIR
value: /bitnami/postgresql
- name: PGDATA
value: /bitnami/postgresql/data
- name: POSTGRES_USER
value: recipes
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: recipes
key: postgresql-password
- name: POSTGRESQL_POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: recipes
key: postgresql-postgres-password
- name: POSTGRES_DB
value: recipes
image: docker.io/bitnami/postgresql:11.5.0-debian-9-r60
imagePullPolicy: IfNotPresent
livenessProbe:
exec:
command:
- sh
- -c
- exec pg_isready -U "postgres" -d "wiki" -h 127.0.0.1 -p 5432
failureThreshold: 6
initialDelaySeconds: 30
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
ports:
- containerPort: 5432
name: postgresql
protocol: TCP
readinessProbe:
exec:
command:
- sh
- -c
- -e
- |
pg_isready -U "postgres" -d "wiki" -h 127.0.0.1 -p 5432
[ -f /opt/bitnami/postgresql/tmp/.initialized ]
failureThreshold: 6
initialDelaySeconds: 5
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
resources:
requests:
cpu: 250m
memory: 256Mi
securityContext:
runAsUser: 1001
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /bitnami/postgresql
name: data
dnsPolicy: ClusterFirst
initContainers:
- command:
- sh
- -c
- |
mkdir -p /bitnami/postgresql/data
chmod 700 /bitnami/postgresql/data
find /bitnami/postgresql -mindepth 0 -maxdepth 1 -not -name ".snapshot" -not -name "lost+found" | \
xargs chown -R 1001:1001
image: docker.io/bitnami/minideb:stretch
imagePullPolicy: Always
name: init-chmod-data
resources:
requests:
cpu: 250m
memory: 256Mi
securityContext:
runAsUser: 0
volumeMounts:
- mountPath: /bitnami/postgresql
name: data
restartPolicy: Always
securityContext:
fsGroup: 1001
serviceAccount: recipes
serviceAccountName: recipes
terminationGracePeriodSeconds: 30
updateStrategy:
type: RollingUpdate
volumeClaimTemplates:
- apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
namespace: default
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
volumeMode: Filesystem

View File

@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: recipes
tier: database
name: recipes-postgresql
namespace: default
spec:
ports:
- name: postgresql
port: 5432
protocol: TCP
targetPort: postgresql
selector:
app: recipes
tier: database
sessionAffinity: None
type: ClusterIP

View File

@@ -2,6 +2,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: recipes
namespace: default
labels:
app: recipes
environment: production
@@ -9,17 +10,78 @@ metadata:
spec:
replicas: 1
strategy:
type: RollingUpdate
type: Recreate
selector:
matchLabels:
app: recipes
environment: production
template:
metadata:
annotations:
backup.velero.io/backup-volumes: media,static
labels:
app: recipes
tier: frontend
environment: production
spec:
restartPolicy: Always
serviceAccount: recipes
serviceAccountName: recipes
initContainers:
- name: init-chmod-data
env:
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: recipes
key: secret-key
- name: DB_ENGINE
value: django.db.backends.postgresql_psycopg2
- name: POSTGRES_HOST
value: recipes-postgresql
- name: POSTGRES_PORT
value: "5432"
- name: POSTGRES_USER
value: postgres
- name: POSTGRES_DB
value: recipes
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: recipes
key: postgresql-postgres-password
image: vabene1111/recipes:1.0.1
imagePullPolicy: Always
resources:
requests:
cpu: 250m
memory: 64Mi
command:
- sh
- -c
- |
set -e
source venv/bin/activate
echo "Updating database"
python manage.py migrate
python manage.py collectstatic_js_reverse
python manage.py collectstatic --noinput
echo "Setting media file attributes"
chown -R 65534:65534 /opt/recipes/mediafiles
find /opt/recipes/mediafiles -type d | xargs -r chmod 755
find /opt/recipes/mediafiles -type f | xargs -r chmod 644
echo "Done"
securityContext:
runAsUser: 0
volumeMounts:
- mountPath: /opt/recipes/mediafiles
name: media
# mount as subPath due to lost+found on ext4 pvc
subPath: files
- mountPath: /opt/recipes/staticfiles
name: static
# mount as subPath due to lost+found on ext4 pvc
subPath: files
containers:
- name: recipes-nginx
image: nginx:latest
@@ -28,69 +90,94 @@ spec:
- containerPort: 80
protocol: TCP
name: http
- containerPort: 8080
protocol: TCP
name: gunicorn
resources:
requests:
cpu: 250m
memory: 64Mi
volumeMounts:
- mountPath: '/media'
- mountPath: /media
name: media
- mountPath: '/static'
# mount as subPath due to lost+found on ext4 pvc
subPath: files
- mountPath: /static
name: static
# mount as subPath due to lost+found on ext4 pvc
subPath: files
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx-config
readOnly: true
- name: recipes
image: 'vabene1111/recipes:latest'
image: vabene1111/recipes:1.0.1
imagePullPolicy: IfNotPresent
command:
- /opt/recipes/venv/bin/gunicorn
- -b
- :8080
- --access-logfile
- "-"
- --error-logfile
- "-"
- --log-level
- INFO
- recipes.wsgi
livenessProbe:
failureThreshold: 3
httpGet:
path: /
port: 8080
scheme: HTTP
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 8080
scheme: HTTP
periodSeconds: 30
resources:
requests:
cpu: 250m
memory: 64Mi
volumeMounts:
- mountPath: '/opt/recipes/mediafiles'
- mountPath: /opt/recipes/mediafiles
name: media
- mountPath: '/opt/recipes/staticfiles'
# mount as subPath due to lost+found on ext4 pvc
subPath: files
- mountPath: /opt/recipes/staticfiles
name: static
# mount as subPath due to lost+found on ext4 pvc
subPath: files
env:
- name: DEBUG
value: "0"
- name: ALLOWED_HOSTS
value: '*'
- name: SECRET_KEY
value: # CHANGEME
valueFrom:
secretKeyRef:
name: recipes
key: secret-key
- name: DB_ENGINE
value: django.db.backends.postgresql_psycopg2
- name: POSTGRES_HOST
value: localhost
value: recipes-postgresql
- name: POSTGRES_PORT
value: "5432"
- name: POSTGRES_USER
value: recipes
value: postgres
- name: POSTGRES_DB
value: recipes
- name: POSTGRES_PASSWORD
value: # CHANGEME
- name: recipes-db
image: 'postgres:latest'
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5432
volumeMounts:
- mountPath: '/var/lib/postgresql/data'
name: database
env:
- name: POSTGRES_USER
value: recipes
- name: POSTGRES_DB
value: recipes
- name: POSTGRES_PASSWORD
value: # CHANGEME
valueFrom:
secretKeyRef:
name: recipes
key: postgresql-postgres-password
securityContext:
runAsUser: 65534
volumes:
- name: database
persistentVolumeClaim:
claimName: recipes-db
- name: media
persistentVolumeClaim:
claimName: recipes-media

View File

@@ -2,14 +2,21 @@ apiVersion: v1
kind: Service
metadata:
name: recipes
namespace: default
labels:
app: recipes
tier: frontend
spec:
selector:
app: recipes
tier: frontend
environment: production
ports:
- port: 80
targetPort: http
name: http
protocol: TCP
- port: 8080
targetPort: gunicorn
name: gunicorn
protocol: TCP

View File

@@ -0,0 +1,38 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
#cert-manager.io/cluster-issuer: letsencrypt-prod
#kubernetes.io/ingress.class: nginx
name: recipes
namespace: default
spec:
rules:
- host: recipes.local
http:
paths:
- backend:
service:
name: recipes
port:
number: 8080
path: /
pathType: Prefix
- backend:
service:
name: recipes
port:
number: 80
path: /media
pathType: Prefix
- backend:
service:
name: recipes
port:
number: 80
path: /static
pathType: Prefix
#tls:
#- hosts:
# - recipes.local
# secretName: recipes-local-tls

View File

@@ -1,31 +1,98 @@
!!! info "Community Contributed"
This guide was contributed by the community and is neither officially supported, nor updated or tested.
**!!! info "Community Contributed" This guide was contributed by the community and is neither officially supported, nor updated or tested.**
This is a basic kubernetes setup.
Please note that this does not necessarily follow Kubernetes best practices and should only used as a
basis to build your own setup from!
# K8s Setup
All files con be found here in the Github Repo:
[docs/install/k8s](https://github.com/vabene1111/recipes/tree/develop/docs/install/k8s)
This is a setup which should be sufficent for production use. Be sure to replace the default secrets!
## Important notes
# Files
State (database, static files and media files) is handled via `PersistentVolumes`.
## 10-configmap.yaml
Note that you will most likely have to change the `PersistentVolumes` in `30-pv.yaml`. The current setup is only usable for a single-node cluster because it uses local storage on the kubernetes worker nodes under `/data/recipes/`. It should just serve as an example.
The nginx config map. This is loaded as nginx.conf in the nginx sidecar to configure nginx to deliver static content.
Currently, the deployment in `50-deployment.yaml` just pulls the `latest` tag of all containers. In a production setup, you should set this to a fixed version!
## 15-secrets.yaml
See env variables tagged with `CHANGEME` in `50-deployment.yaml` and make sure to change those! A better setup would use kubernetes secrets but this is not implemented yet.
The secrets **replace them!!** This file is only here for a quick start. Be aware that changing secrets after installation will be messy and is not documented here. **You should set new secrets before the installation.** As you are reading this document **before** the installation ;-)
## Updates
Create your own postgresql passwords and the secret key for the django app
These manifests are not tested against new versions.
see also [Managing Secrets using kubectl](https://kubernetes.io/docs/tasks/configmap-secret/managing-secret-using-kubectl/)
## Apply the manifets
**Replace** `db-password`, `postgres-user-password` and `secret-key` **with something - well - secret :-)**
To apply the manifest with `kubectl`, use the following command:
~~~
echo -n 'db-password' > ./db-password.txt
echo -n 'postgres-user-password' > ./postgres-password.txt
echo -n 'secret-key' | sha256sum | awk '{ printf $1 }' > ./secret-key.txt
~~~
```
Delete the default secrets file `15-secrets.yaml` and generate the K8s secret from your files.
~~~
kubectl create secret generic recipes \
--from-file=postgresql-password=./db-password.txt \
--from-file=postgresql-postgres-password=./postgres-password.txt \
--from-file=secret-key=./secret-key.txt
~~~
## 20-service-account.yml
Creating service account `recipes` for deployment and stateful set.
## 30-pvc.yaml
The creation of the persistent volume claims for media and static content. May you want to increase the size. This expects to have a storage class installed.
## 40-sts-postgresql.yaml
The PostgreSQL stateful set, based on a bitnami image. It runs a init container as root to do the preparations. The postgres container itsef runs as a lower privileged user. The recipes app uses the database super user (postgres) as the recipies app is doing some db migrations on startup, which needs super user privileges.
## 45-service-db.yaml
Creating the database service.
## 50-deployment.yaml
The deployment first fires up a init container to do the database migrations and file modifications. This init container runs as root. The init conainer runs part of the [boot.sh](https://github.com/TandoorRecipes/recipes/blob/develop/boot.sh) script from the `vabene1111/recipes` image.
The deployment then runs two containers, the recipes-nginx and the recipes container which runs the gunicorn app. The nginx container gets it's nginx.conf via config map to deliver static content `/static` and `/media`. The guincorn container gets it's secret key and the database password from the secret `recipes`. `gunicorn` runs as user `nobody`.
## 60-service.yaml
Creating the app service.
## 70-ingress.yaml
Setting up the ingress for the recipes service. Requests for static content `/static` and `/media` are send to the nginx container, everything else to gunicorn. TLS setup via cert-manager is prepared. You have to **change the host** from `recipes.local` to your specific domain.
# Conclusion
All in all:
- The database is set up as a stateful set.
- The database container runs as a low privileged user.
- Database and application use secrets.
- The application also runs as a low privileged user.
- nginx runs as root but forks children with a low privileged user.
- There's an ingress rule to access the application from outside.
I tried the setup with [kind](https://kind.sigs.k8s.io/) and it runs well on my local cluster.
There is a warning, when you check your system as super user:
**Media Serving Warning**
Serving media files directly using gunicorn/python is not recommend! Please follow the steps described here to update your installation.
I don't know how this check works, but this warning is simply wrong! ;-) Media and static files are routed by ingress to the nginx container - I promise :-)
# Updates
These manifests are tested against Release 1.0.1. Newer versions may not work without changes.
# Apply the manifets
To apply the manifest with kubectl, use the following command:
~~~
kubectl apply -f ./docs/k8s/
```
~~~

21
docs/install/kubesail.md Normal file
View File

@@ -0,0 +1,21 @@
!!! info "Community Contributed"
This guide was contributed by the community and is neither officially supported, nor updated or tested.
[KubeSail](https://kubesail.com/) lets you install Tandoor by providing a simple web interface for installing and managing apps. You can connect any server running Kubernetes, or get a pre-configured [PiBox](https://pibox.io).
<!-- A portion of every PiBox sale goes toward supporting Tandoor development. -->
The KubeSail template is closely based on the [Kubernetes installation]([docs/install/k8s](https://github.com/vabene1111/recipes/tree/develop/docs/install/k8s)) configs
## Quick Start
Load the [Tandoor Recipes](https://kubesail.com/template/PastuDan/Tandoor%20Recipes) template, and click **Launch Template**.
If you have not yet attached your server to KubeSail, see the [Getting a Cluster](https://docs.kubesail.com/guides/bare-metal/) section on the KubeSail docs.
## Important notes
In the "Template Variables" section you will see two input fields. These should show `RANDOM(16)`, indicating they will be randomly generated and specific to your install when you launch the template. If you prefer to set these yourself, you can type them in before launching the template.
![image](https://user-images.githubusercontent.com/1296162/140431276-b823ba1c-175c-436a-9ed9-35bc62f8744e.png)

View File

@@ -3,7 +3,7 @@
These intructions are inspired from a standard django/gunicorn/postgresql instructions ([for example](https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-16-04))
!!! warning
Be sure to use pyton3.9 and pip related to python 3.9. Depending on your distribution calling `python` or `pip` will use python2 instead of pyton 3.9.
Be sure to use python 3.9 and pip related to python 3.9. Depending on your distribution calling `python` or `pip` will use python2 instead of python 3.9.
## Prerequisites

View File

@@ -21,6 +21,9 @@ markdown_extensions:
- pymdownx.highlight
- pymdownx.superfences
plugins:
- include-markdown
nav:
- Home: 'index.md'
- Installation:
@@ -28,6 +31,7 @@ nav:
- Unraid: install/unraid.md
- Synology: install/synology.md
- Kubernetes: install/kubernetes.md
- KubeSail or PiBox: install/kubesail.md
- Manual: install/manual.md
- Other setups: install/other.md
- Features:
@@ -41,4 +45,4 @@ nav:
- Permission System: system/permissions.md
- Backup: system/backup.md
- Contributing: contribute.md
- FAQ: faq.md
- FAQ: faq.md

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