Compare commits

..

125 Commits
2.2.6 ... beta

Author SHA1 Message Date
vabene1111
d26faf14b1 Merge branch 'develop' into beta 2025-11-11 15:42:55 +01:00
vabene1111
d5d5c2c52b Merge pull request #4159 from erikbledsoe/patch-1
documentation typo
2025-11-11 15:42:05 +01:00
vabene1111
7ffabfe711 Merge pull request #4166 from EifX/docs/apache-reverse-proxy
feat: add apache reverse proxy documentation
2025-11-11 15:41:13 +01:00
vabene1111
49e0b5b962 improve handling of vuetify locale 2025-11-11 15:40:50 +01:00
vabene1111
a05f1ece24 updated recipe scrapers 2025-11-11 14:45:34 +01:00
vabene1111
748b91bb8a improve servings parsing and AI failure logging 2025-11-11 14:44:09 +01:00
vabene1111
bd2e9cc3d9 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2025-11-11 14:16:37 +01:00
vabene1111
c40bb20a7a fixed copying meal plans did not add to shopping 2025-11-11 14:16:32 +01:00
vabene1111
b377d2cd35 Merge pull request #4192 from Daan5556/patch-1
Docs: Added trailing slash to `system` path
2025-11-11 14:12:17 +01:00
vabene1111
dc0e91d0f9 Merge pull request #4193 from ThomasLeister/patch-1
Update manual setup instructions: vue is now vue3
2025-11-11 14:11:26 +01:00
vabene1111
5f12907544 fixed edge case where no recipe is displayed in a mostly empty space 2025-11-11 14:09:02 +01:00
vabene1111
889ddac7dc remove debug button from message dialog and added button to open it 2025-11-11 13:41:14 +01:00
vabene1111
b369e2618a fixed decimals in share view 2025-11-11 13:06:56 +01:00
vabene1111
5a4e0204c9 fixed debug toolbar setup 2025-11-11 12:59:45 +01:00
vabene1111
bfc2e96b54 Merge pull request #4101 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vuetify-3.10.3
Bump vuetify from 3.9.7 to 3.10.3 in /vue3
2025-11-11 12:51:23 +01:00
vabene1111
f065ef80aa Merge pull request #4100 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vueuse/router-13.9.0
Bump @vueuse/router from 13.6.0 to 13.9.0 in /vue3
2025-11-11 12:51:11 +01:00
vabene1111
61c14b8b05 Merge pull request #4099 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vue/tsconfig-0.8.1
Bump @vue/tsconfig from 0.7.0 to 0.8.1 in /vue3
2025-11-11 12:51:03 +01:00
vabene1111
35d5d64809 Merge pull request #4096 from TandoorRecipes/dependabot/pip/lxml-6.0.2
Bump lxml from 5.3.1 to 6.0.2
2025-11-11 12:50:52 +01:00
vabene1111
63c711d18c Merge pull request #4094 from TandoorRecipes/dependabot/pip/pytest-8.4.2
Bump pytest from 8.4.1 to 8.4.2
2025-11-11 12:50:42 +01:00
vabene1111
59e3ea70d1 Merge pull request #4095 from TandoorRecipes/dependabot/pip/django-debug-toolbar-6.0.0
Bump django-debug-toolbar from 4.3.0 to 6.0.0
2025-11-11 12:50:19 +01:00
vabene1111
6771662a9f Merge pull request #4092 from TandoorRecipes/dependabot/pip/pyyaml-6.0.3
Bump pyyaml from 6.0.2 to 6.0.3
2025-11-11 12:49:29 +01:00
vabene1111
9b792a1393 Merge pull request #4093 from TandoorRecipes/dependabot/pip/drf-spectacular-0.28.0
Bump drf-spectacular from 0.27.1 to 0.28.0
2025-11-11 12:49:19 +01:00
vabene1111
862957c121 Merge pull request #4098 from TandoorRecipes/dependabot/github_actions/actions/setup-python-6
Bump actions/setup-python from 5 to 6
2025-11-11 12:48:50 +01:00
vabene1111
bdcbafd52f Merge pull request #4200 from TandoorRecipes/dependabot/github_actions/github/codeql-action-4
Bump github/codeql-action from 3 to 4
2025-11-11 12:48:26 +01:00
vabene1111
5e454a5212 Merge pull request #4201 from TandoorRecipes/dependabot/github_actions/actions/setup-node-6
Bump actions/setup-node from 4 to 6
2025-11-11 12:48:18 +01:00
vabene1111
20bea63997 Merge pull request #4202 from TandoorRecipes/dependabot/github_actions/awalsh128/cache-apt-pkgs-action-1.6.0
Bump awalsh128/cache-apt-pkgs-action from 1.5.3 to 1.6.0
2025-11-11 12:48:09 +01:00
vabene1111
8a265772c0 Merge pull request #4211 from TandoorRecipes/dependabot/pip/django-5.2.8
Bump django from 5.2.7 to 5.2.8
2025-11-11 12:47:16 +01:00
vabene1111
6febb4e3e8 Merge pull request #4174 from TandoorRecipes/dependabot/npm_and_yarn/vue3/vite-7.1.11
Bump vite from 7.1.5 to 7.1.11 in /vue3
2025-11-11 12:47:00 +01:00
vabene1111
04f9167fd8 Merge branch 'develop-weblate' into develop
# Conflicts:
#	cookbook/locale/nl/LC_MESSAGES/django.po
#	cookbook/locale/uk/LC_MESSAGES/django.po
#	vue3/src/locales/de.json
#	vue3/src/locales/en.json
#	vue3/src/locales/nl.json
#	vue3/src/locales/sl.json
#	vue3/src/locales/uk.json
2025-11-11 12:27:48 +01:00
dependabot[bot]
8f29e01daf Bump django from 5.2.7 to 5.2.8
Bumps [django](https://github.com/django/django) from 5.2.7 to 5.2.8.
- [Commits](https://github.com/django/django/compare/5.2.7...5.2.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-05 20:17:31 +00:00
Erik Bledsoe
e810363b22 Merge branch 'develop' into patch-1 2025-11-01 07:05:19 -04:00
tan
b5a2120bdf Added translation using Weblate (Korean) 2025-11-01 10:09:09 +00:00
tan
643fcbad9b Added translation using Weblate (Korean) 2025-11-01 10:09:09 +00:00
dependabot[bot]
4a3b834463 Bump awalsh128/cache-apt-pkgs-action from 1.5.3 to 1.6.0
Bumps [awalsh128/cache-apt-pkgs-action](https://github.com/awalsh128/cache-apt-pkgs-action) from 1.5.3 to 1.6.0.
- [Release notes](https://github.com/awalsh128/cache-apt-pkgs-action/releases)
- [Commits](https://github.com/awalsh128/cache-apt-pkgs-action/compare/v1.5.3...v1.6.0)

---
updated-dependencies:
- dependency-name: awalsh128/cache-apt-pkgs-action
  dependency-version: 1.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 00:02:12 +00:00
dependabot[bot]
003149133a Bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 00:02:10 +00:00
dependabot[bot]
a43de0ca4d Bump github/codeql-action from 3 to 4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 00:02:06 +00:00
Thomas Leister
e05aaed75c Update manual setup instructions: vue is now vue3
The documentation has not been updated: "vue" directory is now "vue3".
2025-10-27 13:36:57 +01:00
Daan5556
4984e3e31b Added trailing slash to system path 2025-10-27 13:03:53 +01:00
dependabot[bot]
11dce4c6ad Bump vite from 7.1.5 to 7.1.11 in /vue3
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.5 to 7.1.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.11
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 22:56:53 +00:00
Alexander Eifler
8d0d338ea2 feat: add apache reverse proxy documentation 2025-10-15 20:56:39 +02:00
vabene1111
d09e629415 fixed input type 2025-10-15 15:10:59 +02:00
vabene1111
53ef2ef99f fixed ingredient input decimals 2025-10-15 15:02:19 +02:00
Erik Bledsoe
d7b26d1b29 documentation typo 2025-10-13 13:44:15 -04:00
vabene1111
673d12d233 increased default max body size for file uploads to 512 MB 2025-10-11 11:57:32 +02:00
vabene1111
6359245925 Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2025-10-11 11:56:51 +02:00
vabene1111
a7c4822322 fixed cannoit create steps if none are present 2025-10-11 11:56:47 +02:00
vabene1111
e94419f320 Merge pull request #4138 from KoMa1012/KoMa1012-gunicorn-timeoutconfig
Update boot.sh
2025-10-11 11:55:48 +02:00
vabene1111
01f46483ff clearer batch delete warning 2025-10-11 11:52:52 +02:00
vabene1111
d6da5688af fixed 0 servings 2025-10-11 11:47:10 +02:00
vabene1111
680ae39201 fixed import button not switching to loading in app import 2025-10-11 11:38:58 +02:00
vabene1111
2472ee9c26 fixed api setting example 2025-10-11 11:05:20 +02:00
vabene1111
4428b06d4a fixed properties edge case with missing conversions 2025-10-11 11:04:00 +02:00
vabene1111
e9c38d7d5e added bottom margin to properties editor 2025-10-11 09:22:43 +02:00
vabene1111
6f28d58807 fixed and imporved food properties 2025-10-11 09:20:58 +02:00
vabene1111
88db611f0a Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2025-10-11 09:02:14 +02:00
vabene1111
f3302b4014 fixed start page and ingredient templater 2025-10-11 09:02:10 +02:00
vabene1111
d4bb161275 Merge pull request #4147 from TandoorRecipes/dependabot/pip/python-ldap-3.4.5
Bump python-ldap from 3.4.4 to 3.4.5
2025-10-11 08:36:03 +02:00
dependabot[bot]
32f1538938 Bump python-ldap from 3.4.4 to 3.4.5
Bumps [python-ldap](https://github.com/python-ldap/python-ldap) from 3.4.4 to 3.4.5.
- [Release notes](https://github.com/python-ldap/python-ldap/releases)
- [Changelog](https://github.com/python-ldap/python-ldap/blob/python-ldap-3.4.5/CHANGES)
- [Commits](https://github.com/python-ldap/python-ldap/compare/python-ldap-3.4.4...python-ldap-3.4.5)

---
updated-dependencies:
- dependency-name: python-ldap
  dependency-version: 3.4.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-10 22:55:18 +00:00
KoMa1012
029baea4c7 Update configuration.md
added explanation for GUNICORN_TIMEOUT environmental variable
2025-10-09 12:51:25 +02:00
KoMa1012
38d1b7cef5 Update boot.sh
made gunicorn timout configurable via environmental variable GUNICORN_TIMEOUT this can help in scenarios where e.g. the LLM takes too much time to answer when using a local LLM
2025-10-08 20:09:51 +02:00
vabene1111
85821bcc94 nginx config update 2025-10-08 07:42:09 +02:00
vabene1111
2345af8fd6 updated django 2025-10-05 13:11:34 +02:00
vabene1111
51107c64ee fixed unit merge with duplicate conversion 2025-10-05 13:10:38 +02:00
vabene1111
81983c5ae2 fixed default unit for first ingredient 2025-10-05 13:06:57 +02:00
vabene1111
f7713a43a7 fxied recipe properties editor and added recipe AI properties 2025-10-05 12:55:44 +02:00
vabene1111
ffd951a7f4 Merge branch 'develop' of https://github.com/TandoorRecipes/recipes into develop 2025-10-05 09:39:58 +02:00
vabene1111
319ac8e191 fixed recipe properties editor 2025-10-05 09:39:54 +02:00
dependabot[bot]
e292b72e34 Bump vuetify from 3.9.7 to 3.10.3 in /vue3
Bumps [vuetify](https://github.com/vuetifyjs/vuetify/tree/HEAD/packages/vuetify) from 3.9.7 to 3.10.3.
- [Release notes](https://github.com/vuetifyjs/vuetify/releases)
- [Commits](https://github.com/vuetifyjs/vuetify/commits/v3.10.3/packages/vuetify)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 00:34:43 +00:00
dependabot[bot]
4e795ecf55 Bump @vueuse/router from 13.6.0 to 13.9.0 in /vue3
Bumps [@vueuse/router](https://github.com/vueuse/vueuse/tree/HEAD/packages/router) from 13.6.0 to 13.9.0.
- [Release notes](https://github.com/vueuse/vueuse/releases)
- [Commits](https://github.com/vueuse/vueuse/commits/v13.9.0/packages/router)

---
updated-dependencies:
- dependency-name: "@vueuse/router"
  dependency-version: 13.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 00:34:23 +00:00
dependabot[bot]
e3c2a66723 Bump @vue/tsconfig from 0.7.0 to 0.8.1 in /vue3
Bumps [@vue/tsconfig](https://github.com/vuejs/tsconfig) from 0.7.0 to 0.8.1.
- [Release notes](https://github.com/vuejs/tsconfig/releases)
- [Commits](https://github.com/vuejs/tsconfig/compare/v0.7.0...v0.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 00:33:58 +00:00
dependabot[bot]
eec3e97f97 Bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 00:04:39 +00:00
dependabot[bot]
3f481d6922 Bump lxml from 5.3.1 to 6.0.2
Bumps [lxml](https://github.com/lxml/lxml) from 5.3.1 to 6.0.2.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-5.3.1...lxml-6.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 00:01:38 +00:00
dependabot[bot]
0810ab7210 Bump django-debug-toolbar from 4.3.0 to 6.0.0
Bumps [django-debug-toolbar](https://github.com/django-commons/django-debug-toolbar) from 4.3.0 to 6.0.0.
- [Release notes](https://github.com/django-commons/django-debug-toolbar/releases)
- [Changelog](https://github.com/django-commons/django-debug-toolbar/blob/main/docs/changes.rst)
- [Commits](https://github.com/django-commons/django-debug-toolbar/compare/4.3...6.0.0)

---
updated-dependencies:
- dependency-name: django-debug-toolbar
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 00:01:32 +00:00
dependabot[bot]
abd621145c Bump pytest from 8.4.1 to 8.4.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.1 to 8.4.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.1...8.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 00:01:29 +00:00
dependabot[bot]
7d218aa93d Bump drf-spectacular from 0.27.1 to 0.28.0
Bumps [drf-spectacular](https://github.com/tfranzel/drf-spectacular) from 0.27.1 to 0.28.0.
- [Release notes](https://github.com/tfranzel/drf-spectacular/releases)
- [Changelog](https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tfranzel/drf-spectacular/compare/0.27.1...0.28.0)

---
updated-dependencies:
- dependency-name: drf-spectacular
  dependency-version: 0.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 00:01:24 +00:00
dependabot[bot]
1b41bd9115 Bump pyyaml from 6.0.2 to 6.0.3
Bumps [pyyaml](https://github.com/yaml/pyyaml) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/yaml/pyyaml/releases)
- [Changelog](https://github.com/yaml/pyyaml/blob/6.0.3/CHANGES)
- [Commits](https://github.com/yaml/pyyaml/compare/6.0.2...6.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 00:01:21 +00:00
vabene1111
d456fcf0f2 Merge branch 'develop' into beta 2025-09-30 21:51:53 +02:00
vabene1111
aea247b4a3 Merge pull request #4091 from c0mputerguru/ical-test-fix
Requests using date should be in local timezone, not UTC as tandoor is timezone aware
2025-09-30 21:45:11 +02:00
vabene1111
e2843bb02f if at least one reciep 2025-09-30 21:41:20 +02:00
vabene1111
e3aa3e1137 always show at least one random recipe slider 2025-09-30 21:41:04 +02:00
vabene1111
da1187b03a fixed date editor missing from cook log editor 2025-09-30 21:39:35 +02:00
vabene1111
f9ed79978c improved mealie 1 importer 2025-09-30 21:36:37 +02:00
vabene1111
920a3ed4a3 fixed times cooked filter 2025-09-30 21:07:01 +02:00
vabene1111
2077eae142 fixed step sorter for import page 2025-09-30 20:55:12 +02:00
vabene1111
b1ef35e415 added default ordering for most models 2025-09-30 20:47:44 +02:00
vabene1111
0a687d840c fixed ingredients missing in sub recipe steps 2025-09-30 20:25:09 +02:00
vabene1111
6a3034b966 fixed merging in ingredient editor 2025-09-30 20:17:43 +02:00
Anand Patel
3d7afbfe4f Requests using date should be in local timezone, not UTC as tandoor is timezone aware. 2025-09-30 16:44:05 +00:00
vabene1111
02e43730bd fixed unit conversion division by 0 2025-09-29 22:14:19 +02:00
vabene1111
6adf077ee5 removed outside guincorn binding 2025-09-29 21:37:41 +02:00
vabene1111
d73ffa46ff added auto demo login link to docs index page 2025-09-29 21:21:13 +02:00
vabene1111
8572f338ad fixed ingredient insert focus error 2025-09-25 21:03:35 +02:00
vabene1111
920ec8e74b fixed missing pg extensions 2025-09-25 20:56:53 +02:00
vabene1111
2328bf2342 fixed mealie edgecases 2025-09-25 20:48:20 +02:00
vabene1111
85620a1431 Merge branch 'master' into develop 2025-09-25 12:33:43 +02:00
S
d4f654554b Translated using Weblate (Ukrainian)
Currently translated at 43.9% (380 of 864 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/uk/
2025-09-23 19:45:37 +00:00
Matjaž T.
c8115545b8 Translated using Weblate (Slovenian)
Currently translated at 100.0% (864 of 864 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/sl/
2025-09-23 19:45:37 +00:00
Justin Straver
6dbf0871ec Translated using Weblate (Dutch)
Currently translated at 99.5% (860 of 864 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nl/
2025-09-23 19:45:36 +00:00
Justin Straver
f1c5c8bc43 Translated using Weblate (German)
Currently translated at 99.1% (857 of 864 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/
2025-09-23 19:45:36 +00:00
S
22e0108992 Translated using Weblate (Ukrainian)
Currently translated at 18.6% (91 of 488 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/uk/
2025-09-23 19:45:36 +00:00
Justin Straver
e2e05c8d1d Translated using Weblate (Dutch)
Currently translated at 100.0% (488 of 488 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/nl/
2025-09-23 19:45:36 +00:00
Justin Straver
b02b36812d Translated using Weblate (English)
Currently translated at 100.0% (864 of 864 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/en/
2025-09-23 19:45:36 +00:00
vabene1111
7f6025c99c Merge branch 'develop' into beta 2025-09-22 22:01:33 +02:00
vabene1111
b97e04ead8 Merge branch 'develop' into beta 2025-09-09 07:55:14 +02:00
vabene1111
fc236c97b4 Merge branch 'develop' into beta 2025-09-08 22:16:03 +02:00
vabene1111
5653aca056 Merge branch 'develop' into beta 2025-08-31 12:38:21 +02:00
vabene1111
fdb05c5a9e Merge branch 'develop' into beta 2025-08-22 16:58:40 +02:00
vabene1111
2dffde4091 Merge branch 'develop' into beta 2025-08-16 15:22:05 +02:00
vabene1111
cdd700d2e6 Merge branch 'develop' into beta 2025-07-29 17:37:48 +02:00
vabene1111
ad6fe5fa4d Merge branch 'develop' into beta 2025-07-18 15:48:02 +02:00
vabene1111
ac31c112f3 Merge branch 'develop' into beta 2025-07-11 21:59:18 +02:00
vabene1111
0104b600cc Merge branch 'develop' into beta 2025-07-07 18:28:50 +02:00
vabene1111
7baad85112 Merge branch 'develop' into beta 2025-06-22 10:35:01 +02:00
vabene1111
4b0bfa9a85 Merge branch 'master' into beta 2025-06-22 10:29:43 +02:00
vabene1111
5e7c75ef68 Merge branch 'develop' into beta 2025-01-18 09:24:08 +01:00
vabene1111
954a35bea2 Merge branch 'develop' into beta 2025-01-01 08:17:32 +01:00
vabene1111
88347d44c8 Merge branch 'beta' of https://github.com/TandoorRecipes/recipes into beta 2024-11-23 21:56:13 +01:00
vabene1111
2c13e76fbb Merge branch 'develop' into beta 2024-03-05 08:54:58 +01:00
vabene1111
362f634828 Merge branch 'develop' into beta 2024-03-02 07:41:28 +01:00
vabene1111
2fb968cfd3 Merge branch 'develop' into beta 2024-03-01 07:42:28 +01:00
vabene1111
4d3dab6edd Merge branch 'develop' into beta 2024-02-28 17:21:22 +01:00
vabene1111
8f1b593ad1 Merge branch 'develop' into beta 2024-02-28 17:19:15 +01:00
vabene1111
1002f0d61f Merge branch 'develop' into beta 2024-02-28 17:12:35 +01:00
vabene1111
20cb218688 Merge branch 'develop' into beta 2024-02-26 16:29:16 +01:00
vabene1111
bba44b0c1e Merge branch 'develop' into beta 2024-02-20 07:54:28 +01:00
91 changed files with 8844 additions and 4342 deletions

View File

@@ -35,7 +35,7 @@ jobs:
fi
# Build Vue 3 frontend
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: '22'
cache: yarn

View File

@@ -13,14 +13,14 @@ jobs:
node-version: ["22"]
steps:
- uses: actions/checkout@v5
- uses: awalsh128/cache-apt-pkgs-action@v1.5.3
- uses: awalsh128/cache-apt-pkgs-action@v1.6.0
with:
packages: libsasl2-dev python3-dev libxml2-dev libxmlsec1-dev libxslt-dev libxmlsec1-openssl libxslt-dev libldap2-dev libssl-dev gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev openssl-dev libffi-dev cargo openldap-dev python3-dev xmlsec-dev xmlsec build-base g++ curl
version: 1.0
# Setup python & dependencies
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
@@ -43,7 +43,7 @@ jobs:
# Build Vue frontend & Dependencies
- name: Set up Node ${{ matrix.node-version }}
if: steps.django_cache.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"

View File

@@ -25,7 +25,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
# Override language selection by uncommenting this and choosing your languages
with:
languages: python, javascript
@@ -47,6 +47,6 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
languages: javascript, python

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: 3.x
- run: pip install mkdocs-material mkdocs-include-markdown-plugin

16
boot.sh
View File

@@ -2,7 +2,7 @@
source venv/bin/activate
# these are envsubst in the nginx config, make sure they default to something sensible when unset
export TANDOOR_PORT="${TANDOOR_PORT:-8080}"
export TANDOOR_PORT="${TANDOOR_PORT:-80}"
export MEDIA_ROOT=${MEDIA_ROOT:-/opt/recipes/mediafiles};
export STATIC_ROOT=${STATIC_ROOT:-/opt/recipes/staticfiles};
@@ -12,11 +12,6 @@ GUNICORN_LOG_LEVEL="${GUNICORN_LOG_LEVEL:-'info'}"
PLUGINS_BUILD="${PLUGINS_BUILD:-0}"
if [ "${TANDOOR_PORT}" -eq 80 ]; then
echo "TANDOOR_PORT set to 8080 because 80 is now taken by the integrated nginx"
TANDOOR_PORT=8080
fi
display_warning() {
echo "[WARNING]"
echo -e "$1"
@@ -29,7 +24,6 @@ envsubst '$MEDIA_ROOT $STATIC_ROOT $TANDOOR_PORT' < /opt/recipes/http.d/Recipes.
echo "Starting nginx"
nginx
echo "Checking configuration..."
# SECRET_KEY (or a valid file at SECRET_KEY_FILE) must be set in .env file
@@ -110,9 +104,5 @@ chmod -R 755 ${MEDIA_ROOT:-/opt/recipes/mediafiles}
ipv6_disable=$(cat /sys/module/ipv6/parameters/disable)
echo "Starting gunicorn"
# Check if IPv6 is enabled, only then run gunicorn with ipv6 support
if [ "$ipv6_disable" -eq 0 ]; then
exec gunicorn -b "[::]:$TANDOOR_PORT" --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi
else
exec gunicorn -b ":$TANDOOR_PORT" --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi
fi
exec gunicorn --bind unix:/run/tandoor.sock --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --timeout ${GUNICORN_TIMEOUT:-30} --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi

View File

@@ -48,7 +48,7 @@ class FoodPropertyHelper:
found_property = False
# if food has a value for the given property type (no matter if conversion is possible)
has_property_value = False
if i.food.properties_food_amount == 0 or i.food.properties_food_unit is None and not (i.amount == 0 or i.no_amount): # if food is configured incorrectly
if (i.food.properties_food_amount == 0 or i.food.properties_food_unit is None) and not (i.amount == 0 or i.no_amount): # if food is configured incorrectly
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': None}
computed_properties[pt.id]['missing_value'] = True
else:
@@ -63,8 +63,9 @@ class FoodPropertyHelper:
computed_properties[p.property_type.id]['food_values'], c.food.id, (c.amount / i.food.properties_food_amount) * p.property_amount, c.food)
if not found_property:
# if no amount and food does not exist yet add it but don't count as missing
if i.amount == 0 or i.no_amount and i.food.id not in computed_properties[pt.id]['food_values']:
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': 0}
if i.amount == 0 or i.no_amount:
if i.food.id not in computed_properties[pt.id]['food_values']:
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': 0}
# if amount is present but unit is missing indicate it in the result
elif i.unit is None:
if i.food.id not in computed_properties[pt.id]['food_values']:
@@ -72,7 +73,8 @@ class FoodPropertyHelper:
computed_properties[pt.id]['food_values'][i.food.id]['missing_unit'] = True
else:
computed_properties[pt.id]['missing_value'] = True
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': None}
if i.food.id not in computed_properties[pt.id]['food_values']:
computed_properties[pt.id]['food_values'][i.food.id] = {'id': i.food.id, 'food': {'id': i.food.id, 'name': i.food.name}, 'value': None}
if has_property_value and i.unit is not None:
computed_properties[pt.id]['food_values'][i.food.id]['missing_conversion'] = {'base_unit': {'id': i.unit.id, 'name': i.unit.name}, 'converted_unit': {'id': i.food.properties_food_unit.id, 'name': i.food.properties_food_unit.name}}
@@ -82,8 +84,12 @@ class FoodPropertyHelper:
# TODO move to central helper ? --> use defaultdict
@staticmethod
def add_or_create(d, key, value, food):
if key in d and d[key]['value']:
d[key]['value'] += value
if key in d:
# value can be None if a previous instance of the same food was missing a conversion
if d[key]['value']:
d[key]['value'] += value
else:
d[key]['value'] = value
else:
d[key] = {'id': food.id, 'food': {'id': food.id, 'name': food.name}, 'value': value}
return d

View File

@@ -326,7 +326,7 @@ class RecipeSearch():
def _favorite_recipes(self):
if self._sort_includes('favorite') or self._timescooked or self._timescooked_gte or self._timescooked_lte:
less_than = self._timescooked_lte and not self._sort_includes('-favorite')
if less_than:
if less_than or self._timescooked == 0:
default = 1000
else:
default = 0
@@ -339,7 +339,7 @@ class RecipeSearch():
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
if self._timescooked:
self._queryset = self._queryset.filter(favorite=0)
self._queryset = self._queryset.filter(favorite=self._timescooked)
elif self._timescooked_lte:
self._queryset = self._queryset.filter(favorite__lte=int(self._timescooked_lte)).exclude(favorite=0)
elif self._timescooked_gte:

View File

@@ -69,15 +69,8 @@ def get_from_scraper(scrape, request):
recipe_json['description'] = parse_description(description)
recipe_json['description'] = automation_engine.apply_regex_replace_automation(recipe_json['description'], Automation.DESCRIPTION_REPLACE)
# assign servings attributes
try:
# dont use scrape.yields() as this will always return "x servings" or "x items", should be improved in scrapers directly
servings = scrape.schema.data.get('recipeYield') or 1
except Exception:
servings = 1
recipe_json['servings'] = parse_servings(servings)
recipe_json['servings_text'] = parse_servings_text(servings)
recipe_json['servings'] = parse_servings(scrape.schema.data.get('recipeYield'))
recipe_json['servings_text'] = parse_servings_text(scrape.schema.data.get('recipeYield'))
# assign time attributes
try:
@@ -406,7 +399,7 @@ def parse_servings(servings):
def parse_servings_text(servings):
if isinstance(servings, str):
try:
servings = re.sub("\\d+", '', servings).strip()
servings = re.sub("\\d+", '', servings, 1).strip()
except Exception:
servings = ''
if isinstance(servings, list):

View File

@@ -135,8 +135,9 @@ class UnitConversionHelper:
:param food: base food
:return: converted ingredient object from base amount/unit/food
"""
if uc.food is None or uc.food == food:
if (uc.food is None or uc.food == food) and uc.converted_amount > 0 and uc.base_amount > 0:
if unit == uc.base_unit:
return Ingredient(amount=amount * (uc.converted_amount / uc.base_amount), unit=uc.converted_unit, food=food, space=self.space)
else:
return Ingredient(amount=amount * (uc.base_amount / uc.converted_amount), unit=uc.base_unit, food=food, space=self.space)
return None

View File

@@ -128,6 +128,7 @@ class Mealie1(Integration):
steps_relation = []
first_step_of_recipe_dict = {}
step_id_dict = {}
for s in mealie_database['recipe_instructions']:
if s['recipe_id'] in recipes_dict:
step = Step.objects.create(instruction=(s['text'] if s['text'] else "") + (f" \n {s['summary']}" if s['summary'] else ""),
@@ -135,9 +136,20 @@ class Mealie1(Integration):
name=s['title'],
space=self.request.space)
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[s['recipe_id']], step_id=step.pk))
step_id_dict[s["id"]] = step.pk
if s['recipe_id'] not in first_step_of_recipe_dict:
first_step_of_recipe_dict[s['recipe_id']] = step.pk
# it is possible for a recipe to not have steps but have ingredients, in that case create an empty step to add them to later
for r in recipes_dict.keys():
if r not in first_step_of_recipe_dict:
step = Step.objects.create(instruction='',
order=0,
name='',
space=self.request.space)
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[r], step_id=step.pk))
first_step_of_recipe_dict[r] = step.pk
for n in mealie_database['notes']:
if n['recipe_id'] in recipes_dict:
step = Step.objects.create(instruction=n['text'],
@@ -153,6 +165,11 @@ class Mealie1(Integration):
self.import_log.msg += f"Importing {len(mealie_database["recipes_ingredients"])} ingredients...\n"
self.import_log.save()
# mealie stores the reference to a step (instruction) from an ingredient (reference) in the recipe_ingredient_ref_link table
recipe_ingredient_ref_link_dict = {}
for ref in mealie_database['recipe_ingredient_ref_link']:
recipe_ingredient_ref_link_dict[ref["reference_id"]] = ref["instruction_id"]
ingredients_relation = []
for i in mealie_database['recipes_ingredients']:
if i['recipe_id'] in recipes_dict:
@@ -162,18 +179,18 @@ class Mealie1(Integration):
is_header=True,
space=self.request.space,
)
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=title_ingredient.pk))
ingredients_relation.append(Step.ingredients.through(step_id=get_step_id(i, first_step_of_recipe_dict, step_id_dict,recipe_ingredient_ref_link_dict), ingredient_id=title_ingredient.pk))
if i['food_id']:
ingredient = Ingredient.objects.create(
food_id=foods_dict[i['food_id']] if i['food_id'] in foods_dict else None,
unit_id=units_dict[i['unit_id']] if i['unit_id'] in units_dict else None,
original_text=i['original_text'],
order=i['position'],
amount=i['quantity'],
amount=i['quantity'] if i['quantity'] else 0,
note=i['note'],
space=self.request.space,
)
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=ingredient.pk))
ingredients_relation.append(Step.ingredients.through(step_id=get_step_id(i, first_step_of_recipe_dict, step_id_dict,recipe_ingredient_ref_link_dict), ingredient_id=ingredient.pk))
elif i['note'].strip():
amount, unit, food, note = ingredient_parser.parse(i['note'].strip())
f = ingredient_parser.get_food(food)
@@ -186,7 +203,7 @@ class Mealie1(Integration):
original_text=i['original_text'],
space=self.request.space,
)
ingredients_relation.append(Step.ingredients.through(step_id=first_step_of_recipe_dict[i['recipe_id']], ingredient_id=ingredient.pk))
ingredients_relation.append(Step.ingredients.through(step_id=get_step_id(i, first_step_of_recipe_dict, step_id_dict,recipe_ingredient_ref_link_dict), ingredient_id=ingredient.pk))
Step.ingredients.through.objects.bulk_create(ingredients_relation)
self.import_log.msg += f"Importing {len(mealie_database["recipes_to_categories"]) + len(mealie_database["recipes_to_tags"])} category and keyword relations...\n"
@@ -340,3 +357,10 @@ class Mealie1(Integration):
def get_file_from_recipe(self, recipe):
raise NotImplementedError('Method not implemented in storage integration')
def get_step_id(i, first_step_of_recipe_dict, step_id_dict, recipe_ingredient_ref_link_dict):
try:
return step_id_dict[recipe_ingredient_ref_link_dict[i['reference_id']]]
except KeyError:
return first_step_of_recipe_dict[i['recipe_id']]

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

@@ -16,7 +16,7 @@ import uuid
from django.conf import settings
from django.db import migrations, models
from cookbook.models import SearchFields
from django.contrib.postgres.operations import TrigramExtension, UnaccentExtension
def allSearchFields():
return list(SearchFields.objects.values_list('id', flat=True))
@@ -141,6 +141,8 @@ class Migration(migrations.Migration):
]
operations = [
TrigramExtension(),
UnaccentExtension(),
migrations.RunPython(create_default_groups),
migrations.CreateModel(
name='AiProvider',

View File

@@ -0,0 +1,15 @@
# Generated by Django 5.2.6 on 2025-09-25 18:56
from django.db import migrations
from django.contrib.postgres.operations import TrigramExtension, UnaccentExtension
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0229_alter_ailog_options_alter_aiprovider_options_and_more'),
]
operations = [
TrigramExtension(),
UnaccentExtension(),
]

View File

@@ -0,0 +1,141 @@
# Generated by Django 5.2.6 on 2025-09-30 18:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0230_auto_20250925_2056'),
]
operations = [
migrations.AlterModelOptions(
name='aiprovider',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='automation',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='bookmarkletimport',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='comment',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='connectorconfig',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='cooklog',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='customfilter',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='exportlog',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='food',
options={'ordering': ('name',)},
),
migrations.AlterModelOptions(
name='importlog',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='invitelink',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='keyword',
options={'ordering': ('name',)},
),
migrations.AlterModelOptions(
name='mealplan',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='mealtype',
options={'ordering': ('name',)},
),
migrations.AlterModelOptions(
name='recipe',
options={'ordering': ('name',)},
),
migrations.AlterModelOptions(
name='recipebook',
options={'ordering': ('name',)},
),
migrations.AlterModelOptions(
name='recipeimport',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='sharelink',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='shoppinglistentry',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='shoppinglistrecipe',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='space',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='storage',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='supermarket',
options={'ordering': ('name',)},
),
migrations.AlterModelOptions(
name='supermarketcategory',
options={'ordering': ('name',)},
),
migrations.AlterModelOptions(
name='sync',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='synclog',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='telegrambot',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='unit',
options={'ordering': ('name',)},
),
migrations.AlterModelOptions(
name='unitconversion',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='userfile',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='userspace',
options={'ordering': ('pk',)},
),
migrations.AlterModelOptions(
name='viewlog',
options={'ordering': ('pk',)},
),
]

View File

@@ -402,6 +402,9 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
def __str__(self):
return self.name
class Meta:
ordering = ('pk',)
class AiProvider(models.Model):
name = models.CharField(max_length=128)
@@ -421,13 +424,14 @@ class AiProvider(models.Model):
return self.name
class Meta:
ordering = ('id',)
ordering = ('pk',)
class AiLog(models.Model, PermissionModelMixin):
F_FILE_IMPORT = 'FILE_IMPORT'
F_STEP_SORT = 'STEP_SORT'
F_FOOD_PROPERTIES = 'FOOD_PROPERTIES'
F_RECIPE_PROPERTIES = 'RECIPE_PROPERTIES'
ai_provider = models.ForeignKey(AiProvider, on_delete=models.SET_NULL, null=True)
function = models.CharField(max_length=64)
@@ -476,6 +480,9 @@ class ConnectorConfig(models.Model, PermissionModelMixin):
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
class Meta:
ordering = ('pk',)
class UserPreference(models.Model, PermissionModelMixin):
# Themes
@@ -579,6 +586,9 @@ class UserSpace(models.Model, PermissionModelMixin):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ('pk',)
class Storage(models.Model, PermissionModelMixin):
DROPBOX = 'DB'
@@ -603,6 +613,9 @@ class Storage(models.Model, PermissionModelMixin):
def __str__(self):
return self.name
class Meta:
ordering = ('pk',)
class Sync(models.Model, PermissionModelMixin):
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
@@ -618,6 +631,9 @@ class Sync(models.Model, PermissionModelMixin):
def __str__(self):
return self.path
class Meta:
ordering = ('pk',)
class SupermarketCategory(models.Model, PermissionModelMixin, MergeModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
@@ -643,6 +659,7 @@ class SupermarketCategory(models.Model, PermissionModelMixin, MergeModelMixin):
models.UniqueConstraint(fields=['space', 'name'], name='smc_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='supermarket_category_unique_open_data_slug_per_space')
]
ordering = ('name',)
class Supermarket(models.Model, PermissionModelMixin):
@@ -662,6 +679,7 @@ class Supermarket(models.Model, PermissionModelMixin):
models.UniqueConstraint(fields=['space', 'name'], name='sm_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='supermarket_unique_open_data_slug_per_space')
]
ordering = ('name',)
class SupermarketCategoryRelation(models.Model, PermissionModelMixin):
@@ -693,6 +711,9 @@ class SyncLog(models.Model, PermissionModelMixin):
def __str__(self):
return f"{self.created_at}:{self.sync} - {self.status}"
class Meta:
ordering = ('pk',)
class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelMixin):
if SORT_TREE_BY_NAME:
@@ -710,6 +731,7 @@ class Keyword(ExportModelOperationsMixin('keyword'), TreeModel, PermissionModelM
models.UniqueConstraint(fields=['space', 'name'], name='kw_unique_name_per_space')
]
indexes = (Index(fields=['id', 'name']),)
ordering = ('name',)
class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixin, MergeModelMixin):
@@ -741,6 +763,7 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
models.UniqueConstraint(fields=['space', 'name'], name='u_unique_name_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='unit_unique_open_data_slug_per_space')
]
ordering = ('name',)
class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
@@ -874,6 +897,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
Index(fields=['id']),
Index(fields=['name']),
)
ordering = ('name',)
class UnitConversion(ExportModelOperationsMixin('unit_conversion'), models.Model, PermissionModelMixin):
@@ -900,6 +924,7 @@ class UnitConversion(ExportModelOperationsMixin('unit_conversion'), models.Model
models.UniqueConstraint(fields=['space', 'base_unit', 'converted_unit', 'food'], name='f_unique_conversion_per_space'),
models.UniqueConstraint(fields=['space', 'open_data_slug'], name='unit_conversion_unique_open_data_slug_per_space')
]
ordering = ('pk',)
class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, PermissionModelMixin):
@@ -1104,13 +1129,14 @@ class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModel
sub_food_recipes = Q(id__in=Food.objects.filter(ingredient__step__recipe__in=related_recipes).exclude(recipe=None).values_list('recipe'))
return Recipe.objects.filter(Q(id__in=related_recipes.values_list('id')) | sub_step_recipes | sub_food_recipes)
class Meta():
class Meta:
indexes = (
GinIndex(fields=["name_search_vector"]),
GinIndex(fields=["desc_search_vector"]),
Index(fields=['id']),
Index(fields=['name']),
)
ordering = ('name',)
class Comment(ExportModelOperationsMixin('comment'), models.Model, PermissionModelMixin):
@@ -1131,6 +1157,9 @@ class Comment(ExportModelOperationsMixin('comment'), models.Model, PermissionMod
def __str__(self):
return self.text
class Meta:
ordering = ('pk',)
class RecipeImport(models.Model, PermissionModelMixin):
@@ -1159,6 +1188,9 @@ class RecipeImport(models.Model, PermissionModelMixin):
self.delete()
return recipe
class Meta:
ordering = ('pk',)
class RecipeBook(ExportModelOperationsMixin('book'), models.Model, PermissionModelMixin):
name = models.CharField(max_length=128)
@@ -1176,6 +1208,7 @@ class RecipeBook(ExportModelOperationsMixin('book'), models.Model, PermissionMod
class Meta():
indexes = (Index(fields=['name']),)
ordering = ('name',)
class RecipeBookEntry(ExportModelOperationsMixin('book_entry'), models.Model, PermissionModelMixin):
@@ -1221,6 +1254,7 @@ class MealType(models.Model, PermissionModelMixin):
constraints = [
models.UniqueConstraint(fields=['space', 'name', 'created_by'], name='mt_unique_name_per_space'),
]
ordering = ('name',)
class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, PermissionModelMixin):
@@ -1248,6 +1282,9 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission
def __str__(self):
return f'{self.get_label()} - {self.from_date} - {self.meal_type.name}'
class Meta:
ordering = ('pk',)
class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), models.Model, PermissionModelMixin):
name = models.CharField(max_length=32, blank=True, default='')
@@ -1263,6 +1300,9 @@ class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), mod
def __str__(self):
return f'Shopping list recipe {self.id} - {self.recipe}'
class Meta:
ordering = ('pk',)
class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin):
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True, related_name='entries')
@@ -1294,6 +1334,9 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
except AttributeError:
return None
class Meta:
ordering = ('pk',)
class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
@@ -1309,6 +1352,9 @@ class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, Permissi
def __str__(self):
return f'{self.recipe} - {self.uuid}'
class Meta:
ordering = ('pk',)
def default_valid_until():
return date.today() + timedelta(days=14)
@@ -1332,6 +1378,9 @@ class InviteLink(ExportModelOperationsMixin('invite_link'), models.Model, Permis
def __str__(self):
return f'{self.uuid}'
class Meta:
ordering = ('pk',)
class TelegramBot(models.Model, PermissionModelMixin):
token = models.CharField(max_length=256)
@@ -1346,6 +1395,9 @@ class TelegramBot(models.Model, PermissionModelMixin):
def __str__(self):
return f"{self.name}"
class Meta:
ordering = ('pk',)
class CookLog(ExportModelOperationsMixin('cook_log'), models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
@@ -1363,7 +1415,7 @@ class CookLog(ExportModelOperationsMixin('cook_log'), models.Model, PermissionMo
def __str__(self):
return self.recipe.name
class Meta():
class Meta:
indexes = (
Index(fields=['id']),
Index(fields=['recipe']),
@@ -1372,6 +1424,7 @@ class CookLog(ExportModelOperationsMixin('cook_log'), models.Model, PermissionMo
Index(fields=['created_by']),
Index(fields=['created_by', 'rating']),
)
ordering = ('pk',)
class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, PermissionModelMixin):
@@ -1385,13 +1438,14 @@ class ViewLog(ExportModelOperationsMixin('view_log'), models.Model, PermissionMo
def __str__(self):
return self.recipe.name
class Meta():
class Meta:
indexes = (
Index(fields=['recipe']),
Index(fields=['-created_at']),
Index(fields=['created_by']),
Index(fields=['recipe', '-created_at', 'created_by']),
)
ordering = ('pk',)
class ImportLog(models.Model, PermissionModelMixin):
@@ -1412,6 +1466,9 @@ class ImportLog(models.Model, PermissionModelMixin):
def __str__(self):
return f"{self.created_at}:{self.type}"
class Meta:
ordering = ('pk',)
class ExportLog(models.Model, PermissionModelMixin):
type = models.CharField(max_length=32)
@@ -1432,6 +1489,9 @@ class ExportLog(models.Model, PermissionModelMixin):
def __str__(self):
return f"{self.created_at}:{self.type}"
class Meta:
ordering = ('pk',)
class BookmarkletImport(ExportModelOperationsMixin('bookmarklet_import'), models.Model, PermissionModelMixin):
html = models.TextField()
@@ -1442,6 +1502,9 @@ class BookmarkletImport(ExportModelOperationsMixin('bookmarklet_import'), models
objects = ScopedManager(space='space')
space = models.ForeignKey(Space, on_delete=models.CASCADE)
class Meta:
ordering = ('pk',)
# field names used to configure search behavior - all data populated during data migration
# other option is to use a MultiSelectField from https://github.com/goinnn/django-multiselectfield
@@ -1509,6 +1572,9 @@ class UserFile(ExportModelOperationsMixin('user_files'), models.Model, Permissio
def __str__(self):
return f'{self.name} (#{self.id})'
class Meta:
ordering = ('pk',)
class Automation(ExportModelOperationsMixin('automations'), models.Model, PermissionModelMixin):
FOOD_ALIAS = 'FOOD_ALIAS'
@@ -1555,6 +1621,9 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis
objects = ScopedManager(space='space')
space = models.ForeignKey(Space, on_delete=models.CASCADE)
class Meta:
ordering = ('pk',)
class CustomFilter(models.Model, PermissionModelMixin):
RECIPE = 'RECIPE'
@@ -1585,3 +1654,4 @@ class CustomFilter(models.Model, PermissionModelMixin):
constraints = [
models.UniqueConstraint(fields=['space', 'name'], name='cf_unique_name_per_space')
]
ordering = ('pk',)

View File

@@ -99,19 +99,19 @@ def test_list_filter(obj_1, u1_s1):
response = json.loads(
u1_s1.get(
f'{reverse(LIST_URL)}?from_date={(timezone.now() + timedelta(days=2)).strftime("%Y-%m-%d")}'
f'{reverse(LIST_URL)}?from_date={(timezone.localtime(timezone.now()) + timedelta(days=1)).strftime("%Y-%m-%d")}'
).content)['results']
assert len(response) == 0
response = json.loads(
u1_s1.get(
f'{reverse(LIST_URL)}?to_date={(timezone.now() - timedelta(days=2)).strftime("%Y-%m-%d")}'
f'{reverse(LIST_URL)}?to_date={(timezone.localtime(timezone.now()) - timedelta(days=1)).strftime("%Y-%m-%d")}'
).content)['results']
assert len(response) == 0
response = json.loads(
u1_s1.get(
f'{reverse(LIST_URL)}?from_date={(timezone.now() - timedelta(days=2)).strftime("%Y-%m-%d")}&to_date={(timezone.now() + timedelta(days=2)).strftime("%Y-%m-%d")}'
f'{reverse(LIST_URL)}?from_date={(timezone.localtime(timezone.now()) - timedelta(days=1)).strftime("%Y-%m-%d")}&to_date={(timezone.localtime(timezone.now()) + timedelta(days=1)).strftime("%Y-%m-%d")}'
).content)['results']
assert len(response) == 1
@@ -153,8 +153,8 @@ def test_add(arg, request, u1_s2, recipe_1_s1, meal_type):
'id': meal_type.id,
'name': meal_type.name
},
'from_date': (timezone.now()).strftime("%Y-%m-%d"),
'to_date': (timezone.now()).strftime("%Y-%m-%d"),
'from_date': (timezone.localtime(timezone.now())).strftime("%Y-%m-%d"),
'to_date': (timezone.localtime(timezone.now())).strftime("%Y-%m-%d"),
'servings': 1,
'title': 'test',
'shared': []
@@ -196,8 +196,8 @@ def test_add_with_shopping(u1_s1, meal_type):
'id': meal_type.id,
'name': meal_type.name
},
'from_date': (timezone.now()).strftime("%Y-%m-%d"),
'to_date': (timezone.now()).strftime("%Y-%m-%d"),
'from_date': (timezone.localtime(timezone.now())).strftime("%Y-%m-%d"),
'to_date': (timezone.localtime(timezone.now())).strftime("%Y-%m-%d"),
'servings': 1,
'title': 'test',
'shared': [],
@@ -212,13 +212,13 @@ def test_add_with_shopping(u1_s1, meal_type):
@pytest.mark.parametrize("arg", [
['', 2],
[f'?from_date={timezone.now().strftime("%Y-%m-%d")}', 1],
[f'?from_date={timezone.localtime(timezone.now()).strftime("%Y-%m-%d")}', 1],
[
f'?to_date={(timezone.now() - timedelta(days=1)).strftime("%Y-%m-%d")}',
f'?to_date={(timezone.localtime(timezone.now()) - timedelta(days=1)).strftime("%Y-%m-%d")}',
1
],
[
f'?from_date={(timezone.now() + timedelta(days=2)).strftime("%Y-%m-%d")}&to_date={(timezone.now() + timedelta(days=2)).strftime("%Y-%m-%d")}',
f'?from_date={(timezone.localtime(timezone.now()) + timedelta(days=1)).strftime("%Y-%m-%d")}&to_date={(timezone.localtime(timezone.now()) + timedelta(days=1)).strftime("%Y-%m-%d")}',
0
],
])

View File

@@ -185,3 +185,32 @@ def test_unit_conversions(space_1, space_2, u1_s1):
assert next(x for x in conversions if x.unit == unit_kg_space_2) is not None
assert abs(next(x for x in conversions if x.unit == unit_kg_space_2).amount - Decimal(0.1)) < 0.0001
print(conversions)
def test_conversion_with_zero(space_1, space_2, u1_s1):
with scopes_disabled():
uch = UnitConversionHelper(space_1)
unit_gram = Unit.objects.create(name='gram', base_unit='g', space=space_1)
unit_fantasy = Unit.objects.create(name='Fantasy Unit', base_unit=None, space=space_1)
food_1 = Food.objects.create(name='Test Food 1', space=space_1)
ingredient_food_1_gram = Ingredient.objects.create(
food=food_1,
unit=unit_gram,
amount=100,
space=space_1,
)
print('\n----------- TEST BASE CUSTOM CONVERSION - TO CUSTOM CONVERSION ---------------')
UnitConversion.objects.create(
base_amount=0,
base_unit=unit_gram,
converted_amount=0,
converted_unit=unit_fantasy,
space=space_1,
created_by=auth.get_user(u1_s1),
)
conversions = uch.get_conversions(ingredient_food_1_gram)
assert len(conversions) == 1 # conversion always includes the ingredient, if count is 1 no other conversion was found

View File

@@ -372,11 +372,16 @@ class MergeMixin(ViewSetMixin):
isTree = False
try:
# TODO these checks could be improved to merge existing properties and conversion in a smart way. For now it will just loose them to prevent duplicates
if isinstance(source, Food):
source.properties.all().delete()
source.properties.clear()
UnitConversion.objects.filter(food=source).delete()
if isinstance(source, Unit):
UnitConversion.objects.filter(base_unit=source).delete()
UnitConversion.objects.filter(converted_unit=source).delete()
for link in [field for field in source._meta.get_fields() if issubclass(type(field), ForeignObjectRel)]:
linkManager = getattr(source, link.get_accessor_name())
related = linkManager.all()
@@ -1125,7 +1130,7 @@ class FoodViewSet(LoggingMixin, TreeMixin, DeleteRelationMixing):
"type": "text",
"text": "Given the following food and the following different types of properties please update the food so that the properties attribute contains a list with all property types in the following format [{property_amount: <the property value>, property_type: {id: <the ID of the property type>, name: <the name of the property type>}}]."
"The property values should be in the unit given in the property type and for the amount specified in the properties_food_amount attribute of the food, which is given in the properties_food_unit."
"property_amount is a decimal number. Please try to keep a percision of two decimal places if given in your source data."
"property_amount is a decimal number. Please try to keep a precision of two decimal places if given in your source data."
"Do not make up any data. If there is no data available for the given property type that is ok, just return null as a property_amount for that property type. Do not change anything else!"
"Most property types are likely going to be nutritional values. Please do not make up any values, only return values you can find in the sources available to you."
"Only return values if you are sure they are meant for the food given. Under no circumstance are you allowed to change any other value of the given food or change the structure in any way or form."
@@ -1805,6 +1810,82 @@ class RecipeViewSet(LoggingMixin, viewsets.ModelViewSet, DeleteRelationMixing):
return Response(serializer.errors, 400)
@extend_schema(
parameters=[
OpenApiParameter(name='provider', description='ID of the AI provider that should be used for this AI request', type=int),
]
)
@decorators.action(detail=True, methods=['POST'], )
def aiproperties(self, request, pk):
serializer = RecipeSerializer(data=request.data, partial=True, context={'request': request})
if serializer.is_valid():
if not request.query_params.get('provider', None) or not re.match(r'^(\d)+$', request.query_params.get('provider', None)):
response = {
'error': True,
'msg': _('You must select an AI provider to perform your request.'),
}
return Response(response, status=status.HTTP_400_BAD_REQUEST)
if not can_perform_ai_request(request.space):
response = {
'error': True,
'msg': _("You don't have any credits remaining to use AI or AI features are not enabled for your space."),
}
return Response(response, status=status.HTTP_400_BAD_REQUEST)
ai_provider = AiProvider.objects.filter(pk=request.query_params.get('provider')).filter(Q(space=request.space) | Q(space__isnull=True)).first()
litellm.callbacks = [AiCallbackHandler(request.space, request.user, ai_provider, AiLog.F_RECIPE_PROPERTIES)]
property_type_list = list(PropertyType.objects.filter(space=request.space).values('id', 'name', 'description', 'unit', 'category', 'fdc_id'))
messages = [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Given the following recipe and the following different types of properties please update the recipe so that the properties attribute contains a list with all property types in the following format [{property_amount: <the property value>, property_type: {id: <the ID of the property type>, name: <the name of the property type>}}]."
"The property values should be in the unit given in the property type and calculated based on the total quantity of the foods used for the recipe."
"property_amount is a decimal number. Please try to keep a precision of two decimal places if given in your source data."
"Do not make up any data. If there is no data available for the given property type that is ok, just return null as a property_amount for that property type. Do not change anything else!"
"Most property types are likely going to be nutritional values. Please do not make up any values, only return values you can find in the sources available to you."
"Under no circumstance are you allowed to change any other value of the given food or change the structure in any way or form."
},
{
"type": "text",
"text": json.dumps(request.data)
},
{
"type": "text",
"text": json.dumps(property_type_list)
},
]
},
]
try:
ai_request = {
'api_key': ai_provider.api_key,
'model': ai_provider.model_name,
'response_format': {"type": "json_object"},
'messages': messages,
}
if ai_provider.url:
ai_request['api_base'] = ai_provider.url
ai_response = completion(**ai_request)
response_text = ai_response.choices[0].message.content
return Response(json.loads(response_text), status=status.HTTP_200_OK)
except BadRequestError as err:
pass
response = {
'error': True,
'msg': 'The AI could not process your request. \n\n' + err.message,
}
return Response(response, status=status.HTTP_400_BAD_REQUEST)
@extend_schema(responses=RecipeSerializer(many=False))
@decorators.action(detail=True, pagination_class=None, methods=['PATCH'], serializer_class=RecipeSerializer)
def delete_external(self, request, pk):
@@ -2481,6 +2562,13 @@ class AiImportView(APIView):
'msg': "Error parsing AI results. Response Text:\n\n" + response_text
}
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
except Exception:
traceback.print_exc()
response = {
'error': True,
'msg': "Error processing AI results. Response Text:\n\n" + response_text + "\n\n" + traceback.format_exc()
}
return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST)
else:
response = {
'error': True,

View File

@@ -33,4 +33,4 @@ Convert pictures of recipes to a structure that can be imported to Tandoor with
Maintained by [smilerz](https://github.com/smilerz/tandoor-menu-generator)
Generate a mealplan tbased on complex criteria and optionally insert it into an SVG menu template.
Generate a meal plan based on complex criteria and optionally insert it into an SVG menu template.

View File

@@ -36,7 +36,7 @@ then make sure you have set [all required headers](install/docker.md#required-he
If that doesn't fix it, you can also refer to the appropriate sub section in the [reverse proxy documentation](install/docker.md#reverse-proxy) and verify your general webserver configuration.
### Required Headers
Navigate to `/system` and review the headers listed in the DEBUG section. At a minimum, if you are using a reverse proxy the headers must match the below conditions.
Navigate to `/system/` and review the headers listed in the DEBUG section. At a minimum, if you are using a reverse proxy the headers must match the below conditions.
| Header | Requirement |
| :--- | :---- |

View File

@@ -15,14 +15,14 @@
<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>
<a href="https://app.tandoor.dev/e/demo-auto-login/" 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">Docs</a> •
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a> •
<a href="https://app.tandoor.dev/e/demo-auto-login/" target="_blank" rel="noopener noreferrer">Demo</a> •
<a href="https://community.tandoor.dev" target="_blank" rel="noopener noreferrer">Community</a> •
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord</a>
</p>

View File

@@ -69,8 +69,6 @@ wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/d
Most deployments will likely use a reverse proxy.
If your reverse proxy is not listed below, please refer to chapter [Others](#others).
#### **Traefik**
If you use Traefik, this configuration is the one for you.
@@ -115,6 +113,17 @@ wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/d
{% include "./docker/nginx-proxy/docker-compose.yml" %}
~~~
#### **Apache proxy**
If you use Apache as a reverse proxy, this configuration is the one for you.
~~~yaml
{% include "./docker/apache-proxy/docker-compose.yml" %}
~~~
Keep in mind, that the port configured for the service `web_recipes` should be the same as in chapter [Required Headers: Apache](#apache).
## **DockSTARTer**
The main goal of [DockSTARTer](https://dockstarter.com/) is to make it quick and easy to get up and running with Docker.
@@ -139,7 +148,8 @@ if you manually change it/bind the folder as a volume.
Please be sure to supply all required headers in your nginx/Apache/Caddy/... configuration!
nginx:
#### **nginx**
```nginx
location / {
proxy_set_header Host $http_host; # try $host instead if this doesn't work
@@ -149,7 +159,8 @@ location / {
}
```
Apache:
#### **Apache**
```apache
RequestHeader set X-Forwarded-Proto "https"
Header always set Access-Control-Allow-Origin "*"

View File

@@ -0,0 +1,24 @@
services:
db_recipes:
restart: always
image: postgres:16-alpine
volumes:
- ./postgresql:/var/lib/postgresql/data
env_file:
- ./.env
web_recipes:
restart: always
image: vabene1111/recipes
ports:
- 127.0.0.1:8080:80 # replace port
env_file:
- ./.env
volumes:
- staticfiles:/opt/recipes/staticfiles
- ./mediafiles:/opt/recipes/mediafiles
depends_on:
- db_recipes
volumes:
staticfiles:

View File

@@ -3,7 +3,7 @@
These instructions 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
Make sure to use at least Python 3.10 (although 3.12 is preferred) or higher, and ensure that `pip` is associated with Python 3. Depending on your system configuration, using `python` or `pip` might default to Python 2. Make sure your machine has at least 2048 MB of memory; otherwise, the `yarn build` process may fail with the error: `FATAL ERROR: Reached heap limit - Allocation failed: JavaScript heap out of memory`.
Make sure to use at least Python 3.12 or higher, and ensure that `pip` is associated with Python 3. Depending on your system configuration, using `python` or `pip` might default to Python 2. Make sure your machine has at least 2048 MB of memory; otherwise, the `yarn build` process may fail with the error: `FATAL ERROR: Reached heap limit - Allocation failed: JavaScript heap out of memory`.
!!! warning
These instructions are **not** regularly reviewed and might be outdated.
@@ -77,10 +77,10 @@ Using binaries from the virtual env:
/var/www/recipes/bin/pip3 install -r requirements.txt
```
You will also need to install front end requirements and build them. For this navigate to the `./vue` folder and run
You will also need to install front end requirements and build them. For this navigate to the `./vue3` folder and run
```shell
cd ./vue
cd ./vue3
yarn install
yarn build
```
@@ -224,7 +224,7 @@ bin/python3 manage.py migrate
bin/python3 manage.py collectstatic --no-input
bin/python3 manage.py collectstatic_js_reverse
# change to frontend directory
cd vue
cd vue3
# install and build frontend
yarn install
yarn build

View File

@@ -96,12 +96,15 @@ Configuration options for serving related services.
#### Port
> default `8080` - options: `1-65535`
> default `80` - options: `1-65535`
Port for gunicorn to bind to. Should not be changed if using docker stack with reverse proxy.
!!! warning
Changed in version 2.3 to no longer configure the port of gunicorn but the port of the internal nginx
Port where Tandoor exposes its internal web server.
```
TANDOOR_PORT=8080
TANDOOR_PORT=80
```
@@ -186,6 +189,19 @@ See [Gunicorn docs](https://docs.gunicorn.org/en/stable/design.html#how-many-wor
GUNICORN_THREADS=2
```
#### Gunicorn Timeout
> default `30` - options `1-X`
Set the timeout in seconds of gunicorn when starting using `boot.sh` (all container installations).
The default is likely appropriate for most installations. However, if you are using a LLM which high response times gunicornmight time out during the wait until the LLM finished, in such cases you might want to increase the timeout.
See [Gunicorn docs]([https://docs.gunicorn.org/en/stable/design.html#how-many-workers](https://docs.gunicorn.org/en/stable/settings.html#timeout)) for default settings.
```
GUNICORN_TIMEOUT=30
```
#### Gunicorn Media
> default `0` - options `0`, `1`

View File

@@ -1,9 +1,9 @@
server {
listen 80;
listen [::]:80 ipv6only=on;
listen ${TANDOOR_PORT};
listen [::]:${TANDOOR_PORT} ipv6only=on;
server_name localhost;
client_max_body_size 128M;
client_max_body_size 512M;
# serve media files
location /media {
@@ -19,7 +19,10 @@ server {
# pass requests for dynamic content to gunicorn
location / {
proxy_set_header Host $http_host;
proxy_pass http://localhost:${TANDOOR_PORT};
proxy_pass http://unix:/run/tandoor.sock;
# param needed by django allauth sessions to log IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# disabled for now because it redirects to the error page and not back, also not showing html
#error_page 502 /errors/http502.html;

View File

@@ -34,7 +34,7 @@ urlpatterns = [
),
]
if settings.DEBUG:
if settings.DEBUG and settings.DEBUG_TOOLBAR:
urlpatterns += path('__debug__/', include('debug_toolbar.urls')),
if settings.ENABLE_METRICS:

View File

@@ -1,18 +1,18 @@
Django==5.2.6
Django==5.2.8
cryptography===45.0.5
django-annoying==0.10.6
django-cleanup==9.0.0
django-crispy-forms==2.4
crispy-bootstrap4==2025.6
djangorestframework==3.16.1
drf-spectacular==0.27.1
drf-spectacular==0.28.0
drf-spectacular-sidecar==2025.8.1
drf-writable-nested==0.7.2
django-oauth-toolkit==2.4.0
django-debug-toolbar==4.3.0
django-debug-toolbar==6.0.0
bleach==6.2.0
gunicorn==23.0.0
lxml==5.3.1
lxml==6.0.2
Markdown==3.7
Pillow==11.3.0
psycopg2-binary==2.9.10
@@ -22,14 +22,14 @@ six==1.17.0
webdavclient3==3.14.6
whitenoise==6.8.2
icalendar==6.3.1
pyyaml==6.0.2
pyyaml==6.0.3
uritemplate==4.1.1
beautifulsoup4==4.12.3
microdata==0.8.0
mock==5.2.0
Jinja2==3.1.6
django-allauth[mfa,socialaccount]==65.9.0
recipe-scrapers==15.8.0
recipe-scrapers==15.9.0
django-scopes==2.0.0
django-treebeard==4.7.1
django-cors-headers==4.6.0
@@ -37,7 +37,7 @@ django-storages==1.14.6
boto3==1.28.75
django-prometheus==2.4.1
django-hCaptcha==0.2.0
python-ldap==3.4.4
python-ldap==3.4.5
django-auth-ldap==4.6.0
pyppeteer==2.0.0
pytubefix==9.2.2
@@ -53,7 +53,7 @@ django-vite==3.1.0
litellm==1.64.1
# Development
pytest==8.4.1
pytest==8.4.2
pytest-django==4.11.0
pytest-cov===6.2.1
pytest-factoryboy==2.8.1

View File

@@ -13,7 +13,7 @@
"@types/sortablejs": "^1.15.8",
"@vueform/multiselect": "^2.6.11",
"@vueuse/core": "^13.6.0",
"@vueuse/router": "^13.6.0",
"@vueuse/router": "^13.9.0",
"luxon": "^3.7.1",
"mavon-editor": "^3.0.1",
"pinia": "^3.0.2",
@@ -23,7 +23,7 @@
"vue-router": "^4.5.0",
"vue-simple-calendar": "7.1.0",
"vuedraggable": "^4.1.0",
"vuetify": "^3.9.7"
"vuetify": "^3.10.3"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
@@ -31,11 +31,11 @@
"@types/jsdom": "^21.1.7",
"@types/node": "^24.0.8",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
"@vue/tsconfig": "^0.8.1",
"esbuild-register": "^3.6.0",
"jsdom": "^26.1.0",
"typescript": "^5.8.3",
"vite": "7.1.5",
"vite": "7.1.11",
"vite-plugin-pwa": "^1.0.3",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^3.0.6",

View File

@@ -131,7 +131,7 @@
<script lang="ts" setup>
import GlobalSearchDialog from "@/components/inputs/GlobalSearchDialog.vue"
import {useDisplay} from "vuetify"
import {useDisplay, useLocale} from "vuetify"
import VSnackbarQueued from "@/components/display/VSnackbarQueued.vue";
import MessageListDialog from "@/components/dialogs/MessageListDialog.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
@@ -152,6 +152,7 @@ const {t} = useI18n()
const title = useTitle()
const router = useRouter()
const i18n = useI18n()
const isPrintMode = useMediaQuery('print')
@@ -161,13 +162,20 @@ onMounted(() => {
router.push({name: 'WelcomePage'})
}
})
const {current} = useLocale()
let locale = document.querySelector('html')!.getAttribute('lang')
if (locale != null) {
current.value = locale
}
})
/**
* global title update handler, might be overridden by page specific handlers
*/
router.afterEach((to, from) => {
if(to.name == 'StartPage' && useUserPreferenceStore().initCompleted && !useUserPreferenceStore().activeSpace.spaceSetupCompleted != undefined &&!useUserPreferenceStore().activeSpace.spaceSetupCompleted && useUserPreferenceStore().activeSpace.createdBy.id! == useUserPreferenceStore().userSettings.user.id!){
if (to.name == 'StartPage' && useUserPreferenceStore().initCompleted && !useUserPreferenceStore().activeSpace.spaceSetupCompleted != undefined && !useUserPreferenceStore().activeSpace.spaceSetupCompleted && useUserPreferenceStore().activeSpace.createdBy.id! == useUserPreferenceStore().userSettings.user.id!) {
router.push({name: 'WelcomePage'})
}
nextTick(() => {

View File

@@ -79,7 +79,7 @@
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="useMessageStore().deleteAllMessages()" color="error">{{$t('Delete_All')}}</v-btn>
<v-btn @click="addTestMessage()" color="warning">{{$t('Add')}}</v-btn>
<!-- <v-btn @click="addTestMessage()" color="warning">{{$t('Add')}}</v-btn>-->
<v-btn @click="isActive.value = false">{{ $t('Close')}}</v-btn>
</v-card-actions>
</v-card>

View File

@@ -157,6 +157,7 @@ function dropCalendarItemOnDate(undefinedItem: IMealPlanNormalizedCalendarItem,
let new_entry = Object.assign({}, mealPlan)
new_entry.fromDate = targetDate
new_entry.toDate = DateTime.fromJSDate(targetDate).plus(fromToDiff).toJSDate()
new_entry.addshopping = mealPlan.shopping
useMealPlanStore().createObject(new_entry)
} else {
mealPlan.fromDate = targetDate

View File

@@ -53,13 +53,13 @@
{{ fv.food.name }}
</span>
<template #append>
<v-chip v-if="fv.value != undefined">{{ $n(fv.value) }} {{ dialogProperty.unit }}</v-chip>
<v-chip color="create" v-else-if="fv.missing_conversion" class="cursor-pointer" prepend-icon="$create">
<v-chip color="create" v-if="fv.missing_conversion" class="cursor-pointer" prepend-icon="$create">
{{ $t('Conversion') }}: {{ fv.missing_conversion.base_unit.name }} <i class="fa-solid fa-arrow-right me-1 ms-1"></i>
{{ fv.missing_conversion.converted_unit.name }}
<model-edit-dialog model="UnitConversion" @create="refreshRecipe()"
:item-defaults="{baseAmount: 1, baseUnit: fv.missing_conversion.base_unit, convertedUnit: fv.missing_conversion.converted_unit, food: fv.food}"></model-edit-dialog>
</v-chip>
<v-chip v-else-if="fv.value != undefined">{{ $n(fv.value) }} {{ dialogProperty.unit }}</v-chip>
<v-chip color="warning" prepend-icon="$edit" class="cursor-pointer" :to="{name: 'ModelEditPage', params: {model: 'Recipe', id: recipe.id}}" v-else-if="fv.missing_unit">
{{ $t('NoUnit') }}
</v-chip>

View File

@@ -229,7 +229,7 @@ const selectedAiProvider = ref<undefined | AiProvider>(useUserPreferenceStore().
* factor for multiplying ingredient amounts based on recipe base servings and user selected servings
*/
const ingredientFactor = computed(() => {
return servings.value / ((recipe.value.servings != undefined) ? recipe.value.servings : 1)
return servings.value / ((recipe.value.servings != undefined) ? Math.max(recipe.value.servings, 1) : 1)
})
/**

View File

@@ -4,7 +4,6 @@
<v-row>
<v-col>
<span v-if="step.name">{{ step.name }}</span>
<span v-else-if="step.stepRecipe"><v-icon icon="$recipes"></v-icon> {{ step.stepRecipeData.name }}</span>
<span v-else>{{ $t('Step') }} {{ props.stepNumber }}</span>
</v-col>
<v-col class="text-right">
@@ -23,11 +22,12 @@
<timer :seconds="step.time != undefined ? step.time*60 : 0" @stop="timerRunning = false" v-if="timerRunning"></timer>
<v-card-text v-if="step.ingredients.length > 0 || step.instruction != ''">
<v-row>
<v-col cols="12" md="6" v-if="step.ingredients.length > 0 && step.showIngredientsTable">
<v-col cols="12" md="6" v-if="step.ingredients.length > 0 && (step.showIngredientsTable || step.show_ingredients_table)">
<ingredients-table v-model="step.ingredients" :ingredient-factor="ingredientFactor"></ingredients-table>
</v-col>
<v-col cols="12" md="6" class="markdown-body">
<instructions :instructions_html="step.instructionsMarkdown" :ingredient_factor="ingredientFactor" v-if="step.instructionsMarkdown != undefined"></instructions>
<instructions :instructions_html="step.instructionsMarkdown" :ingredient_factor="ingredientFactor"
v-if="step.instructionsMarkdown != undefined"></instructions>
<!-- sub recipes dont have a correct schema, thus they use different variable naming -->
<instructions :instructions_html="step.instructions_markdown" :ingredient_factor="ingredientFactor" v-else></instructions>
</v-col>
@@ -35,7 +35,12 @@
</v-card-text>
<template v-if="step.stepRecipe">
<v-card class="ma-2 border-md" prepend-icon="$recipes" :title="step.stepRecipeData.name">
<v-card class="ma-2 border-md">
<v-card-title>
<v-icon icon="$recipes"></v-icon>
{{ step.stepRecipeData.name }}
<v-btn icon="fa-solid fa-up-right-from-square" size="x-small" :to="{name: 'RecipeViewPage', params: {id: step.stepRecipeData.id}}" target="_blank" variant="plain"></v-btn>
</v-card-title>
<v-card-text class="mt-1" v-for="(subRecipeStep, subRecipeStepIndex) in step.stepRecipeData.steps" :key="subRecipeStep.id">
<step-view v-model="step.stepRecipeData.steps[subRecipeStepIndex]" :step-number="subRecipeStepIndex+1" :ingredientFactor="ingredientFactor"></step-view>
</v-card-text>

View File

@@ -1,11 +1,11 @@
<template>
<v-btn-group density="compact">
<v-btn color="create" @click="food.properties.push({} as Property)" prepend-icon="$create">{{ $t('Add') }}</v-btn>
<v-btn color="create" @click="editingObj.properties.push({} as Property); addPropertiesFoodUnit()" prepend-icon="$create">{{ $t('Add') }}</v-btn>
<v-btn color="secondary" @click="addAllProperties" prepend-icon="fa-solid fa-list">{{ $t('AddAll') }}</v-btn>
<ai-action-button color="info" @selected="propertiesFromAi" :loading="aiLoading" prepend-icon="$ai">{{ $t('AI') }}</ai-action-button>
</v-btn-group>
<v-row class="d-none d-md-flex mt-2" v-for="p in food.properties" dense>
<v-row class="d-none d-md-flex mt-2" v-for="p in editingObj.properties" dense>
<v-col cols="0" md="6">
<v-number-input :step="10" v-model="p.propertyAmount" control-variant="stacked" :precision="2">
<template #append-inner v-if="p.propertyType">
@@ -25,7 +25,7 @@
</v-col>
</v-row>
<v-list class="d-md-none">
<v-list-item v-for="p in food.properties" border>
<v-list-item v-for="p in editingObj.properties" border>
<span v-if="p.propertyType">{{ p.propertyAmount }} {{ p.propertyType.unit }} {{ p.propertyType.name }} / {{ props.amountFor }}
</span>
<span v-else><i><{{ $t('New') }}></i></span>
@@ -41,18 +41,23 @@
<script setup lang="ts">
import {ApiApi, Food, Property} from "@/openapi";
import {ApiApi, Food, Property, Recipe, Unit} from "@/openapi";
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {PropType, ref} from "vue";
import {computed, nextTick, onMounted, ref} from "vue";
import AiActionButton from "@/components/buttons/AiActionButton.vue";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const props = defineProps({
amountFor: {type: String, required: true},
})
const food = defineModel<Food>({required: true})
const isFood = computed(() => {
return !('steps' in editingObj.value)
})
const editingObj = defineModel<Food | Recipe>({required: true})
const aiLoading = ref(false)
@@ -61,8 +66,8 @@ const aiLoading = ref(false)
* @param property property to delete
*/
function deleteProperty(property: Property) {
if (food.value.properties) {
food.value.properties = food.value.properties.filter(p => p !== property)
if (editingObj.value.properties) {
editingObj.value.properties = editingObj.value.properties.filter(p => p !== property)
// TODO delete from DB, needs endpoint for property relation to either recipe or food
}
}
@@ -74,14 +79,16 @@ function deleteProperty(property: Property) {
function addAllProperties() {
const api = new ApiApi()
if (food.value.properties) {
food.value.properties = []
}
// if (editingObj.value.properties) {
// editingObj.value.properties = []
// }
addPropertiesFoodUnit()
api.apiPropertyTypeList().then(r => {
r.results.forEach(pt => {
if (food.value.properties.findIndex(x => x.propertyType.name == pt.name) == -1) {
food.value.properties.push({propertyAmount: 0, propertyType: pt} as Property)
if (editingObj.value.properties.findIndex(x => x.propertyType.name == pt.name) == -1) {
editingObj.value.properties.push({propertyAmount: 0, propertyType: pt} as Property)
}
})
})
@@ -90,13 +97,39 @@ function addAllProperties() {
function propertiesFromAi(providerId: number) {
const api = new ApiApi()
aiLoading.value = true
api.apiFoodAipropertiesCreate({id: food.value.id!, food: food.value, provider: providerId}).then(r => {
food.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
}).finally(() => {
aiLoading.value = false
})
if (isFood.value) {
api.apiFoodAipropertiesCreate({id: editingObj.value.id!, food: editingObj.value, provider: providerId}).then(r => {
editingObj.value = r
nextTick(() => {
addPropertiesFoodUnit()
})
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
}).finally(() => {
aiLoading.value = false
})
} else {
api.apiRecipeAipropertiesCreate({id: editingObj.value.id!, recipe: editingObj.value, provider: providerId}).then(r => {
editingObj.value = r
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
}).finally(() => {
aiLoading.value = false
})
}
}
/**
* if its empty add the properties food unit
*/
function addPropertiesFoodUnit(){
console.log('ADDING UNIT', !editingObj.value.propertiesFoodUnit)
if (isFood.value && !editingObj.value.propertiesFoodUnit) {
console.log('ADDING UNIT ACTUALLY')
editingObj.value.propertiesFoodUnit = (useUserPreferenceStore().defaultUnitObj != null) ? useUserPreferenceStore().defaultUnitObj! : {name: 'g'} as Unit
}
}

View File

@@ -67,13 +67,13 @@
</div>
<div class="d-flex flex-nowrap">
<div class="flex-col flex-grow-0 ma-1" style="min-width: 15%" v-if="!ingredient.isHeader">
<v-text-field :id="`id_input_amount_${step.id}_${index}`" :label="$t('Amount')" type="number" v-model="ingredient.amount" density="compact"
hide-details :disabled="ingredient.noAmount">
<v-number-input :id="`id_input_amount_${props.stepIndex}_${index}`" :label="$t('Amount')" v-model="ingredient.amount" density="compact"
hide-details control-variant="hidden" :disabled="ingredient.noAmount" :precision="useUserPreferenceStore().userSettings.ingredientDecimals">
<template #prepend>
<v-icon icon="$dragHandle" class="drag-handle cursor-grab"></v-icon>
</template>
</v-text-field>
</v-number-input>
</div>
<div class="flex-col flex-grow-0 ma-1" style="min-width: 15%" v-if="!ingredient.isHeader ">
<model-select model="Unit" v-model="ingredient.unit" density="compact" allow-create hide-details :disabled="ingredient.noAmount"></model-select>
@@ -195,7 +195,7 @@
<v-text-field :label="$t('Original_Text')" readonly v-model="step.ingredients[editingIngredientIndex].originalText"
v-if="step.ingredients[editingIngredientIndex].originalText"></v-text-field>
<v-number-input v-model="step.ingredients[editingIngredientIndex].amount" inset control-variant="stacked" autofocus :label="$t('Amount')"
:min="0" :precision="2" v-if="!step.ingredients[editingIngredientIndex].isHeader"></v-number-input>
:min="0" :precision="useUserPreferenceStore().userSettings.ingredientDecimals" v-if="!step.ingredients[editingIngredientIndex].isHeader"></v-number-input>
<model-select model="Unit" v-model="step.ingredients[editingIngredientIndex].unit" :label="$t('Unit')" v-if="!step.ingredients[editingIngredientIndex].isHeader"
allow-create></model-select>
<model-select model="Food" v-model="step.ingredients[editingIngredientIndex].food" :label="$t('Food')" v-if="!step.ingredients[editingIngredientIndex].isHeader"
@@ -261,24 +261,6 @@ const dialogIngredientSorter = ref(false)
const editingIngredientIndex = ref(0)
const ingredientTextInput = ref("")
const defaultUnit = ref<null | Unit>(null)
onMounted(() => {
let api = new ApiApi()
if (useUserPreferenceStore().userSettings.defaultUnit) {
api.apiUnitList({query: useUserPreferenceStore().userSettings.defaultUnit}).then(r => {
r.results.forEach(u => {
if (u.name == useUserPreferenceStore().userSettings.defaultUnit) {
defaultUnit.value = u
}
})
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
})
/**
* sort function called by draggable when ingredient table is sorted
*/
@@ -334,14 +316,10 @@ function handleIngredientNoteTab(event: KeyboardEvent, index: number) {
function insertAndFocusIngredient() {
let ingredient = {
amount: 0,
unit: null,
unit: useUserPreferenceStore().defaultUnitObj,
food: null,
} as Ingredient
if (defaultUnit.value != null) {
ingredient.unit = defaultUnit.value
}
step.value.ingredients.push(ingredient)
nextTick(() => {
sortIngredients()
@@ -349,7 +327,7 @@ function insertAndFocusIngredient() {
editingIngredientIndex.value = step.value.ingredients.length - 1
dialogIngredientEditor.value = true
} else {
document.getElementById(`id_input_amount_${step.value.id}_${step.value.ingredients.length - 1}`).select()
document.getElementById(`id_input_amount_${props.stepIndex}_${step.value.ingredients.length - 1}`).select()
}
})
}

View File

@@ -17,10 +17,10 @@
<v-list density="compact">
<v-list-subheader>{{$t('Ingredients')}}</v-list-subheader>
<v-list-item
v-for="template in templates"
@click="insertTextAtPosition(template.template + ' ')"
v-for="t in templates"
@click="insertTextAtPosition(t.template + ' ')"
>
<ingredient-string :ingredient="template.ingredient"></ingredient-string>
<ingredient-string :ingredient="t.ingredient"></ingredient-string>
</v-list-item>
</v-list>
</v-menu>
@@ -65,7 +65,7 @@ const templates = computed(() => {
function insertTextAtPosition(text: string){
let textarea = markdownEditor.value.getTextareaDom()
let position = textarea.selectionStart
if (step.value.instruction){
if (step.value.instruction != undefined){
step.value.instruction = step.value.instruction.slice(0, position) + text + step.value.instruction.slice(position)
nextTick(() => {

View File

@@ -21,7 +21,6 @@
<v-rating v-model="editingObj.rating" clearable hover density="compact"></v-rating>
</v-col>
<v-col cols="12" md="4">
<v-number-input :label="$t('Servings')" v-model="editingObj.servings" :precision="2"></v-number-input>
</v-col>
<v-col cols="12" md="4">
@@ -42,7 +41,7 @@ import {onMounted, PropType, watch} from "vue";
import {CookLog} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import {VDateInput} from 'vuetify/labs/VDateInput' //TODO remove once component is out of labs
const props = defineProps({
item: {type: {} as PropType<CookLog>, required: false, default: null},

View File

@@ -226,7 +226,7 @@ function initializeEditor() {
setupState(props.item, props.itemId, {
newItemFunction: () => {
editingObj.value.propertiesFoodAmount = 100
editingObj.value.propertiesFoodUnit = {name: (useUserPreferenceStore().userSettings.defaultUnit != undefined ? useUserPreferenceStore().userSettings.defaultUnit : 'g')} as Unit
editingObj.value.propertiesFoodUnit = (useUserPreferenceStore().defaultUnitObj != null) ? useUserPreferenceStore().defaultUnitObj! : {name: 'g'} as Unit
},
itemDefaults: props.itemDefaults,
})

View File

@@ -15,8 +15,8 @@
<v-tabs v-model="tab" :disabled="loading || fileApiLoading" grow>
<v-tab value="recipe">{{ $t('Recipe') }}</v-tab>
<v-tab value="steps">{{ $t('Steps') }}</v-tab>
<v-tab value="properties">{{ $t('Properties') }}</v-tab>
<v-tab value="settings">{{ $t('Miscellaneous') }}</v-tab>
<v-tab value="properties" :disabled="!isUpdate()">{{ $t('Properties') }}</v-tab>
<v-tab value="settings" :disabled="!isUpdate()">{{ $t('Miscellaneous') }}</v-tab>
</v-tabs>
</v-card-text>
<v-card-text v-if="!isSpaceAtRecipeLimit(useUserPreferenceStore().activeSpace)">
@@ -87,6 +87,12 @@
</v-row>
<v-form :disabled="loading || fileApiLoading">
<v-row v-if="editingObj.steps.length == 0">
<v-col class="text-center">
<v-btn icon="$create" variant="outlined" size="x-small" @click="addStep(i+1)"></v-btn>
</v-col>
</v-row>
<v-row v-for="(s,i ) in editingObj.steps" :key="s.id" dense>
<v-col>
<step-editor v-model="editingObj.steps[i]" v-model:recipe="editingObj" :step-index="i" @delete="deleteStepAtIndex(i)" @move="dialogStepManager = true"></step-editor>
@@ -106,7 +112,10 @@
<v-tabs-window-item value="properties">
<v-form :disabled="loading || fileApiLoading">
<closable-help-alert :text="$t('PropertiesFoodHelp')"></closable-help-alert>
<properties-editor v-model="editingObj.properties" :amount-for="$t('Serving')"></properties-editor>
<properties-editor v-model="editingObj" :amount-for="$t('Serving')"></properties-editor>
<!-- TODO remove once append to body for model select is working properly -->
<v-spacer style="margin-top: 100px;"></v-spacer>
</v-form>
</v-tabs-window-item>
<v-tabs-window-item value="settings">
@@ -226,7 +235,7 @@ function initializeEditor() {
addStep()
editingObj.value.steps[0].ingredients.push({
food: null,
unit: null,
unit: useUserPreferenceStore().defaultUnitObj,
amount: 0,
} as Ingredient)
editingObj.value.internal = true //TODO make database default after v2

View File

@@ -19,11 +19,11 @@
</v-row>
<v-row>
<v-col md="6">
<v-number-input :label="$t('Amount')" :step="10" v-model="editingObj.baseAmount" control-variant="stacked" :precision="3"></v-number-input>
<v-number-input :label="$t('Amount')" :step="10" v-model="editingObj.baseAmount" control-variant="stacked" :precision="3" :min="0.001"></v-number-input>
</v-col>
<v-col md="6">
<!-- TODO fix card overflow invisible, overflow-visible class is not working -->
<model-select :label="$t('Unit')" v-model="editingObj.baseUnit" model="Unit"></model-select>
<model-select v-model="editingObj.baseUnit" model="Unit"></model-select>
</v-col>
</v-row>
<v-row class="mt-0">
@@ -33,11 +33,11 @@
</v-row>
<v-row>
<v-col md="6">
<v-number-input :label="$t('Amount')" :step="10" v-model="editingObj.convertedAmount" control-variant="stacked" :precision="3"></v-number-input>
<v-number-input :label="$t('Amount')" :step="10" v-model="editingObj.convertedAmount" control-variant="stacked" :precision="3" :min="0.001"></v-number-input>
</v-col>
<v-col md="6">
<!-- TODO fix card overflow invisible, overflow-visible class is not working -->
<model-select :label="$t('Unit')" v-model="editingObj.convertedUnit" model="Unit"></model-select>
<model-select v-model="editingObj.convertedUnit" model="Unit"></model-select>
</v-col>
</v-row>
<v-row>

View File

@@ -29,7 +29,11 @@
<v-checkbox v-model="useUserPreferenceStore().deviceSettings.start_showMealPlan" :label="$t('ShowMealPlanOnStartPage')"></v-checkbox>
<v-btn @click="useUserPreferenceStore().resetDeviceSettings()" color="warning">{{ $t('Reset') }}</v-btn> <br/>
<v-btn @click="useUserPreferenceStore().deviceSettings.general_closedHelpAlerts = []" color="warning" class="mt-1">{{ $t('ResetHelp') }}</v-btn>
<v-btn @click="useUserPreferenceStore().deviceSettings.general_closedHelpAlerts = []" color="warning" class="mt-1">{{ $t('ResetHelp') }}</v-btn> <br/>
<v-btn color="info" class="mt-1">
<message-list-dialog></message-list-dialog>
{{ $t('Messages') }}
</v-btn>
</v-form>
</template>
@@ -43,6 +47,7 @@ import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/Messa
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useDjangoUrls} from "@/composables/useDjangoUrls";
import ThankYouNote from "@/components/display/ThankYouNote.vue";
import MessageListDialog from "@/components/dialogs/MessageListDialog.vue";
const {getDjangoUrl} = useDjangoUrls()

View File

@@ -26,7 +26,7 @@
Authentication works by proving the word <code>Bearer</code> followed by an API Token as a request Authorization
header as shown below. <br/>
<code>Authorization: Bearer TOKEN</code> -or-<br/>
<code>curl -X GET http://your.domain.com/api/recipes/ -H 'Authorization:
<code>curl -X GET http://your.domain.com/api/recipe/ -H 'Authorization:
Bearer TOKEN'</code>
<br/>

View File

@@ -124,6 +124,8 @@ export function useFileApi() {
* @returns Promise resolving to the import ID of the app import
*/
function doAppImport(files: File[], app: string, includeDuplicates: boolean, mealPlans: boolean = true, shoppingLists: boolean = true, nutritionPerServing: boolean = false,) {
fileApiLoading.value = true
let formData = new FormData()
formData.append('type', app);
formData.append('duplicates', includeDuplicates ? 'true' : 'false')

View File

@@ -115,6 +115,7 @@
"Fats": "",
"File": "",
"Files": "",
"Finish": "",
"Food": "",
"FoodInherit": "",
"FoodNotOnHand": "",

View File

@@ -112,6 +112,7 @@
"Fats": "Мазнини",
"File": "Файл",
"Files": "Файлове",
"Finish": "",
"Food": "Храна",
"FoodInherit": "Хранителни наследствени полета",
"FoodNotOnHand": "Нямате {храна} под ръка.",

View File

@@ -154,6 +154,7 @@
"Fats": "Greixos",
"File": "Arxiu",
"Files": "Arxius",
"Finish": "",
"First_name": "Nom",
"Food": "Aliment",
"FoodInherit": "Camps Heretats",

View File

@@ -153,6 +153,7 @@
"Fats": "Tuky",
"File": "Soubor",
"Files": "Soubory",
"Finish": "",
"First_name": "Jméno",
"Food": "Potravina",
"FoodInherit": "Propisovatelná pole potraviny",

View File

@@ -154,6 +154,7 @@
"Fats": "Fedtstoffer",
"File": "Fil",
"Files": "Filer",
"Finish": "",
"First_name": "Fornavn",
"Food": "Mad",
"FoodInherit": "Nedarvelige mad felter",

View File

@@ -57,7 +57,7 @@
"BaseUnit": "Basiseinheit",
"BaseUnitHelp": "Optionale Standardeinheit zur automatischen Umrechnung",
"Basics": "Grundlagen",
"BatchDeleteConfirm": "Möchtest du alle angezeigten Objekte löschen? Dies kann nicht rückgängig gemacht werden!",
"BatchDeleteConfirm": "Möchtest du alle angezeigten Objekte löschen? Dies kann nicht rückgängig gemacht werden! ACHTUNG: Es ist möglich das Objekte gelöscht werden die an anderen Stellen verwendet werden!",
"BatchDeleteHelp": "Wenn ein Objekt nicht gelöscht werden kann, wird es noch irgendwo verwendet. ",
"BatchEdit": "Massenbearbeitung",
"BatchEditUpdatingItemsCount": "Bearbeite {count} {type}",
@@ -212,6 +212,7 @@
"Fats": "Fette",
"File": "Datei",
"Files": "Dateien",
"Finish": "Fertigstellen",
"FinishedAt": "Fertig um",
"First": "Erstes",
"First_name": "Vorname",

View File

@@ -154,6 +154,7 @@
"Fats": "Λιπαρά",
"File": "Αρχείο",
"Files": "Αρχεία",
"Finish": "",
"First_name": "Όνομα",
"Food": "Φαγητό",
"FoodInherit": "Πεδία φαγητών που κληρονομούνται",

View File

@@ -55,7 +55,7 @@
"BaseUnit": "Base Unit",
"BaseUnitHelp": "Standard unit for automatic unit conversion",
"Basics": "Basics",
"BatchDeleteConfirm": "Do you want to delete all shown items? This cannot be undone!",
"BatchDeleteConfirm": "Do you want to delete all shown items? This cannot be undone! WARNING: It is possible that this deletes objects that are used elsewhere. ",
"BatchDeleteHelp": "If an item cannot be deleted it is used somewhere. ",
"BatchEdit": "Batch Edit",
"BatchEditUpdatingItemsCount": "Editing {count} {type}",
@@ -210,6 +210,7 @@
"Fats": "Fats",
"File": "File",
"Files": "Files",
"Finish": "Finish",
"FinishedAt": "Finished at",
"First": "First",
"First_name": "First Name",

View File

@@ -207,6 +207,7 @@
"Fats": "Grasas",
"File": "Archivo",
"Files": "Archivos",
"Finish": "",
"FinishedAt": "Finaliza a las",
"First": "Primero",
"First_name": "Nombre",

View File

@@ -151,6 +151,7 @@
"Fats": "Rasvat",
"File": "Tiedosto",
"Files": "Tiedostot",
"Finish": "",
"First_name": "Etunimi",
"Food": "Ruoka",
"FoodInherit": "Ruoan perinnölliset kentät",

View File

@@ -210,6 +210,7 @@
"Fats": "Matières grasses",
"File": "Fichier",
"Files": "Fichiers",
"Finish": "",
"FinishedAt": "Terminé à",
"First": "Premier",
"First_name": "Prénom",

View File

@@ -154,6 +154,7 @@
"Fats": "שומנים",
"File": "קובץ",
"Files": "קבצים",
"Finish": "",
"First_name": "שם פרטי",
"Food": "אוכל",
"FoodInherit": "ערכי מזון",

View File

@@ -154,6 +154,7 @@
"Fats": "Masti",
"File": "Datoteka",
"Files": "Datoteke",
"Finish": "",
"First_name": "Ime",
"Food": "Namirnica",
"FoodInherit": "Nasljedna polja namirnice",

View File

@@ -137,6 +137,7 @@
"Fats": "Zsírok",
"File": "Fájl",
"Files": "Fájlok",
"Finish": "",
"First_name": "Keresztnév",
"Food": "Alapanyag",
"FoodInherit": "",

View File

@@ -69,6 +69,7 @@
"Fats": "",
"File": "",
"Files": "",
"Finish": "",
"Food": "Սննդամթերք",
"FromBalance": "",
"Fulltext": "",

View File

@@ -126,6 +126,7 @@
"Fats": "Lemak",
"File": "Berkas",
"Files": "File",
"Finish": "",
"First_name": "",
"Food": "",
"FoodInherit": "",

View File

@@ -153,6 +153,7 @@
"Fats": "",
"File": "",
"Files": "",
"Finish": "",
"First_name": "",
"Food": "",
"FoodInherit": "",

View File

@@ -56,7 +56,7 @@
"BaseUnit": "Unità di base",
"BaseUnitHelp": "Unità standard per la conversione automatica di unità",
"Basics": "Informazioni di base",
"BatchDeleteConfirm": "Vuoi eliminare tutti gli elementi mostrati? Questo non può essere annullato!",
"BatchDeleteConfirm": "",
"BatchDeleteHelp": "Se un elemento non può essere eliminato, è utilizzato altrove. ",
"BatchEdit": "Modifica massiva",
"BatchEditUpdatingItemsCount": "Modifica di {count} {type}",
@@ -211,6 +211,7 @@
"Fats": "Grassi",
"File": "File",
"Files": "File",
"Finish": "",
"FinishedAt": "Finito alle",
"First": "Primo",
"First_name": "Nome",

866
vue3/src/locales/ko.json Normal file
View File

@@ -0,0 +1,866 @@
{
"AI": "",
"AIImportSubtitle": "",
"AISettingsHostedHelp": "",
"API": "",
"APIKey": "",
"API_Browser": "",
"API_Documentation": "",
"AccessTokenHelp": "",
"Access_Token": "",
"Account": "",
"Actions": "",
"Active": "",
"Activity": "",
"Add": "",
"AddAll": "",
"AddChild": "",
"AddFilter": "",
"AddFoodToShopping": "",
"AddMany": "",
"AddToShopping": "",
"Add_Servings_to_Shopping": "",
"Add_Step": "",
"Add_nutrition_recipe": "",
"Add_to_Plan": "",
"Add_to_Shopping": "",
"Added_To_Shopping_List": "",
"Added_by": "",
"Added_on": "",
"Admin": "",
"Advanced": "",
"AiCreditsBalance": "",
"AiLog": "",
"AiLogHelp": "",
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"AllRecipes": "",
"Amount": "",
"App": "",
"AppImportSubtitle": "",
"Apply": "",
"Are_You_Sure": "",
"Auto_Planner": "",
"Auto_Sort": "",
"Auto_Sort_Help": "",
"Automate": "",
"Automation": "",
"AutomationHelp": "",
"Available": "",
"AvailableCategories": "",
"Back": "",
"BaseUnit": "",
"BaseUnitHelp": "",
"Basics": "",
"BatchDeleteConfirm": "",
"BatchDeleteHelp": "",
"BatchEdit": "",
"BatchEditUpdatingItemsCount": "",
"Blocking": "",
"BlockingHelp": "",
"Book": "",
"Bookmarklet": "",
"BookmarkletHelp1": "",
"BookmarkletHelp2": "",
"BookmarkletHelp3": "",
"BookmarkletImportSubtitle": "",
"Books": "",
"CREATE_ERROR": "",
"Calculator": "",
"Calories": "",
"Cancel": "",
"Cannot_Add_Notes_To_Shopping": "",
"Carbohydrates": "",
"Cards": "",
"Cascading": "",
"CascadingHelp": "",
"Categories": "",
"Category": "",
"CategoryInstruction": "",
"CategoryName": "",
"Change_Password": "",
"Changing": "",
"ChildInheritFields": "",
"ChildInheritFields_help": "",
"Choose_Category": "",
"Clear": "",
"Click_To_Edit": "",
"Clone": "",
"Close": "",
"Color": "",
"Combine_All_Steps": "",
"Coming_Soon": "",
"Comment": "",
"Comments_setting": "",
"Completed": "",
"Confirm": "",
"ConnectorConfig": "",
"ConnectorConfigHelp": "",
"Continue": "",
"Conversion": "",
"ConversionsHelp": "",
"ConvertUsingAI": "",
"CookLog": "",
"CookLogHelp": "",
"Cooked": "",
"Copied": "",
"Copy": "",
"Copy Link": "",
"Copy Token": "",
"Copy_template_reference": "",
"Cosmetic": "",
"CountMore": "",
"Create": "",
"Create Food": "",
"Create Recipe": "",
"CreateFirstRecipe": "",
"CreateInvitation": "",
"Create_Meal_Plan_Entry": "",
"Create_New_Food": "",
"Create_New_Keyword": "",
"Create_New_Meal_Type": "",
"Create_New_Shopping Category": "",
"Create_New_Shopping_Category": "",
"Create_New_Unit": "",
"Created": "",
"CreatedBy": "",
"Credits": "",
"Ctrl+K": "",
"Current_Period": "",
"Custom Filter": "",
"CustomImageHelp": "",
"CustomLogoHelp": "",
"CustomLogos": "",
"CustomNavLogoHelp": "",
"CustomTheme": "",
"CustomThemeHelp": "",
"DELETE_ERROR": "",
"Data_Import_Info": "",
"Database": "",
"DatabaseHelp": "",
"Datatype": "",
"Date": "",
"Day": "",
"Days": "",
"Decimals": "",
"Default": "",
"DefaultPage": "",
"Default_Unit": "",
"DelayFor": "",
"DelayUntil": "",
"Delete": "",
"DeleteConfirmQuestion": "",
"DeleteShoppingConfirm": "",
"DeleteSomething": "",
"Delete_All": "",
"Delete_Food": "",
"Delete_Keyword": "",
"Deleted": "",
"Description": "",
"Description_Replace": "",
"DeviceSettings": "",
"DeviceSettingsHelp": "",
"Disable": "",
"Disable_Amount": "",
"Disabled": "",
"Documentation": "",
"DontChange": "",
"Down": "",
"Download": "",
"DragToUpload": "",
"Drag_Here_To_Delete": "",
"Duplicate": "",
"DuplicateFoundInfo": "",
"Edit": "",
"Edit_Food": "",
"Edit_Keyword": "",
"Edit_Meal_Plan_Entry": "",
"Edit_Recipe": "",
"Email": "",
"Empty": "",
"Enable": "",
"Enable_Amount": "",
"Enabled": "",
"EndDate": "",
"Energy": "",
"Entries": "",
"Error": "",
"ErrorUrlListImport": "",
"Events": "",
"Export": "",
"Export_As_ICal": "",
"Export_Not_Yet_Supported": "",
"Export_Supported": "",
"Export_To_ICal": "",
"External": "",
"ExternalRecipe": "",
"ExternalRecipeImport": "",
"ExternalRecipeImportHelp": "",
"ExternalStorage": "",
"External_Recipe_Image": "",
"FDC_ID": "",
"FDC_ID_help": "",
"FDC_Search": "",
"FETCH_ERROR": "",
"Failure": "",
"Fats": "",
"File": "",
"Files": "",
"FinishedAt": "",
"First": "",
"First_name": "",
"Food": "",
"FoodHelp": "",
"FoodInherit": "",
"FoodNotOnHand": "",
"FoodOnHand": "",
"Food_Alias": "",
"Food_Replace": "",
"Foods": "",
"Friday": "",
"FromBalance": "",
"Fulltext": "",
"FulltextHelp": "",
"Fuzzy": "",
"FuzzySearchHelp": "",
"GettingStarted": "",
"Global": "",
"GlobalHelp": "",
"Group": "",
"GroupBy": "",
"HeaderWarning": "",
"Headline": "",
"Help": "",
"Hide_External": "",
"Hide_Food": "",
"Hide_Keyword": "",
"Hide_Keywords": "",
"Hide_Recipes": "",
"Hide_as_header": "",
"Hierarchy": "",
"History": "",
"HostedFreeVersion": "",
"Hour": "",
"Hours": "",
"Icon": "",
"IgnoreAccents": "",
"IgnoreAccentsHelp": "",
"IgnoreThis": "",
"Ignore_Shopping": "",
"IgnoredFood": "",
"Image": "",
"Import": "",
"Import Recipe": "",
"ImportAll": "",
"ImportFirstRecipe": "",
"ImportIntoTandoor": "",
"ImportMealPlans": "",
"ImportShoppingList": "",
"Import_Error": "",
"Import_Not_Yet_Supported": "",
"Import_Result_Info": "",
"Import_Supported": "",
"Import_finished": "",
"Imported": "",
"Imported_From": "",
"Importer_Help": "",
"Information": "",
"Ingredient": "",
"Ingredient Editor": "",
"Ingredient Overview": "",
"IngredientEditorHelp": "",
"IngredientHelp": "",
"IngredientInShopping": "",
"Ingredients": "",
"Inherit": "",
"InheritFields": "",
"InheritFields_help": "",
"InheritWarning": "",
"Input": "",
"Instruction_Replace": "",
"Instructions": "",
"InstructionsEditHelp": "",
"Internal": "",
"InviteLinkHelp": "",
"Invite_Link": "",
"Invites": "",
"Key_Ctrl": "",
"Key_Shift": "",
"Keyword": "",
"KeywordHelp": "",
"Keyword_Alias": "",
"Keywords": "",
"Language": "",
"Last": "",
"Last_name": "",
"Learn_More": "",
"LeaveSpace": "",
"Link": "",
"Load": "",
"Load_More": "",
"LogCredits": "",
"LogCreditsHelp": "",
"Log_Cooking": "",
"Log_Recipe_Cooking": "",
"Logo": "",
"Logout": "",
"Make_Header": "",
"Make_Ingredient": "",
"ManageSubscription": "",
"Manage_Books": "",
"Manage_Emails": "",
"MealPlanHelp": "",
"MealPlanShoppingHelp": "",
"MealTypeHelp": "",
"Meal_Plan": "",
"Meal_Plan_Days": "",
"Meal_Type": "",
"Meal_Type_Required": "",
"Meal_Types": "",
"Merge": "",
"MergeAutomateHelp": "",
"MergeInsteadOfDelete": "",
"Merge_Keyword": "",
"Message": "",
"Messages": "",
"Miscellaneous": "",
"MissingConversion": "",
"MissingProperties": "",
"Model": "",
"ModelSelectResultsHelp": "",
"Monday": "",
"Month": "",
"MonthlyCredits": "",
"MonthlyCreditsUsed": "",
"More": "",
"Move": "",
"MoveCategory": "",
"MoveToStep": "",
"Move_Down": "",
"Move_Food": "",
"Move_Keyword": "",
"Move_Up": "",
"Multiple": "",
"Name": "",
"Name_Replace": "",
"Nav_Color": "",
"Nav_Color_Help": "",
"Nav_Text_Mode": "",
"Nav_Text_Mode_Help": "",
"Never_Unit": "",
"New": "",
"New_Cookbook": "",
"New_Entry": "",
"New_Food": "",
"New_Keyword": "",
"New_Meal_Type": "",
"New_Recipe": "",
"New_Supermarket": "",
"New_Supermarket_Category": "",
"New_Unit": "",
"Next": "",
"Next_Day": "",
"Next_Period": "",
"No": "",
"NoCategory": "",
"NoMoreUndo": "",
"NoUnit": "",
"No_ID": "",
"No_Results": "",
"NotFound": "",
"NotFoundHelp": "",
"NotInShopping": "",
"Note": "",
"NullingHelp": "",
"Number of Objects": "",
"Nutrition": "",
"NutritionsPerServing": "",
"NutritionsPerServingHelp": "",
"OfflineAlert": "",
"Ok": "",
"OnHand": "",
"OnHand_help": "",
"Open": "",
"Open_Data_Import": "",
"Open_Data_Slug": "",
"Options": "",
"Order": "",
"OrderInformation": "",
"Original_Text": "",
"Owner": "",
"Page": "",
"Parameter": "",
"Parent": "",
"PartialMatch": "",
"PartialMatchHelp": "",
"Password": "",
"Path": "",
"PerPage": "",
"Period": "",
"Periods": "",
"Pin": "",
"Pinned": "",
"PinnedConfirmation": "",
"Plan_Period_To_Show": "",
"Plan_Show_How_Many_Periods": "",
"Planned": "",
"Planner": "",
"Planner_Settings": "",
"Planning&Shopping": "",
"Plural": "",
"Postpone": "",
"PostponedUntil": "",
"PrecisionSearchHelp": "",
"Preferences": "",
"Preparation": "",
"Preview": "",
"Previous_Day": "",
"Previous_Period": "",
"Print": "",
"Private": "",
"Private_Recipe": "",
"Private_Recipe_Help": "",
"Profile": "",
"Properties": "",
"PropertiesFoodHelp": "",
"Properties_Food_Amount": "",
"Properties_Food_Unit": "",
"Property": "",
"PropertyHelp": "",
"PropertyType": "",
"PropertyTypeHelp": "",
"Property_Editor": "",
"Protected": "",
"Proteins": "",
"Quick actions": "",
"QuickEntry": "",
"Random Recipes": "",
"RandomOrder": "",
"RateLimit": "",
"RateLimitHelp": "",
"Rating": "",
"Ratings": "",
"Recently_Viewed": "",
"Recipe": "",
"RecipeBookEntryHelp": "",
"RecipeBookHelp": "",
"RecipeHelp": "",
"RecipeStepsHelp": "",
"Recipe_Book": "",
"Recipe_Image": "",
"Recipes": "",
"Recipes_In_Import": "",
"Recipes_per_page": "",
"Refresh": "",
"Remove": "",
"RemoveAllType": "",
"RemoveFoodFromShopping": "",
"RemoveParent": "",
"Remove_nutrition_recipe": "",
"Reset": "",
"ResetHelp": "",
"Reset_Search": "",
"Reusable": "",
"Role": "",
"Root": "",
"Saturday": "",
"Save": "",
"Save/Load": "",
"Save_and_View": "",
"SavedSearch": "",
"SavedSearchHelp": "",
"ScalableNumber": "",
"Search": "",
"Search Settings": "",
"SearchMethod": "",
"SearchSettingsOverview": "",
"SearchSettingsWarning": "",
"Second": "",
"Seconds": "",
"Select": "",
"SelectAll": "",
"SelectNone": "",
"Select_App_To_Import": "",
"Select_Book": "",
"Select_File": "",
"Selected": "",
"SelectedCategories": "",
"Serving": "",
"Servings": "",
"ServingsText": "",
"Settings": "",
"SettingsOnlySuperuser": "",
"Share": "",
"ShopLater": "",
"ShopNow": "",
"ShoppingBackgroundSyncWarning": "",
"ShoppingListEntry": "",
"ShoppingListEntryHelp": "",
"ShoppingListRecipe": "",
"Shopping_Categories": "",
"Shopping_Category": "",
"Shopping_List_Empty": "",
"Shopping_input_placeholder": "",
"Shopping_list": "",
"ShowDelayed": "",
"ShowIngredients": "",
"ShowMealPlanOnStartPage": "",
"ShowRecentlyCompleted": "",
"ShowUncategorizedFood": "",
"Show_Logo": "",
"Show_Logo_Help": "",
"Show_Week_Numbers": "",
"Show_as_header": "",
"Single": "",
"Size": "",
"Skip": "",
"Social_Authentication": "",
"Sort_by_new": "",
"Source": "",
"SourceImportHelp": "",
"SourceImportSubtitle": "",
"Space": "",
"SpaceHelp": "",
"SpaceLimitExceeded": "",
"SpaceLimitReached": "",
"SpaceMemberHelp": "",
"SpaceMembers": "",
"SpaceMembersHelp": "",
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"SpaceSettings": "",
"Space_Cosmetic_Settings": "",
"Split": "",
"Split_All_Steps": "",
"StartDate": "",
"Starting_Day": "",
"StartsWith": "",
"StartsWithHelp": "",
"Step": "",
"StepHelp": "",
"Step_Name": "",
"Step_Type": "",
"Step_start_time": "",
"Steps": "",
"StepsOverview": "",
"Sticky_Nav": "",
"Sticky_Nav_Help": "",
"Storage": "",
"StorageHelp": "",
"StoragePasswordTokenHelp": "",
"Structured": "",
"SubstituteOnHand": "",
"Substitutes": "",
"Success": "",
"SuccessClipboard": "",
"Summary": "",
"Sunday": "",
"Supermarket": "",
"SupermarketCategoriesOnly": "",
"SupermarketCategoryHelp": "",
"SupermarketHelp": "",
"SupermarketName": "",
"Supermarkets": "",
"SupportsDescriptionField": "",
"SyncLog": "",
"SyncLogHelp": "",
"SyncedPath": "",
"SyncedPathHelp": "",
"System": "",
"Table": "",
"Table_of_Contents": "",
"Text": "",
"ThankYou": "",
"ThanksTextHosted": "",
"ThanksTextSelfhosted": "",
"Theme": "",
"Thursday": "",
"Time": "",
"Title": "",
"Title_or_Recipe_Required": "",
"Today": "",
"Toggle": "",
"Transpose_Words": "",
"TrigramThreshold": "",
"TrigramThresholdHelp": "",
"Tuesday": "",
"Type": "",
"UPDATE_ERROR": "",
"Unchanged": "",
"Undefined": "",
"Undo": "",
"Unit": "",
"UnitConversion": "",
"UnitConversionHelp": "",
"UnitHelp": "",
"Unit_Alias": "",
"Unit_Replace": "",
"Units": "",
"Unpin": "",
"UnpinnedConfirmation": "",
"Unrated": "",
"Up": "",
"Update": "",
"Update_Existing_Data": "",
"Updated": "",
"UpgradeNow": "",
"Url": "",
"UrlImportSubtitle": "",
"UrlList": "",
"UrlListSubtitle": "",
"Url_Import": "",
"Use_Fractions": "",
"Use_Fractions_Help": "",
"Use_Kj": "",
"Use_Metric": "",
"Use_Plural_Food_Always": "",
"Use_Plural_Food_Simple": "",
"Use_Plural_Unit_Always": "",
"Use_Plural_Unit_Simple": "",
"User": "",
"UserFileHelp": "",
"UserHelp": "",
"Username": "",
"Users": "",
"Valid Until": "",
"View": "",
"ViewLogHelp": "",
"View_Recipes": "",
"Viewed": "",
"Visibility": "",
"Waiting": "",
"WaitingTime": "",
"WarnPageLeave": "",
"Warning": "",
"WarningRecipeBookEntryDuplicate": "",
"Warning_Delete_Supermarket_Category": "",
"Website": "",
"Wednesday": "",
"Week": "",
"Week_Numbers": "",
"Welcome": "",
"WelcomeSettingsHelp": "",
"WelcometoTandoor": "",
"WorkingTime": "",
"Year": "",
"Yes": "",
"YourSpaces": "",
"active": "",
"add_keyword": "",
"additional_options": "",
"advanced": "",
"advanced_search_settings": "",
"after": "",
"all": "",
"all_fields_optional": "",
"and": "",
"and_down": "",
"and_up": "",
"any": "",
"asc": "",
"base_amount": "",
"base_unit": "",
"before": "",
"book_filter_help": "",
"click_image_import": "",
"confirm_delete": "",
"convert_internal": "",
"converted_amount": "",
"converted_unit": "",
"copy_markdown_table": "",
"copy_to_clipboard": "",
"copy_to_new": "",
"create_food_desc": "",
"create_rule": "",
"create_title": "",
"created_by": "",
"created_on": "",
"csv_delim_help": "",
"csv_delim_label": "",
"csv_prefix_help": "",
"csv_prefix_label": "",
"date_created": "",
"date_viewed": "",
"default_delay": "",
"default_delay_desc": "",
"del_confirmation_tree": "",
"delete_confirmation": "",
"delete_title": "",
"desc": "",
"download_csv": "",
"download_pdf": "",
"edit_title": "",
"empty_list": "",
"enable_expert": "",
"err_creating_resource": "",
"err_deleting_protected_resource": "",
"err_deleting_resource": "",
"err_fetching_resource": "",
"err_importing_recipe": "",
"err_merge_self": "",
"err_merging_resource": "",
"err_move_self": "",
"err_moving_resource": "",
"err_updating_resource": "",
"exact": "",
"exclude": "",
"expert_mode": "",
"explain": "",
"fields": "",
"file_upload_disabled": "",
"filter": "",
"filter_name": "",
"filter_to_supermarket": "",
"filter_to_supermarket_desc": "",
"fluid_ounce": "",
"food_inherit_info": "",
"food_recipe_help": "",
"g": "",
"gallon": "",
"hide_step_ingredients": "",
"hours": "",
"ignore_shopping_help": "",
"imperial_fluid_ounce": "",
"imperial_gallon": "",
"imperial_pint": "",
"imperial_quart": "",
"imperial_tbsp": "",
"imperial_tsp": "",
"import_duplicates": "",
"import_running": "",
"in_shopping": "",
"ingredient_list": "",
"kg": "",
"l": "",
"last_cooked": "",
"last_viewed": "",
"left_handed": "",
"left_handed_help": "",
"make_now": "",
"make_now_count": "",
"mark_complete": "",
"mealplan_autoadd_shopping": "",
"mealplan_autoadd_shopping_desc": "",
"mealplan_autoexclude_onhand": "",
"mealplan_autoexclude_onhand_desc": "",
"mealplan_autoinclude_related": "",
"mealplan_autoinclude_related_desc": "",
"merge_confirmation": "",
"merge_selection": "",
"merge_title": "",
"min": "",
"ml": "",
"move_confirmation": "",
"move_selection": "",
"move_title": "",
"no_more_images_found": "",
"no_pinned_recipes": "",
"not": "",
"nothing": "",
"nothing_planned_today": "",
"on": "",
"one_url_per_line": "",
"open_data_help_text": "",
"or": "",
"ounce": "",
"parameter_count": "",
"paste_ingredients": "",
"paste_ingredients_placeholder": "",
"paste_json": "",
"per_serving": "",
"pint": "",
"plan_share_desc": "",
"plural_short": "",
"plural_usage_info": "",
"pound": "",
"property_type_fdc_hint": "",
"quart": "",
"recipe_filter": "",
"recipe_name": "",
"recipe_property_info": "",
"related_recipes": "",
"remember_hours": "",
"remember_search": "",
"remove_selection": "",
"reset_children": "",
"reset_children_help": "",
"reset_food_inheritance": "",
"reset_food_inheritance_info": "",
"reusable_help_text": "",
"review_shopping": "",
"save_filter": "",
"searchFilterCreatedByHelp": "",
"searchFilterObjectsAndHelp": "",
"searchFilterObjectsAndNotHelp": "",
"searchFilterObjectsHelp": "",
"searchFilterObjectsOrNotHelp": "",
"search_create_help_text": "",
"search_import_help_text": "",
"search_no_recipes": "",
"search_rank": "",
"seconds": "",
"select_file": "",
"select_food": "",
"select_keyword": "",
"select_recipe": "",
"select_unit": "",
"shared_with": "",
"shopping_add_onhand": "",
"shopping_add_onhand_desc": "",
"shopping_auto_sync": "",
"shopping_auto_sync_desc": "",
"shopping_category_help": "",
"shopping_recent_days": "",
"shopping_recent_days_desc": "",
"shopping_share": "",
"shopping_share_desc": "",
"show_books": "",
"show_filters": "",
"show_foods": "",
"show_ingredient_overview": "",
"show_ingredients_table": "",
"show_keywords": "",
"show_only_internal": "",
"show_rating": "",
"show_sortby": "",
"show_split_screen": "",
"show_sql": "",
"show_step_ingredients": "",
"show_step_ingredients_setting": "",
"show_step_ingredients_setting_help": "",
"show_units": "",
"simple_mode": "",
"sort_by": "",
"sql_debug": "",
"step_time_minutes": "",
"substitute_children": "",
"substitute_children_help": "",
"substitute_help": "",
"substitute_siblings": "",
"substitute_siblings_help": "",
"success_creating_resource": "",
"success_deleting_resource": "",
"success_fetching_resource": "",
"success_merging_resource": "",
"success_moving_resource": "",
"success_updating_resource": "",
"tbsp": "",
"theUsernameCannotBeChanged": "",
"times_cooked": "",
"to_close": "",
"to_navigate": "",
"to_select": "",
"today_recipes": "",
"total": "",
"tree_root": "",
"tree_select": "",
"tsp": "",
"unsaved": "",
"updatedon": "",
"view_recipe": "",
"warning_duplicate_filter": "",
"warning_feature_beta": "",
"warning_space_delete": ""
}

View File

@@ -139,6 +139,7 @@
"Fats": "",
"File": "",
"Files": "",
"Finish": "",
"First_name": "",
"Food": "",
"FoodInherit": "",

View File

@@ -154,6 +154,7 @@
"Fats": "",
"File": "",
"Files": "",
"Finish": "",
"First_name": "",
"Food": "",
"FoodInherit": "",

View File

@@ -146,6 +146,7 @@
"Fats": "Fett",
"File": "Fil",
"Files": "Filer",
"Finish": "",
"First_name": "Fornavn",
"Food": "Matretter",
"FoodInherit": "Arvbare felt for matvarer",

File diff suppressed because it is too large Load Diff

View File

@@ -180,6 +180,7 @@
"Fats": "Tłuszcze",
"File": "Plik",
"Files": "Pliki",
"Finish": "",
"First_name": "Imię",
"Food": "Żywność",
"FoodInherit": "Pola dziedziczone w żywności",

View File

@@ -26,6 +26,7 @@
"Auto_Sort_Help": "Mover todos os ingredientes para o passo mais indicado.",
"Automate": "Automatizar",
"Automation": "Automação",
"BatchDeleteConfirm": "",
"Books": "Livros",
"Calculator": "Calculadora",
"Calories": "Calorias",
@@ -103,6 +104,7 @@
"Fats": "Gorduras",
"File": "Ficheiro",
"Files": "Ficheiros",
"Finish": "",
"Food": "Comida",
"FoodInherit": "Campos herdados por comida",
"FoodNotOnHand": "Não têm {food} disponível.",

View File

@@ -209,6 +209,7 @@
"Fats": "Gorduras",
"File": "Arquivo",
"Files": "Arquivos",
"Finish": "",
"FinishedAt": "Finalizado em",
"First": "Primeiro",
"First_name": "Primeiro Nome",

View File

@@ -133,6 +133,7 @@
"Fats": "Grăsimi",
"File": "Fișier",
"Files": "Fișiere",
"Finish": "",
"First_name": "Prenume",
"Food": "Mâncare",
"FoodInherit": "Câmpuri moștenite de alimente",

View File

@@ -210,6 +210,7 @@
"Fats": "Жиры",
"File": "Файл",
"Files": "Файлы",
"Finish": "",
"FinishedAt": "Завершено в",
"First": "Первый",
"First_name": "Имя",

File diff suppressed because it is too large Load Diff

View File

@@ -191,6 +191,7 @@
"Fats": "Fett",
"File": "Fil",
"Files": "Filer",
"Finish": "",
"First_name": "Förnamn",
"Food": "Livsmedel",
"FoodInherit": "Ärftliga livsmedels fält",

View File

@@ -154,6 +154,7 @@
"Fats": "Yağlar",
"File": "Dosya",
"Files": "Dosyalar",
"Finish": "",
"First_name": "İsim",
"Food": "Yiyecek",
"FoodInherit": "Yiyeceğin Devralınabileceği Alanlar",

File diff suppressed because it is too large Load Diff

View File

@@ -154,6 +154,7 @@
"Fats": "脂肪",
"File": "文件",
"Files": "文件",
"Finish": "",
"First_name": "名",
"Food": "食物",
"FoodInherit": "食物可继承的字段",

View File

@@ -209,6 +209,7 @@
"Fats": "脂肪",
"File": "檔案",
"Files": "檔案",
"Finish": "",
"FinishedAt": "完成於",
"First": "第一個",
"First_name": "名字",

View File

@@ -877,6 +877,12 @@ export interface ApiEnterpriseSocialKeywordUpdateRequest {
keyword: Omit<Keyword, 'label'|'parent'|'numchild'|'createdAt'|'updatedAt'|'fullName'>;
}
export interface ApiEnterpriseSocialRecipeAipropertiesCreateRequest {
id: number;
recipe: Omit<Recipe, 'image'|'createdBy'|'createdAt'|'updatedAt'|'foodProperties'|'rating'|'lastCooked'>;
provider?: number;
}
export interface ApiEnterpriseSocialRecipeBatchUpdateUpdateRequest {
recipeBatchUpdate: RecipeBatchUpdate;
}
@@ -1689,6 +1695,12 @@ export interface ApiPropertyUpdateRequest {
property: Property;
}
export interface ApiRecipeAipropertiesCreateRequest {
id: number;
recipe: Omit<Recipe, 'image'|'createdBy'|'createdAt'|'updatedAt'|'foodProperties'|'rating'|'lastCooked'>;
provider?: number;
}
export interface ApiRecipeBatchUpdateUpdateRequest {
recipeBatchUpdate: RecipeBatchUpdate;
}
@@ -5574,6 +5586,57 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiEnterpriseSocialRecipeAipropertiesCreateRaw(requestParameters: ApiEnterpriseSocialRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Recipe>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiEnterpriseSocialRecipeAipropertiesCreate().'
);
}
if (requestParameters['recipe'] == null) {
throw new runtime.RequiredError(
'recipe',
'Required parameter "recipe" was null or undefined when calling apiEnterpriseSocialRecipeAipropertiesCreate().'
);
}
const queryParameters: any = {};
if (requestParameters['provider'] != null) {
queryParameters['provider'] = requestParameters['provider'];
}
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/enterprise-social-recipe/{id}/aiproperties/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: RecipeToJSON(requestParameters['recipe']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => RecipeFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiEnterpriseSocialRecipeAipropertiesCreate(requestParameters: ApiEnterpriseSocialRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Recipe> {
const response = await this.apiEnterpriseSocialRecipeAipropertiesCreateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
@@ -12351,6 +12414,57 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiRecipeAipropertiesCreateRaw(requestParameters: ApiRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Recipe>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiRecipeAipropertiesCreate().'
);
}
if (requestParameters['recipe'] == null) {
throw new runtime.RequiredError(
'recipe',
'Required parameter "recipe" was null or undefined when calling apiRecipeAipropertiesCreate().'
);
}
const queryParameters: any = {};
if (requestParameters['provider'] != null) {
queryParameters['provider'] = requestParameters['provider'];
}
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/recipe/{id}/aiproperties/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: RecipeToJSON(requestParameters['recipe']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => RecipeFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiRecipeAipropertiesCreate(requestParameters: ApiRecipeAipropertiesCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Recipe> {
const response = await this.apiRecipeAipropertiesCreateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/

View File

@@ -26,7 +26,7 @@
</v-list-item>
<v-list-item link prepend-icon="fa-solid fa-arrows-to-dot" :disabled="!selectedFood">
{{ $t('Merge') }}
<model-merge-dialog :source="selectedFood" model="Food"
<model-merge-dialog :source="[selectedFood]" model="Food"
@change="(obj: Food) => {selectedFood = obj;refreshPage()} "></model-merge-dialog>
</v-list-item>
@@ -61,7 +61,7 @@
</v-list-item>
<v-list-item link prepend-icon="fa-solid fa-arrows-to-dot" :disabled="!selectedUnit">
{{ $t('Merge') }}
<model-merge-dialog :source="selectedUnit" model="Unit"
<model-merge-dialog :source="[selectedUnit]" model="Unit"
@change="(obj: Food) => {selectedUnit = obj;refreshPage()} "></model-merge-dialog>
</v-list-item>
<v-list-item link prepend-icon="$automation" :disabled="!selectedUnit">
@@ -117,12 +117,12 @@
@update:modelValue="item.changed = true" :precision="2"></v-number-input>
</template>
<template v-slot:item.unit="{ item }">
<model-select model="Unit" v-model="item.unit" :label="$t('Unit')" density="compact" hide-details allow-create append-to-body
<model-select model="Unit" v-model="item.unit" density="compact" hide-details allow-create append-to-body
@update:modelValue="item.changed = true">
</model-select>
</template>
<template v-slot:item.food="{ item }">
<model-select model="Food" v-model="item.food" :label="$t('Food')" density="compact" hide-details allow-create append-to-body
<model-select model="Food" v-model="item.food" density="compact" hide-details allow-create append-to-body
@update:modelValue="item.changed = true"></model-select>
</template>
<template v-slot:item.note="{ item }">

View File

@@ -49,10 +49,10 @@
<td>
{{ ingredient.food.name }}
<!-- TODO weird mixture of using ingredients but not in the correct relation to the recipe not good, properly sort out and add easy unitconversion/food edit features -->
<!-- <v-btn variant="outlined" block>-->
<!-- {{ ingredient.food.name }}-->
<!-- <model-edit-dialog model="Food" :item="ingredient.food!" @save="args => ingredient.food = args"></model-edit-dialog>-->
<!-- </v-btn>-->
<!-- <v-btn variant="outlined" block>-->
<!-- {{ ingredient.food.name }}-->
<!-- <model-edit-dialog model="Food" :item="ingredient.food!" @save="args => ingredient.food = args"></model-edit-dialog>-->
<!-- </v-btn>-->
<!-- <v-chip v-if="ingredient.unit && ingredient.food.propertiesFoodUnit && ingredient.unit.id == ingredient.food.propertiesFoodUnit.id" color="success"-->
<!-- size="small">{{ ingredient.unit.name }}-->
<!-- </v-chip>-->
@@ -73,7 +73,8 @@
@click="fdcSelectedIngredient = ingredient; fdcDialog = true"></v-btn>
<v-btn @click="updateFoodFdcData(ingredient)" icon="fa-solid fa-arrows-rotate" size="small" density="compact" variant="plain"
v-if="ingredient.food.fdcId"></v-btn>
<v-btn @click="openFdcPage(ingredient.food.fdcId)" :href="`https://fdc.nal.usda.gov/food-details/${ingredient.food.fdcId}/nutrients`" target="_blank"
<v-btn @click="openFdcPage(ingredient.food.fdcId)" :href="`https://fdc.nal.usda.gov/food-details/${ingredient.food.fdcId}/nutrients`"
target="_blank"
icon="fa-solid fa-arrow-up-right-from-square"
size="small" variant="plain" v-if="ingredient.food.fdcId"></v-btn>
</template>
@@ -81,7 +82,7 @@
</td>
<td>
<v-number-input v-model="ingredient.food.propertiesFoodAmount" density="compact" hide-details @change="updateFood(ingredient)"
:loading="ingredient.loading" style="min-width: 100px" control-variant="hidden" :precision="2">
:loading="ingredient.loading" style="min-width: 100px" control-variant="hidden" :precision="2">
</v-number-input>
</td>
@@ -90,8 +91,10 @@
:loading="ingredient.loading"></model-select>
</td>
<td v-for="p in ingredient.food.properties" v-bind:key="`${ingredient.food.id}_${p.propertyType.id}`">
<v-number-input v-model="p.propertyAmount" density="compact" hide-details v-if="p.propertyAmount != null" @change="updateFood(ingredient)" :precision="2"
:loading="ingredient.loading" @click:clear="deleteFoodProperty(p, ingredient)" style="min-width: 120px" control-variant="hidden" clearable>
<v-number-input v-model="p.propertyAmount" density="compact" hide-details v-if="p.propertyAmount != null" @change="updateFood(ingredient)"
:precision="2"
:loading="ingredient.loading" @click:clear="deleteFoodProperty(p, ingredient)" style="min-width: 120px" control-variant="hidden"
clearable>
</v-number-input>
@@ -104,11 +107,10 @@
</td>
</tr>
</tbody>
<!-- TODO remove once append to body for model select is working properly -->
<v-spacer style="margin-top: 120px;"></v-spacer>
</v-table>
</v-col>
</v-row>
<v-row>
<v-col>
<v-card prepend-icon="fa-solid fa-calculator" :title="$t('Calculator')">
<v-card-text>
<v-row dense>

View File

@@ -837,7 +837,7 @@ function deleteStep(step: SourceImportStep) {
function handleMergeAllSteps(): void {
if (importResponse.value.recipe && importResponse.value.recipe.steps) {
mergeAllSteps(importResponse.value.recipe.steps)
importResponse.value.recipe.steps = mergeAllSteps(importResponse.value.recipe.steps)
}
}
@@ -931,7 +931,10 @@ function setAllKeywordsImportStatus(status: boolean) {
* add a new (empty) step at the end of the step list
*/
function addStep() {
importResponse.value.recipe?.steps.push({} as SourceImportStep)
importResponse.value.recipe?.steps.push({
ingredients: [],
instruction: ''
} as SourceImportStep)
}

View File

@@ -788,7 +788,7 @@ const filters = ref({
enabled: false,
default: undefined,
is: VNumberInput,
modelValue: useRouteQuery('timescookedGte', undefined, {transform: Number}),
modelValue: useRouteQuery('timescooked', undefined, {transform: Number}),
},
timescookedGte: {
id: 'timescookedGte',

View File

@@ -34,13 +34,13 @@
</v-card-text>
</v-card>
<template v-if="totalRecipes > 0">
<horizontal-recipe-scroller :skeletons="4" mode="recent" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="new"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="recent" v-if="totalRecipes > 10"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="new" v-if="totalRecipes > 10"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="keyword" v-if="totalRecipes > 10"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="random" v-if="totalRecipes > 10"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="created_by" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="2" mode="rating" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="keyword" v-if="totalRecipes > 5"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="random" v-if="totalRecipes > 0"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="created_by" v-if="totalRecipes > 10"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="2" mode="rating" v-if="totalRecipes > 10"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="keyword" v-if="totalRecipes > 25"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :skeletons="4" mode="random" v-if="totalRecipes > 25"></horizontal-recipe-scroller>
<v-row>

View File

@@ -1,7 +1,7 @@
import {acceptHMRUpdate, defineStore} from 'pinia'
import {useStorage} from "@vueuse/core";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import {ApiApi, ServerSettings, Space, UserPreference, UserSpace} from "@/openapi";
import {ApiApi, ServerSettings, Space, Unit, UserPreference, UserSpace} from "@/openapi";
import {ShoppingGroupingOptions} from "@/types/Shopping";
import {computed, ComputedRef, ref} from "vue";
import {DeviceSettings} from "@/types/settings";
@@ -50,6 +50,11 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
*/
const initCompleted = ref(false)
/**
* load the default unit to the store for easy use in editors and more
*/
const defaultUnitObj = ref<Unit | null>(null)
const theme = useTheme()
const router = useRouter()
@@ -77,6 +82,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
userSettings.value = r[0]
isAuthenticated.value = true
updateTheme()
loadDefaultUnit()
} else {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, r)
}
@@ -87,6 +93,28 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
})
}
/**
* load the default unit from the backend
* TODO migrate to nested serializer but requires actually creating the unit as currently its possible the default unit does not exist yet
*/
function loadDefaultUnit() {
let api = new ApiApi()
if (userSettings.value.defaultUnit) {
api.apiUnitList({query: userSettings.value.defaultUnit}).then(r => {
r.results.forEach(u => {
if (u.name == userSettings.value.defaultUnit) {
defaultUnitObj.value = u
}
})
}).catch(err => {
if (err.response.status != 403) {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
}
})
}
}
/**
* persist changes to user settings to DB
*/
@@ -254,6 +282,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
activeUserSpace,
isAuthenticated,
initCompleted,
defaultUnitObj,
loadUserSettings,
loadServerSettings,
updateUserSettings,

View File

@@ -6,6 +6,9 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
*/
export function roundDecimals(num: number) {
let decimals = useUserPreferenceStore().userSettings.ingredientDecimals
if (decimals === undefined) {
decimals = 2
}
return Number(num.toFixed(decimals))
}

View File

@@ -5,6 +5,7 @@ import {aliases, fa} from 'vuetify/iconsets/fa'
// Composables
import {createVuetify} from 'vuetify'
import {DateTime} from "luxon";
import {af, ar, az, bg, ca, ckb, cs, da, de, el, en, es, et, fi, fr, he, hr, hu, id, it, ja, km, ko, lt, lv, nl, no, pl, pt, ro, ru, sk, sl, srCyrl, srLatn, sv, th, tr, uk, vi, zhHans, zhHant} from "vuetify/locale";
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
@@ -22,17 +23,23 @@ export default createVuetify({
maxWidth: '1400px'
},
// always localize the date display of DateInputs
VDateInput: {
displayFormat : (date: Date) => DateTime.fromJSDate(date).toLocaleString()
},
// VDateInput: {
// displayFormat: (date: Date) => DateTime.fromJSDate(date).toLocaleString()
// },
// always use color for switches to properly see if enabled or not
VSwitch: {
color: 'primary'
},
// globally set the correct decimal seperator
VNumberInput: {
decimalSeparator: 0.1.toLocaleString().replace(/\d/g, '')
}
// VNumberInput: {
// decimalSeparator: 0.1.toLocaleString().replace(/\d/g, '')
// }
},
locale: {
locale: 'en',
fallback: 'en',
messages: {af, ar, az, bg, ca, ckb, cs, da, de, el, en, es, et, fi, fr, he, hr, hu, id, it, ja, km, ko, lt, lv, nl, no, pl, pt, ro, ru, sk, sl, srCyrl, srLatn, sv, th, tr, uk, vi, zhHans, zhHant},
decimalSeparator: 0.1.toLocaleString().replace(/\d/g, '')
},
theme: {
defaultTheme: 'light',

View File

@@ -1417,10 +1417,10 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.17.tgz#e8b3a41f0be76499882a89e8ed40d86a70fa4b70"
integrity sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==
"@vue/tsconfig@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.7.0.tgz#67044c847b7a137b8cbfd6b23104c36dbaf80d1d"
integrity sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==
"@vue/tsconfig@^0.8.1":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.8.1.tgz#4732251fa58945024424385cf3be0b1708fad5fe"
integrity sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==
"@vueform/multiselect@^2.6.11":
version "2.6.11"
@@ -1448,18 +1448,23 @@
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-13.6.0.tgz#49196025c96c7daeb591c20a54b61cc336af99b6"
integrity sha512-rnIH7JvU7NjrpexTsl2Iwv0V0yAx9cw7+clymjKuLSXG0QMcLD0LDgdNmXic+qL0SGvgSVPEpM9IDO/wqo1vkQ==
"@vueuse/router@^13.6.0":
version "13.6.0"
resolved "https://registry.yarnpkg.com/@vueuse/router/-/router-13.6.0.tgz#29456dab42eb75a0dc5fe4c62f59dd3f7c21a6ab"
integrity sha512-iXRwR4K7nz4PReW0QudhnM9NtYGvN4KrskFgF9G7NouM43big3bpSNRRocJKFWK7iu97ww5y82B3QA2zz3S/vw==
"@vueuse/router@^13.9.0":
version "13.9.0"
resolved "https://registry.yarnpkg.com/@vueuse/router/-/router-13.9.0.tgz#44235e6732a30b53d1c8e2ef13ce783fdd189ca6"
integrity sha512-7AYay8Pv/0fC4D0eygbIyZuLyVs+9D7dsnO5D8aqat9qcOz91v/XFWR667WE1+p+OkU0ib+FjQUdnTVBNoIw8g==
dependencies:
"@vueuse/shared" "13.6.0"
"@vueuse/shared" "13.9.0"
"@vueuse/shared@13.6.0":
version "13.6.0"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-13.6.0.tgz#872fdbd725fb4e3a12bd5aab85af9a5db0b1e481"
integrity sha512-pDykCSoS2T3fsQrYqf9SyF0QXWHmcGPQ+qiOVjlYSzlWd9dgppB2bFSM1GgKKkt7uzn0BBMV3IbJsUfHG2+BCg==
"@vueuse/shared@13.9.0":
version "13.9.0"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-13.9.0.tgz#7168b4ed647e625b05eb4e7e80fe8aabd00e3923"
integrity sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==
acorn@^8.14.0:
version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
@@ -3343,10 +3348,10 @@ vite-plugin-vuetify@^2.1.1:
debug "^4.3.3"
upath "^2.0.1"
vite@7.1.5:
version "7.1.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.5.tgz#4dbcb48c6313116689be540466fc80faa377be38"
integrity sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==
vite@7.1.11:
version "7.1.11"
resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.11.tgz#4d006746112fee056df64985191e846ebfb6007e"
integrity sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==
dependencies:
esbuild "^0.25.0"
fdir "^6.5.0"
@@ -3418,10 +3423,10 @@ vuedraggable@^4.1.0:
dependencies:
sortablejs "1.14.0"
vuetify@^3.9.7:
version "3.9.7"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.9.7.tgz#aea996f35111f25dd7e31ab956fbb40911841c24"
integrity sha512-Ib8PB3ItcguCol8f0DXLpoGyy7FvoOYW23SEWqXX+in1CSItJZHxUXXGSus94m5JWqYqQrFiwCykbHm7UWPi4Q==
vuetify@^3.10.3:
version "3.10.3"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.10.3.tgz#f04e507bb5efee6b52f11b2fd60a20dced1a8831"
integrity sha512-psc7oZfjz3LwH96ZRzSm4iGcOKKoeoVZIyO5Q5xO4vcUfWYxobL7TvMQv53jv1PnNvaMIXWeVIrQmiyce5dpTg==
w3c-xmlserializer@^5.0.0:
version "5.0.0"