mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-24 08:43:11 -05:00
Compare commits
344 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7c8b33cca | ||
|
|
674ad237f1 | ||
|
|
8ecddb07ae | ||
|
|
b86c01e405 | ||
|
|
124301823c | ||
|
|
c784a64c44 | ||
|
|
e79aeb9e8c | ||
|
|
85fe770be0 | ||
|
|
6f157b60a5 | ||
|
|
1e6bbc6699 | ||
|
|
543a53cab4 | ||
|
|
a17529bd71 | ||
|
|
1dc7b24146 | ||
|
|
60ea83d737 | ||
|
|
8180aefc0b | ||
|
|
007c99c77a | ||
|
|
292ff8313b | ||
|
|
79b3308f15 | ||
|
|
56a557fc82 | ||
|
|
b8e62ab8dd | ||
|
|
2a9a6fa5e6 | ||
|
|
79b36024a4 | ||
|
|
c40d2d0486 | ||
|
|
8da08cdd60 | ||
|
|
e47d171463 | ||
|
|
80caa5ffaf | ||
|
|
03485ecc73 | ||
|
|
b3ad32ee31 | ||
|
|
6d89fe37ad | ||
|
|
1af2473a72 | ||
|
|
77de9fee98 | ||
|
|
cba381cb67 | ||
|
|
02791e294d | ||
|
|
7f396ab483 | ||
|
|
0e299e98ee | ||
|
|
4e839711eb | ||
|
|
02c0fe993b | ||
|
|
3aea229f2d | ||
|
|
d08b3d8943 | ||
|
|
91353d6d7e | ||
|
|
d12d3d12ef | ||
|
|
e06572b7ca | ||
|
|
a2bdb02a7f | ||
|
|
433336b8b4 | ||
|
|
d50d4bee08 | ||
|
|
445f55812b | ||
|
|
0e247f22f8 | ||
|
|
4e4bc1b542 | ||
|
|
8379e3565f | ||
|
|
d36041fa75 | ||
|
|
655122c390 | ||
|
|
07e70b419d | ||
|
|
b4ebe4f9a6 | ||
|
|
172160c862 | ||
|
|
01a43f28f2 | ||
|
|
00baa397dd | ||
|
|
a2beab6cbb | ||
|
|
56bd68d824 | ||
|
|
a559335bd7 | ||
|
|
b0ed242ff2 | ||
|
|
b5c0104aba | ||
|
|
5ed0ec029b | ||
|
|
4f1abcf4a3 | ||
|
|
7140dcb188 | ||
|
|
8f229b0dde | ||
|
|
113347a6e5 | ||
|
|
c904f2d818 | ||
|
|
49fe6a6057 | ||
|
|
fef3f1cee3 | ||
|
|
fa32a6489c | ||
|
|
3d1b08779b | ||
|
|
f1d56cad9c | ||
|
|
14dbd79c7f | ||
|
|
1bd3d38dfc | ||
|
|
49a392f4e2 | ||
|
|
4c1d855690 | ||
|
|
edf420491f | ||
|
|
75bbe34ce5 | ||
|
|
743d52ef81 | ||
|
|
e125d1a45a | ||
|
|
df359a58a4 | ||
|
|
73fafa9fb3 | ||
|
|
28b0190648 | ||
|
|
8fe1b0c123 | ||
|
|
30cf37effa | ||
|
|
cd305cd47d | ||
|
|
a9dcb1538a | ||
|
|
30699ac4cd | ||
|
|
4712994242 | ||
|
|
38502e82d4 | ||
|
|
f8cd8b00a5 | ||
|
|
8f2f4d45af | ||
|
|
291808b204 | ||
|
|
75166b5b0f | ||
|
|
f7608071d8 | ||
|
|
64a1a652ca | ||
|
|
2053f29ff8 | ||
|
|
82737613b4 | ||
|
|
04dc593b16 | ||
|
|
d1e3b64a19 | ||
|
|
7922e4d2c1 | ||
|
|
f393c05d6d | ||
|
|
eb640ac270 | ||
|
|
02a36509b6 | ||
|
|
1d4ff66824 | ||
|
|
13522a0402 | ||
|
|
ea1f727a8b | ||
|
|
7c274de778 | ||
|
|
2a6922a85c | ||
|
|
c4f753ee32 | ||
|
|
c774f626ee | ||
|
|
abc1174877 | ||
|
|
f7a1ef597a | ||
|
|
12938f9cd5 | ||
|
|
7f4e958198 | ||
|
|
67791e4d86 | ||
|
|
ba363da251 | ||
|
|
e971efd809 | ||
|
|
09b688cc22 | ||
|
|
22edec4d9a | ||
|
|
821766a6ae | ||
|
|
96a2ccb5e4 | ||
|
|
d41273592a | ||
|
|
64273144d9 | ||
|
|
edb43a80a1 | ||
|
|
f736423fff | ||
|
|
fd0257c1b8 | ||
|
|
b1820f9b23 | ||
|
|
cff33cb15c | ||
|
|
8778559a20 | ||
|
|
77208384ed | ||
|
|
dbbd662e7d | ||
|
|
abe4504640 | ||
|
|
d8dbcac196 | ||
|
|
8bd26d2230 | ||
|
|
400d251381 | ||
|
|
7088bea90a | ||
|
|
0eb3e3f7ca | ||
|
|
6f1df3a95e | ||
|
|
c97053ef83 | ||
|
|
f796258529 | ||
|
|
f6cf58334f | ||
|
|
dda6f297d8 | ||
|
|
97c3135a43 | ||
|
|
871160cb42 | ||
|
|
e77b9e972f | ||
|
|
e1f382f8ba | ||
|
|
baf78573f7 | ||
|
|
e579017e6d | ||
|
|
589b226360 | ||
|
|
98ac9cd290 | ||
|
|
6253fdb3db | ||
|
|
7ff532429f | ||
|
|
a14a4bcf89 | ||
|
|
12a4b16bde | ||
|
|
98f91351d5 | ||
|
|
6a4a88371f | ||
|
|
fc749f520a | ||
|
|
5b3be18fe2 | ||
|
|
0aaa40432d | ||
|
|
6ecdb39a2d | ||
|
|
29c5944d92 | ||
|
|
68ec3f7e42 | ||
|
|
59e5094669 | ||
|
|
ed5cd2a0c5 | ||
|
|
bb2badc526 | ||
|
|
41df1d67e0 | ||
|
|
4d3edbfccb | ||
|
|
979a977d77 | ||
|
|
b2c8585ec5 | ||
|
|
70d5f1a918 | ||
|
|
82169fc316 | ||
|
|
c2fb4d3fa7 | ||
|
|
4734fae891 | ||
|
|
787b826aa1 | ||
|
|
22cdb7305b | ||
|
|
107fc138fd | ||
|
|
e9285881f0 | ||
|
|
e0b5d76278 | ||
|
|
9acf9ec27c | ||
|
|
7ef2e91ecf | ||
|
|
220c383ccb | ||
|
|
a3f474e088 | ||
|
|
2ad6e1b198 | ||
|
|
8b6d8e60eb | ||
|
|
9e392cbdad | ||
|
|
29f21a0cd8 | ||
|
|
22e9c8b462 | ||
|
|
eae474d9b9 | ||
|
|
296a1a036c | ||
|
|
0ca1b6e57c | ||
|
|
9636fc82f6 | ||
|
|
e4aeb01acb | ||
|
|
4c6357e8dc | ||
|
|
91a7e09569 | ||
|
|
2c2c07feb2 | ||
|
|
c49610ec74 | ||
|
|
587002c523 | ||
|
|
7466e5d7f5 | ||
|
|
795f5ea4f1 | ||
|
|
9b6f323b6f | ||
|
|
c688114e15 | ||
|
|
042ac6bfa5 | ||
|
|
67dc0d7066 | ||
|
|
22057cad19 | ||
|
|
ce13242f61 | ||
|
|
7dd66a52d5 | ||
|
|
6ed2b99188 | ||
|
|
29f88eade0 | ||
|
|
a63cdf8534 | ||
|
|
eb170cc7e5 | ||
|
|
0c29cef17d | ||
|
|
f86d4d5d8d | ||
|
|
c721533557 | ||
|
|
0d06494bbf | ||
|
|
9c04950948 | ||
|
|
34c37a2bee | ||
|
|
2cd1e0ad37 | ||
|
|
828afe6674 | ||
|
|
6a705b7352 | ||
|
|
3fa931466e | ||
|
|
4ba8269ff1 | ||
|
|
581310b57d | ||
|
|
ace18ab4aa | ||
|
|
abae973454 | ||
|
|
08bc29ca8a | ||
|
|
9d7ef1837b | ||
|
|
8a15f400e1 | ||
|
|
3d921cb677 | ||
|
|
f0e065efa4 | ||
|
|
d06589b31b | ||
|
|
3405bc4eb6 | ||
|
|
a75eb07a47 | ||
|
|
432914e310 | ||
|
|
65ece35966 | ||
|
|
f11af52d30 | ||
|
|
b4da5c3d5a | ||
|
|
a4e416cabc | ||
|
|
26173704aa | ||
|
|
5876c1ecf7 | ||
|
|
c4a339ed36 | ||
|
|
2967eca819 | ||
|
|
66b19eecfb | ||
|
|
37d93d4e4b | ||
|
|
656d46e9cb | ||
|
|
169d659b72 | ||
|
|
f92a9afbe5 | ||
|
|
d53f81cdfb | ||
|
|
bd4f858ba7 | ||
|
|
ce3a95f38a | ||
|
|
e3ffa03ffd | ||
|
|
79910deb8a | ||
|
|
e0c532ab94 | ||
|
|
73c09ab138 | ||
|
|
7edf0ee3cc | ||
|
|
48381fe897 | ||
|
|
e68300037c | ||
|
|
c643f24a72 | ||
|
|
7820ddc8f7 | ||
|
|
ca3cb2447c | ||
|
|
a55fdb634d | ||
|
|
8b0c607712 | ||
|
|
8c990a5dd2 | ||
|
|
abf5cf0116 | ||
|
|
84069bf9df | ||
|
|
059e5b7ea2 | ||
|
|
3677d04b56 | ||
|
|
05c034fca2 | ||
|
|
17d1cd26dc | ||
|
|
c9bbae6f77 | ||
|
|
a41ad8c6ed | ||
|
|
9c38c89c44 | ||
|
|
998440d064 | ||
|
|
b01d12c377 | ||
|
|
1dee574a08 | ||
|
|
257c4461a3 | ||
|
|
01f4257190 | ||
|
|
d7b7dd6c83 | ||
|
|
23c2eab682 | ||
|
|
def346d16d | ||
|
|
cc324b29ae | ||
|
|
9d58f9b266 | ||
|
|
30b2776f3c | ||
|
|
60d23d0686 | ||
|
|
edf649dea6 | ||
|
|
29b4a3cd22 | ||
|
|
f3a5148628 | ||
|
|
33abd777e0 | ||
|
|
739055caf6 | ||
|
|
8c29bd3439 | ||
|
|
2c4d0b692b | ||
|
|
946b79b77a | ||
|
|
cd154d09b2 | ||
|
|
236c930b54 | ||
|
|
980c847e36 | ||
|
|
91700771e6 | ||
|
|
abb6ad5fd0 | ||
|
|
ac7af02f77 | ||
|
|
525b398687 | ||
|
|
fafc836ccc | ||
|
|
c617b829e5 | ||
|
|
5b1e827d45 | ||
|
|
e33b62be2a | ||
|
|
60c33b499c | ||
|
|
c205dff523 | ||
|
|
ce69899c4b | ||
|
|
a4183e3453 | ||
|
|
ab39408a24 | ||
|
|
8c6c98483c | ||
|
|
ae095ab572 | ||
|
|
65356bc21a | ||
|
|
3aed5de3fc | ||
|
|
a4e9e54dae | ||
|
|
8f698e437e | ||
|
|
ab0d36825a | ||
|
|
4e2f6c57f1 | ||
|
|
de4cb8ba83 | ||
|
|
375f43c596 | ||
|
|
3034945e7e | ||
|
|
8e5effa532 | ||
|
|
3b81d3b18a | ||
|
|
d0f8b5773d | ||
|
|
14910162dc | ||
|
|
035f780d27 | ||
|
|
f10161ee92 | ||
|
|
b1a100a8c5 | ||
|
|
7db39d32d1 | ||
|
|
10921f9a64 | ||
|
|
ba1c44172e | ||
|
|
fd2dc15a15 | ||
|
|
47124488bb | ||
|
|
6e680c972a | ||
|
|
1fd2eb37ae | ||
|
|
923a59791a | ||
|
|
1fcc2c755a | ||
|
|
d5f7a883df | ||
|
|
17f9eef551 | ||
|
|
ca1ab33291 | ||
|
|
6e6ae80c46 | ||
|
|
aa6e109162 | ||
|
|
a6e4b778c1 | ||
|
|
31c7cb7906 | ||
|
|
d954b5cf48 | ||
|
|
e5c2f5570f |
@@ -38,3 +38,6 @@ RUN apt-get update \
|
||||
libwebp-dev \
|
||||
libsasl2-dev libldap2-dev libssl-dev \
|
||||
gnupg gnupg2 gnupg1
|
||||
|
||||
# create directory used for Docker Secrets
|
||||
RUN mkdir -p /run/secrets
|
||||
|
||||
@@ -25,8 +25,10 @@
|
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
"mypy.runUsingActiveInterpreter": true
|
||||
},
|
||||
"extensions": [
|
||||
"charliermarsh.ruff",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"matangover.mypy",
|
||||
"ms-python.black-formatter",
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/task.yaml
vendored
2
.github/ISSUE_TEMPLATE/task.yaml
vendored
@@ -4,7 +4,7 @@ description: "CONTRIBUTORS ONLY: Submit a Task that needs to be completed"
|
||||
title: "[Task] - TASK DESCRIPTION"
|
||||
labels:
|
||||
- task
|
||||
- v1
|
||||
- v2
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
8
.github/pull_request_template.md
vendored
8
.github/pull_request_template.md
vendored
@@ -6,6 +6,7 @@
|
||||
- `fix: `
|
||||
- `docs: `
|
||||
- `chore: `
|
||||
- `dev:`
|
||||
|
||||
If a section of the PR template does not apply to this PR, then delete that section.
|
||||
|
||||
@@ -27,10 +28,11 @@ _(REQUIRED)_
|
||||
Delete any of the following that do not apply:
|
||||
-->
|
||||
|
||||
- bug
|
||||
- cleanup
|
||||
- documentation
|
||||
- feature
|
||||
- bug
|
||||
- documentation
|
||||
- cleanup
|
||||
- dev (Internal development)
|
||||
|
||||
## What this PR does / why we need it:
|
||||
|
||||
|
||||
88
.github/release-drafter.yml
vendored
Normal file
88
.github/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name-template: "v$RESOLVED_VERSION"
|
||||
tag-template: "v$RESOLVED_VERSION"
|
||||
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
|
||||
sort-direction: ascending
|
||||
|
||||
categories:
|
||||
- title: "🚨 Breaking changes"
|
||||
labels:
|
||||
- "breaking-change"
|
||||
- "major"
|
||||
- title: "✨ New features"
|
||||
labels:
|
||||
- "feature"
|
||||
- "minor"
|
||||
- title: "🐛 Bug fixes"
|
||||
labels:
|
||||
- "bugfix"
|
||||
- title: "🧰 Maintenance"
|
||||
collapse-after: 3
|
||||
labels:
|
||||
- "ci"
|
||||
- "chore"
|
||||
- "l10n"
|
||||
- title: "📚 Documentation"
|
||||
labels:
|
||||
- "documentation"
|
||||
- title: "🔨 Internal development"
|
||||
labels:
|
||||
- "dev"
|
||||
- title: "⬆️ Dependency updates"
|
||||
collapse-after: 3
|
||||
labels:
|
||||
- "dependencies"
|
||||
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- "major"
|
||||
- "breaking-change"
|
||||
minor:
|
||||
labels:
|
||||
- "minor"
|
||||
- "feature"
|
||||
patch:
|
||||
labels:
|
||||
- "bugfix"
|
||||
- "chore"
|
||||
- "ci"
|
||||
- "dependencies"
|
||||
- "documentation"
|
||||
- "l10n"
|
||||
- "dev"
|
||||
default: patch
|
||||
|
||||
template: |
|
||||
# 🍴🍴🍴🍴🍴🍴
|
||||
|
||||
## 🎉 Highlights
|
||||
|
||||
- Highlight 1
|
||||
|
||||
- Highlight 2
|
||||
|
||||
$CHANGES
|
||||
|
||||
## 🙏 New Contributors
|
||||
|
||||
!!! Need to source this from GitHub's auto generated release notes !!!
|
||||
|
||||
# 🍴🍴🍴🍴🍴🍴
|
||||
|
||||
autolabeler:
|
||||
- label: 'feature'
|
||||
title:
|
||||
- '/feat/i'
|
||||
- label: 'bugfix'
|
||||
title:
|
||||
- '/fix:/i'
|
||||
- label: 'documentation'
|
||||
title:
|
||||
- '/docs:/i'
|
||||
- label: 'chore'
|
||||
title:
|
||||
- '/chore:/i'
|
||||
- label: 'dev'
|
||||
title:
|
||||
- '/dev:/i'
|
||||
5
.github/workflows/codeql.yml
vendored
5
.github/workflows/codeql.yml
vendored
@@ -14,10 +14,9 @@ name: "CodeQL"
|
||||
on:
|
||||
push:
|
||||
branches: [ "mealie-next" ]
|
||||
pull_request:
|
||||
branches: [ "mealie-next" ]
|
||||
schedule:
|
||||
- cron: '36 9 * * 3'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -45,7 +44,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
9
.github/workflows/e2e.yml
vendored
9
.github/workflows/e2e.yml
vendored
@@ -1,8 +1,7 @@
|
||||
name: E2E Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- mealie-next
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
@@ -11,8 +10,8 @@ jobs:
|
||||
run:
|
||||
working-directory: ./tests/e2e
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'yarn'
|
||||
|
||||
6
.github/workflows/partial-backend.yml
vendored
6
.github/workflows/partial-backend.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
# Steps
|
||||
steps:
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
uses: arduino/setup-task@v2
|
||||
with:
|
||||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
- name: Load cached venv
|
||||
id: cached-poetry-dependencies
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
|
||||
|
||||
4
.github/workflows/partial-frontend.yml
vendored
4
.github/workflows/partial-frontend.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v3.3.2
|
||||
uses: actions/cache@v4
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache node_modules 📦
|
||||
uses: actions/cache@v3.3.2
|
||||
uses: actions/cache@v4
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
||||
@@ -26,6 +26,6 @@ jobs:
|
||||
output: "trivy-results.sarif"
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: "trivy-results.sarif"
|
||||
|
||||
3
.github/workflows/pull-request-lint.yml
vendored
3
.github/workflows/pull-request-lint.yml
vendored
@@ -20,10 +20,11 @@ jobs:
|
||||
# Configure which types are allowed (newline-delimited).
|
||||
# Default: https://github.com/commitizen/conventional-commit-types
|
||||
types: |
|
||||
fix
|
||||
feat
|
||||
fix
|
||||
docs
|
||||
chore
|
||||
dev
|
||||
# Configure which scopes are allowed (newline-delimited).
|
||||
# These are regex patterns auto-wrapped in `^ $`.
|
||||
scopes: |
|
||||
|
||||
16
.github/workflows/pull-requests.yml
vendored
16
.github/workflows/pull-requests.yml
vendored
@@ -5,6 +5,10 @@ on:
|
||||
branches:
|
||||
- mealie-next
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pull-request-lint:
|
||||
name: "Lint PR"
|
||||
@@ -21,3 +25,15 @@ jobs:
|
||||
container-scanning:
|
||||
name: "Trivy Container Scanning"
|
||||
uses: ./.github/workflows/partial-trivy-container-scanning.yml
|
||||
|
||||
end-to-end:
|
||||
name: "End-to-End Tests"
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
|
||||
code-ql:
|
||||
name: "CodeQL"
|
||||
uses: ./.github/workflows/codeql.yml
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
30
.github/workflows/release-drafter.yml
vendored
Normal file
30
.github/workflows/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- mealie-next
|
||||
# pull_request event is required for autolabeler
|
||||
pull_request:
|
||||
types: [opened, labeled, unlabeled, reopened, synchronize]
|
||||
# pull_request_target event is required for autolabeler to support PRs from forks
|
||||
pull_request_target:
|
||||
types: [opened, labeled, unlabeled, reopened, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
permissions:
|
||||
# write permission is required to create a github release
|
||||
contents: write
|
||||
# write permission is required for autolabeler
|
||||
# otherwise, read permission is required at least
|
||||
pull-requests: write
|
||||
name: ✏️ Draft release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🚀 Run Release Drafter
|
||||
uses: release-drafter/release-drafter@v6.0.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -61,10 +61,16 @@ jobs:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract Version From Tag Name
|
||||
run: echo "VERSION_NUM=$(echo ${{ github.event.release.tag_name }} | sed 's/^v//')" >> $GITHUB_ENV
|
||||
|
||||
- name: Modify version strings
|
||||
run: |
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:${{ github.event.release.tag_name }}/' docs/docs/documentation/getting-started/installation/sqlite.md
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:${{ github.event.release.tag_name }}/' docs/docs/documentation/getting-started/installation/postgres.md
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/installation-checklist.md
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/sqlite.md
|
||||
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/postgres.md
|
||||
sed -i 's/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' pyproject.toml
|
||||
sed -i 's/^\s*"version": "[^"]*"/"version": "${{ env.VERSION_NUM }}"/' frontend/package.json
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
@@ -74,6 +80,8 @@ jobs:
|
||||
with:
|
||||
commit-message: "Update image tag, for release ${{ github.event.release.tag_name }}"
|
||||
branch: "docs/newrelease-update-version-${{ github.event.release.tag_name }}"
|
||||
labels: |
|
||||
documentation
|
||||
delete-branch: true
|
||||
base: mealie-next
|
||||
title: "docs(auto): Update image tag, for release ${{ github.event.release.tag_name }}"
|
||||
|
||||
34
.github/workflows/scheduled-checks.yml
vendored
Normal file
34
.github/workflows/scheduled-checks.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Scheduled Checks
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every monday at 7 AM
|
||||
- cron: 0 7 * * 1
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update pre-commit Hooks
|
||||
uses: vrslev/pre-commit-autoupdate@v1.0.0
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
# This doesn't currently work for us because it creates the PR but the workflows don't run.
|
||||
# TODO: Provide a personal access token as a parameter here, that solves that problem.
|
||||
# https://github.com/peter-evans/create-pull-request
|
||||
with:
|
||||
commit-message: "Update pre-commit hooks"
|
||||
branch: "fix/update-pre-commit-hooks"
|
||||
labels: |
|
||||
chore
|
||||
delete-branch: true
|
||||
base: mealie-next
|
||||
title: "chore(auto): Update pre-commit hooks"
|
||||
body: "Auto-generated by `.github/workflows/scheduled-checks.yml`"
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
exclude: "mkdocs.yml"
|
||||
@@ -12,6 +12,7 @@ repos:
|
||||
exclude: ^tests/data/
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.5.0
|
||||
rev: v0.7.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
@@ -90,7 +90,7 @@ Thanks to Depot for providing build instances for our Docker image builds.
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/mealie-recipes/mealie.svg?style=flat-square
|
||||
[docker-pull]: https://img.shields.io/docker/pulls/hkotel/mealie?style=flat-square
|
||||
[docker-url]: https://hub.docker.com/r/hkotel/mealie
|
||||
[ghcr-pulls]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fipitio%2Fghcr-pulls%2Fmaster%2Findex.json&query=%24%5B%3F(%40.owner%3D%3D%22mealie-recipes%22%20%26%26%20%40.repo%3D%3D%22mealie%22%20%26%26%20%40.image%3D%3D%22mealie%22)%5D.pulls&style=flat-square&label=ghcr%20pulls
|
||||
[ghcr-pulls]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fipitio.github.io%2Fbackage%2Fmealie-recipes%2Fmealie%2Fmealie.json&query=%24.downloads&style=flat-square&label=ghcr%20pulls
|
||||
[ghcr-url]: https://github.com/mealie-recipes/mealie/pkgs/container/mealie
|
||||
[contributors-url]: https://github.com/mealie-recipes/mealie/graphs/contributors
|
||||
[stars-shield]: https://img.shields.io/github/stars/mealie-recipes/mealie.svg?style=flat-square
|
||||
|
||||
@@ -148,7 +148,7 @@ tasks:
|
||||
- poetry run python mealie/app.py
|
||||
|
||||
py:migrate:
|
||||
desc: generates a new database migration file e.g. task py:migrate "add new column"
|
||||
desc: generates a new database migration file e.g. task py:migrate -- "add new column"
|
||||
cmds:
|
||||
- poetry run alembic revision --autogenerate -m "{{ .CLI_ARGS }}"
|
||||
- task: py:format
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from typing import Any
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.models._all_models # noqa: F401
|
||||
from alembic import context
|
||||
@@ -29,6 +31,28 @@ if not settings.DB_URL:
|
||||
config.set_main_option("sqlalchemy.url", settings.DB_URL.replace("%", "%%"))
|
||||
|
||||
|
||||
def include_object(object: Any, name: str, type_: str, reflected: bool, compare_to: Any):
|
||||
# skip dropping food/unit unique constraints; they are defined manually so alembic doesn't see them
|
||||
# see: revision dded3119c1fe
|
||||
if type_ == "unique_constraint" and name == "ingredient_foods_name_group_id_key" and compare_to is None:
|
||||
return False
|
||||
if type_ == "unique_constraint" and name == "ingredient_units_name_group_id_key" and compare_to is None:
|
||||
return False
|
||||
|
||||
# skip changing the quantity column in recipes_ingredients; it's a float on postgres, but an integer on sqlite
|
||||
# see: revision 263dd6707191
|
||||
if (
|
||||
type_ == "column"
|
||||
and name == "quantity"
|
||||
and object.table.name == "recipes_ingredients"
|
||||
and hasattr(compare_to, "type")
|
||||
and isinstance(compare_to.type, sa.Integer)
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
@@ -60,15 +84,19 @@ def run_migrations_online():
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
connectable = sa.engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
poolclass=sa.pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata, user_module_prefix="mealie.db.migration_types."
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
user_module_prefix="mealie.db.migration_types.",
|
||||
render_as_batch=True,
|
||||
include_object=include_object,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
|
||||
@@ -9,13 +9,15 @@ import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
${imports if imports else ""}
|
||||
% if imports:
|
||||
${imports}
|
||||
% endif
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
down_revision: str | None = ${repr(down_revision)}
|
||||
branch_labels: str | tuple[str, ...] | None = ${repr(branch_labels)}
|
||||
depends_on: str | tuple[str, ...] | None = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -14,9 +14,9 @@ from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "6b0f5f32d602"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
down_revision: str | None = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
# Adapted from https://improveandrepeat.com/2021/09/python-friday-87-handling-pre-existing-tables-with-alembic-and-sqlalchemy/
|
||||
|
||||
@@ -13,8 +13,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "263dd6707191"
|
||||
down_revision = "6b0f5f32d602"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def is_postgres():
|
||||
|
||||
@@ -13,8 +13,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "f1a2dbee5fe9"
|
||||
down_revision = "263dd6707191"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -14,8 +14,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "59eb59135381"
|
||||
down_revision = "f1a2dbee5fe9"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -14,8 +14,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "09dfc897ad62"
|
||||
down_revision = "59eb59135381"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -13,8 +13,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "ab0bae02578f"
|
||||
down_revision = "09dfc897ad62"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -14,8 +14,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "f30cf048c228"
|
||||
down_revision = "ab0bae02578f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -13,8 +13,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "188374910655"
|
||||
down_revision = "f30cf048c228"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -13,8 +13,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "089bfa50d0ed"
|
||||
down_revision = "188374910655"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -14,8 +14,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "44e8d670719d"
|
||||
down_revision = "089bfa50d0ed"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -14,8 +14,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "2ea7a807915c"
|
||||
down_revision = "44e8d670719d"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -13,8 +13,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1923519381ad"
|
||||
down_revision = "2ea7a807915c"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -13,8 +13,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "167eb69066ad"
|
||||
down_revision = "1923519381ad"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -14,8 +14,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "165d943c64ee"
|
||||
down_revision = "167eb69066ad"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -11,8 +11,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "ff5f73b01a7a"
|
||||
down_revision = "165d943c64ee"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -6,16 +6,13 @@ Create Date: 2023-02-10 21:18:32.405130
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "16160bf731a0"
|
||||
down_revision = "ff5f73b01a7a"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -11,15 +11,14 @@ from sqlalchemy import orm, select
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
from text_unidecode import unidecode
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
from mealie.db.models._model_utils import GUID
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "5ab195a474eb"
|
||||
down_revision = "16160bf731a0"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
class SqlAlchemyBase(DeclarativeBase):
|
||||
|
||||
@@ -13,14 +13,32 @@ from sqlalchemy import orm
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
from mealie.db.models.group.shopping_list import ShoppingList
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "b04a08da2108"
|
||||
down_revision = "5ab195a474eb"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
# Intermediate table definitions
|
||||
class SqlAlchemyBase(orm.DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class ShoppingList(SqlAlchemyBase):
|
||||
__tablename__ = "shopping_lists"
|
||||
|
||||
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
group_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
|
||||
|
||||
class MultiPurposeLabel(SqlAlchemyBase):
|
||||
__tablename__ = "multi_purpose_labels"
|
||||
|
||||
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
group_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
|
||||
|
||||
def populate_shopping_lists_multi_purpose_labels(
|
||||
|
||||
@@ -8,14 +8,13 @@ Create Date: 2023-02-22 21:45:52.900964
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "38514b39a824"
|
||||
down_revision = "b04a08da2108"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def is_postgres():
|
||||
|
||||
@@ -6,18 +6,13 @@ Create Date: 2023-04-13 06:47:04.617131
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
import alembic.context as context
|
||||
from mealie.core.config import get_app_settings
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "b3dbb554ba53"
|
||||
down_revision = "38514b39a824"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def get_db_type():
|
||||
|
||||
@@ -16,8 +16,8 @@ from mealie.db.models.group.group import Group
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "04ac51cbe9a4"
|
||||
down_revision = "b3dbb554ba53"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def populate_group_slugs(session: Session):
|
||||
|
||||
@@ -13,8 +13,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1825b5225403"
|
||||
down_revision = "04ac51cbe9a4"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -11,8 +11,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "bcfdad6b7355"
|
||||
down_revision = "1825b5225403"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -15,8 +15,8 @@ from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUn
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0341b154f79a"
|
||||
down_revision = "bcfdad6b7355"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def populate_normalized_fields():
|
||||
|
||||
@@ -12,19 +12,40 @@ from typing import Any
|
||||
|
||||
import sqlalchemy as sa
|
||||
from pydantic import UUID4
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.orm import Session, load_only
|
||||
|
||||
from alembic import op
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
from mealie.db.models.group.shopping_list import ShoppingListItem
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel, RecipeIngredientModel
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "dded3119c1fe"
|
||||
down_revision = "0341b154f79a"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
# Intermediate table definitions
|
||||
class SqlAlchemyBase(orm.DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class ShoppingList(SqlAlchemyBase):
|
||||
__tablename__ = "shopping_lists"
|
||||
|
||||
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
group_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
|
||||
|
||||
|
||||
class ShoppingListItem(SqlAlchemyBase):
|
||||
__tablename__ = "shopping_list_items"
|
||||
|
||||
id: orm.Mapped[GUID] = orm.mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
food_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("ingredient_foods.id"))
|
||||
unit_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("ingredient_units.id"))
|
||||
label_id: orm.Mapped[GUID] = orm.mapped_column(GUID, sa.ForeignKey("multi_purpose_labels.id"))
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -42,7 +63,7 @@ def _is_postgres():
|
||||
return op.get_context().dialect.name == "postgresql"
|
||||
|
||||
|
||||
def _get_duplicates(session: Session, model: SqlAlchemyBase) -> defaultdict[str, list]:
|
||||
def _get_duplicates(session: Session, model: orm.DeclarativeBase) -> defaultdict[str, list]:
|
||||
duplicate_map: defaultdict[str, list] = defaultdict(list)
|
||||
|
||||
query = session.execute(sa.text(f"SELECT id, group_id, name FROM {model.__tablename__}"))
|
||||
|
||||
@@ -14,8 +14,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "ba1e4a6cfe99"
|
||||
down_revision = "dded3119c1fe"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -20,8 +20,8 @@ logger = get_logger()
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "2298bb460ffd"
|
||||
down_revision = "ba1e4a6cfe99"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def is_postgres():
|
||||
@@ -66,7 +66,7 @@ def populate_shopping_list_users():
|
||||
user_id = find_user_id_for_group(group_id)
|
||||
if user_id:
|
||||
session.execute(
|
||||
sa.text(f"UPDATE shopping_lists SET user_id=:user_id WHERE id=:id").bindparams(
|
||||
sa.text("UPDATE shopping_lists SET user_id=:user_id WHERE id=:id").bindparams(
|
||||
user_id=user_id, id=list_id
|
||||
)
|
||||
)
|
||||
@@ -74,7 +74,7 @@ def populate_shopping_list_users():
|
||||
logger.warning(
|
||||
f"No user found for shopping list {list_id} with group {group_id}; deleting shopping list"
|
||||
)
|
||||
session.execute(sa.text(f"DELETE FROM shopping_lists WHERE id=:id").bindparams(id=list_id))
|
||||
session.execute(sa.text("DELETE FROM shopping_lists WHERE id=:id").bindparams(id=list_id))
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -6,16 +6,13 @@ Create Date: 2024-03-10 05:08:32.397027
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "09aba125b57a"
|
||||
down_revision = "2298bb460ffd"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def is_postgres():
|
||||
|
||||
@@ -6,7 +6,7 @@ Create Date: 2024-03-18 02:28:15.896959
|
||||
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
@@ -20,8 +20,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "d7c6efd2de42"
|
||||
down_revision = "09aba125b57a"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def is_postgres():
|
||||
@@ -32,9 +32,9 @@ def new_user_rating(user_id: Any, recipe_id: Any, rating: float | None = None, i
|
||||
if is_postgres():
|
||||
id = str(uuid4())
|
||||
else:
|
||||
id = "%.32x" % uuid4().int
|
||||
id = "%.32x" % uuid4().int # noqa: UP031
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
return {
|
||||
"id": id,
|
||||
"user_id": user_id,
|
||||
@@ -202,8 +202,6 @@ def downgrade():
|
||||
)
|
||||
op.drop_index(op.f("ix_recipes_rating"), table_name="recipes")
|
||||
op.alter_column("recipes", "rating", existing_type=sa.Float(), type_=sa.INTEGER(), existing_nullable=True)
|
||||
op.create_unique_constraint("ingredient_units_name_group_id_key", "ingredient_units", ["name", "group_id"])
|
||||
op.create_unique_constraint("ingredient_foods_name_group_id_key", "ingredient_foods", ["name", "group_id"])
|
||||
op.create_table(
|
||||
"users_to_favorites",
|
||||
sa.Column("user_id", sa.CHAR(length=32), nullable=True),
|
||||
|
||||
@@ -14,8 +14,8 @@ from alembic import op
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "7788478a0338"
|
||||
down_revision = "d7c6efd2de42"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
@@ -11,12 +11,11 @@ from sqlalchemy import orm
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "32d69327997b"
|
||||
down_revision = "7788478a0338"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def is_postgres():
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
"""add households
|
||||
|
||||
Revision ID: feecc8ffb956
|
||||
Revises: 32d69327997b
|
||||
Create Date: 2024-07-12 16:16:29.973929
|
||||
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import sqlalchemy as sa
|
||||
from slugify import slugify
|
||||
from sqlalchemy import orm
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
from mealie.core.config import get_app_settings
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "feecc8ffb956"
|
||||
down_revision = "32d69327997b"
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
settings = get_app_settings()
|
||||
|
||||
|
||||
def is_postgres():
|
||||
return op.get_context().dialect.name == "postgresql"
|
||||
|
||||
|
||||
def generate_id() -> str:
|
||||
"""See GUID.convert_value_to_guid"""
|
||||
val = uuid4()
|
||||
if is_postgres():
|
||||
return str(val)
|
||||
else:
|
||||
return f"{val.int:032x}"
|
||||
|
||||
|
||||
def dedupe_cookbook_slugs():
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
with session:
|
||||
sql = sa.text(
|
||||
dedent(
|
||||
"""
|
||||
SELECT slug, group_id, COUNT(*)
|
||||
FROM cookbooks
|
||||
GROUP BY slug, group_id
|
||||
HAVING COUNT(*) > 1
|
||||
"""
|
||||
)
|
||||
)
|
||||
rows = session.execute(sql).fetchall()
|
||||
|
||||
for slug, group_id, _ in rows:
|
||||
sql = sa.text(
|
||||
dedent(
|
||||
"""
|
||||
SELECT id
|
||||
FROM cookbooks
|
||||
WHERE slug = :slug AND group_id = :group_id
|
||||
ORDER BY id
|
||||
"""
|
||||
)
|
||||
)
|
||||
cookbook_ids = session.execute(sql, {"slug": slug, "group_id": group_id}).fetchall()
|
||||
|
||||
for i, (cookbook_id,) in enumerate(cookbook_ids):
|
||||
if i == 0:
|
||||
continue
|
||||
|
||||
sql = sa.text(
|
||||
dedent(
|
||||
"""
|
||||
UPDATE cookbooks
|
||||
SET slug = :slug || '-' || :i
|
||||
WHERE id = :id
|
||||
"""
|
||||
)
|
||||
)
|
||||
session.execute(sql, {"slug": slug, "i": i, "id": cookbook_id})
|
||||
|
||||
|
||||
def create_household(session: orm.Session, group_id: str) -> str:
|
||||
# create/insert household
|
||||
household_id = generate_id()
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
household_data = {
|
||||
"id": household_id,
|
||||
"name": settings.DEFAULT_HOUSEHOLD,
|
||||
"slug": slugify(settings.DEFAULT_HOUSEHOLD),
|
||||
"group_id": group_id,
|
||||
"created_at": timestamp,
|
||||
"update_at": timestamp,
|
||||
}
|
||||
columns = ", ".join(household_data.keys())
|
||||
placeholders = ", ".join(f":{key}" for key in household_data.keys())
|
||||
sql_statement = f"INSERT INTO households ({columns}) VALUES ({placeholders})"
|
||||
|
||||
session.execute(sa.text(sql_statement), household_data)
|
||||
|
||||
# fetch group preferences so we can copy them over to household preferences
|
||||
migrated_field_defaults = {
|
||||
"private_group": True, # this is renamed later
|
||||
"first_day_of_week": 0,
|
||||
"recipe_public": True,
|
||||
"recipe_show_nutrition": False,
|
||||
"recipe_show_assets": False,
|
||||
"recipe_landscape_view": False,
|
||||
"recipe_disable_comments": False,
|
||||
"recipe_disable_amount": True,
|
||||
}
|
||||
sql_statement = (
|
||||
f"SELECT {', '.join(migrated_field_defaults.keys())} FROM group_preferences WHERE group_id = :group_id"
|
||||
)
|
||||
group_preferences = session.execute(sa.text(sql_statement), {"group_id": group_id}).fetchone()
|
||||
|
||||
# build preferences data
|
||||
if group_preferences:
|
||||
preferences_data: dict[str, Any] = {}
|
||||
for i, (field, default_value) in enumerate(migrated_field_defaults.items()):
|
||||
value = group_preferences[i]
|
||||
preferences_data[field] = value if value is not None else default_value
|
||||
else:
|
||||
preferences_data = migrated_field_defaults
|
||||
|
||||
preferences_data["id"] = generate_id()
|
||||
preferences_data["household_id"] = household_id
|
||||
preferences_data["created_at"] = timestamp
|
||||
preferences_data["update_at"] = timestamp
|
||||
preferences_data["private_household"] = preferences_data.pop("private_group")
|
||||
|
||||
# insert preferences data
|
||||
columns = ", ".join(preferences_data.keys())
|
||||
placeholders = ", ".join(f":{key}" for key in preferences_data.keys())
|
||||
sql_statement = f"INSERT INTO household_preferences ({columns}) VALUES ({placeholders})"
|
||||
|
||||
session.execute(sa.text(sql_statement), preferences_data)
|
||||
|
||||
return household_id
|
||||
|
||||
|
||||
def create_households_for_groups() -> dict[str, str]:
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
group_id_household_id_map: dict[str, str] = {}
|
||||
with session:
|
||||
rows = session.execute(sa.text("SELECT id FROM groups")).fetchall()
|
||||
for row in rows:
|
||||
group_id = row[0]
|
||||
group_id_household_id_map[group_id] = create_household(session, group_id)
|
||||
|
||||
return group_id_household_id_map
|
||||
|
||||
|
||||
def _do_assignment(session: orm.Session, table: str, group_id: str, household_id: str):
|
||||
sql = sa.text(
|
||||
dedent(
|
||||
f"""
|
||||
UPDATE {table}
|
||||
SET household_id = :household_id
|
||||
WHERE group_id = :group_id
|
||||
""",
|
||||
)
|
||||
)
|
||||
session.execute(sql, {"group_id": group_id, "household_id": household_id})
|
||||
|
||||
|
||||
def assign_households(group_id_household_id_map: dict[str, str]):
|
||||
tables = [
|
||||
"cookbooks",
|
||||
"group_events_notifiers",
|
||||
"group_meal_plan_rules",
|
||||
"invite_tokens",
|
||||
"recipe_actions",
|
||||
"users",
|
||||
"webhook_urls",
|
||||
]
|
||||
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
with session:
|
||||
for table in tables:
|
||||
for group_id, household_id in group_id_household_id_map.items():
|
||||
_do_assignment(session, table, group_id, household_id)
|
||||
|
||||
|
||||
def populate_household_data():
|
||||
group_id_household_id_map = create_households_for_groups()
|
||||
assign_households(group_id_household_id_map)
|
||||
|
||||
|
||||
def upgrade():
|
||||
dedupe_cookbook_slugs()
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"households",
|
||||
sa.Column("id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
sa.Column("slug", sa.String(), nullable=True),
|
||||
sa.Column("group_id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("update_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["group_id"],
|
||||
["groups.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("group_id", "name", name="household_name_group_id_key"),
|
||||
sa.UniqueConstraint("group_id", "slug", name="household_slug_group_id_key"),
|
||||
)
|
||||
op.create_index(op.f("ix_households_created_at"), "households", ["created_at"], unique=False)
|
||||
op.create_index(op.f("ix_households_group_id"), "households", ["group_id"], unique=False)
|
||||
op.create_index(op.f("ix_households_name"), "households", ["name"], unique=False)
|
||||
op.create_index(op.f("ix_households_slug"), "households", ["slug"], unique=False)
|
||||
op.create_table(
|
||||
"household_preferences",
|
||||
sa.Column("id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=False),
|
||||
sa.Column("private_household", sa.Boolean(), nullable=True),
|
||||
sa.Column("first_day_of_week", sa.Integer(), nullable=True),
|
||||
sa.Column("recipe_public", sa.Boolean(), nullable=True),
|
||||
sa.Column("recipe_show_nutrition", sa.Boolean(), nullable=True),
|
||||
sa.Column("recipe_show_assets", sa.Boolean(), nullable=True),
|
||||
sa.Column("recipe_landscape_view", sa.Boolean(), nullable=True),
|
||||
sa.Column("recipe_disable_comments", sa.Boolean(), nullable=True),
|
||||
sa.Column("recipe_disable_amount", sa.Boolean(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("update_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["household_id"],
|
||||
["households.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_household_preferences_created_at"), "household_preferences", ["created_at"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_household_preferences_household_id"), "household_preferences", ["household_id"], unique=False
|
||||
)
|
||||
|
||||
with op.batch_alter_table("cookbooks") as batch_op:
|
||||
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
|
||||
batch_op.create_index(op.f("ix_cookbooks_household_id"), ["household_id"], unique=False)
|
||||
batch_op.create_foreign_key("fk_cookbooks_household_id", "households", ["household_id"], ["id"])
|
||||
|
||||
# not directly related to households, but important for frontend routes
|
||||
batch_op.create_unique_constraint("cookbook_slug_group_id_key", ["slug", "group_id"])
|
||||
|
||||
with op.batch_alter_table("group_events_notifiers") as batch_op:
|
||||
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
|
||||
batch_op.create_index(op.f("ix_group_events_notifiers_household_id"), ["household_id"], unique=False)
|
||||
batch_op.create_foreign_key("fk_group_events_notifiers_household_id", "households", ["household_id"], ["id"])
|
||||
|
||||
with op.batch_alter_table("group_meal_plan_rules") as batch_op:
|
||||
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
|
||||
batch_op.create_index(op.f("ix_group_meal_plan_rules_household_id"), ["household_id"], unique=False)
|
||||
batch_op.create_foreign_key("fk_group_meal_plan_rules_household_id", "households", ["household_id"], ["id"])
|
||||
|
||||
with op.batch_alter_table("invite_tokens") as batch_op:
|
||||
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
|
||||
batch_op.create_index(op.f("ix_invite_tokens_household_id"), ["household_id"], unique=False)
|
||||
batch_op.create_foreign_key("fk_invite_tokens_household_id", "households", ["household_id"], ["id"])
|
||||
|
||||
with op.batch_alter_table("recipe_actions") as batch_op:
|
||||
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
|
||||
batch_op.create_index(op.f("ix_recipe_actions_household_id"), ["household_id"], unique=False)
|
||||
batch_op.create_foreign_key("fk_recipe_actions_household_id", "households", ["household_id"], ["id"])
|
||||
|
||||
with op.batch_alter_table("users") as batch_op:
|
||||
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
|
||||
batch_op.create_index(op.f("ix_users_household_id"), ["household_id"], unique=False)
|
||||
batch_op.create_foreign_key("fk_users_household_id", "households", ["household_id"], ["id"])
|
||||
|
||||
with op.batch_alter_table("webhook_urls") as batch_op:
|
||||
batch_op.add_column(sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True))
|
||||
batch_op.create_index(op.f("ix_webhook_urls_household_id"), ["household_id"], unique=False)
|
||||
batch_op.create_foreign_key("fk_webhook_urls_household_id", "households", ["household_id"], ["id"])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
populate_household_data()
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, "webhook_urls", type_="foreignkey")
|
||||
op.drop_index(op.f("ix_webhook_urls_household_id"), table_name="webhook_urls")
|
||||
op.drop_column("webhook_urls", "household_id")
|
||||
op.drop_constraint(None, "users", type_="foreignkey")
|
||||
op.drop_index(op.f("ix_users_household_id"), table_name="users")
|
||||
op.drop_column("users", "household_id")
|
||||
op.drop_constraint(None, "recipe_actions", type_="foreignkey")
|
||||
op.drop_index(op.f("ix_recipe_actions_household_id"), table_name="recipe_actions")
|
||||
op.drop_column("recipe_actions", "household_id")
|
||||
op.drop_constraint(None, "invite_tokens", type_="foreignkey")
|
||||
op.drop_index(op.f("ix_invite_tokens_household_id"), table_name="invite_tokens")
|
||||
op.drop_column("invite_tokens", "household_id")
|
||||
op.drop_constraint(None, "group_meal_plan_rules", type_="foreignkey")
|
||||
op.drop_index(op.f("ix_group_meal_plan_rules_household_id"), table_name="group_meal_plan_rules")
|
||||
op.drop_column("group_meal_plan_rules", "household_id")
|
||||
op.drop_constraint(None, "group_events_notifiers", type_="foreignkey")
|
||||
op.drop_index(op.f("ix_group_events_notifiers_household_id"), table_name="group_events_notifiers")
|
||||
op.drop_column("group_events_notifiers", "household_id")
|
||||
op.drop_constraint(None, "cookbooks", type_="foreignkey")
|
||||
op.drop_index(op.f("ix_cookbooks_household_id"), table_name="cookbooks")
|
||||
op.drop_column("cookbooks", "household_id")
|
||||
op.drop_constraint("cookbook_slug_group_id_key", "cookbooks", type_="unique")
|
||||
op.drop_index(op.f("ix_household_preferences_household_id"), table_name="household_preferences")
|
||||
op.drop_index(op.f("ix_household_preferences_created_at"), table_name="household_preferences")
|
||||
op.drop_table("household_preferences")
|
||||
op.drop_index(op.f("ix_households_slug"), table_name="households")
|
||||
op.drop_index(op.f("ix_households_name"), table_name="households")
|
||||
op.drop_index(op.f("ix_households_group_id"), table_name="households")
|
||||
op.drop_index(op.f("ix_households_created_at"), table_name="households")
|
||||
op.drop_table("households")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,75 @@
|
||||
"""added household recipe lock setting and household management user permission
|
||||
|
||||
Revision ID: be568e39ffdf
|
||||
Revises: feecc8ffb956
|
||||
Create Date: 2024-09-02 21:39:49.210355
|
||||
|
||||
"""
|
||||
|
||||
from textwrap import dedent
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "be568e39ffdf"
|
||||
down_revision = "feecc8ffb956"
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def populate_defaults():
|
||||
if op.get_context().dialect.name == "postgresql":
|
||||
TRUE = "TRUE"
|
||||
FALSE = "FALSE"
|
||||
else:
|
||||
TRUE = "1"
|
||||
FALSE = "0"
|
||||
|
||||
op.execute(
|
||||
dedent(
|
||||
f"""
|
||||
UPDATE household_preferences
|
||||
SET lock_recipe_edits_from_other_households = {TRUE}
|
||||
WHERE lock_recipe_edits_from_other_households IS NULL
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
dedent(
|
||||
f"""
|
||||
UPDATE users
|
||||
SET can_manage_household = {FALSE}
|
||||
WHERE can_manage_household IS NULL AND admin = {FALSE}
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
dedent(
|
||||
f"""
|
||||
UPDATE users
|
||||
SET can_manage_household = {TRUE}
|
||||
WHERE can_manage_household IS NULL AND admin = {TRUE}
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"household_preferences",
|
||||
sa.Column("lock_recipe_edits_from_other_households", sa.Boolean(), nullable=True),
|
||||
)
|
||||
op.add_column("users", sa.Column("can_manage_household", sa.Boolean(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
populate_defaults()
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("users", "can_manage_household")
|
||||
op.drop_column("household_preferences", "lock_recipe_edits_from_other_households")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,53 @@
|
||||
"""add households filter to meal plans
|
||||
|
||||
Revision ID: 1fe4bd37ccc8
|
||||
Revises: be568e39ffdf
|
||||
Create Date: 2024-09-18 14:52:55.831540
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1fe4bd37ccc8"
|
||||
down_revision: str | None = "be568e39ffdf"
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"plan_rules_to_households",
|
||||
sa.Column("group_plan_rule_id", mealie.db.migration_types.GUID(), nullable=True),
|
||||
sa.Column("household_id", mealie.db.migration_types.GUID(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["group_plan_rule_id"],
|
||||
["group_meal_plan_rules.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["household_id"],
|
||||
["households.id"],
|
||||
),
|
||||
sa.UniqueConstraint("group_plan_rule_id", "household_id", name="group_plan_rule_id_household_id_key"),
|
||||
)
|
||||
with op.batch_alter_table("plan_rules_to_households", schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_plan_rules_to_households_group_plan_rule_id"), ["group_plan_rule_id"], unique=False
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_plan_rules_to_households_household_id"), ["household_id"], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("plan_rules_to_households", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_plan_rules_to_households_household_id"))
|
||||
batch_op.drop_index(batch_op.f("ix_plan_rules_to_households_group_plan_rule_id"))
|
||||
|
||||
op.drop_table("plan_rules_to_households")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,39 @@
|
||||
"""'add the rest of the schema.org nutrition properties'
|
||||
|
||||
Revision ID: 602927e1013e
|
||||
Revises: 1fe4bd37ccc8
|
||||
Create Date: 2024-10-01 14:17:00.611398
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "602927e1013e"
|
||||
down_revision: str | None = "1fe4bd37ccc8"
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("recipe_nutrition", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("cholesterol_content", sa.String(), nullable=True))
|
||||
batch_op.add_column(sa.Column("saturated_fat_content", sa.String(), nullable=True))
|
||||
batch_op.add_column(sa.Column("trans_fat_content", sa.String(), nullable=True))
|
||||
batch_op.add_column(sa.Column("unsaturated_fat_content", sa.String(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("recipe_nutrition", schema=None) as batch_op:
|
||||
batch_op.drop_column("unsaturated_fat_content")
|
||||
batch_op.drop_column("trans_fat_content")
|
||||
batch_op.drop_column("saturated_fat_content")
|
||||
batch_op.drop_column("cholesterol_content")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,188 @@
|
||||
"""added query_filter_string to cookbook and mealplan
|
||||
|
||||
Revision ID: 86054b40fd06
|
||||
Revises: 602927e1013e
|
||||
Create Date: 2024-10-08 21:17:31.601903
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
||||
from alembic import op
|
||||
from mealie.db.models._model_utils import guid
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "86054b40fd06"
|
||||
down_revision: str | None = "602927e1013e"
|
||||
branch_labels: str | tuple[str, ...] | None = None
|
||||
depends_on: str | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
# Intermediate table definitions
|
||||
class SqlAlchemyBase(orm.DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class Category(SqlAlchemyBase):
|
||||
__tablename__ = "categories"
|
||||
id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
|
||||
|
||||
class Tag(SqlAlchemyBase):
|
||||
__tablename__ = "tags"
|
||||
id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
|
||||
|
||||
class Tool(SqlAlchemyBase):
|
||||
__tablename__ = "tools"
|
||||
id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
|
||||
|
||||
class Household(SqlAlchemyBase):
|
||||
__tablename__ = "households"
|
||||
id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
|
||||
|
||||
cookbooks_to_categories = sa.Table(
|
||||
"cookbooks_to_categories",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("cookbook_id", guid.GUID, sa.ForeignKey("cookbooks.id"), index=True),
|
||||
sa.Column("category_id", guid.GUID, sa.ForeignKey("categories.id"), index=True),
|
||||
)
|
||||
|
||||
cookbooks_to_tags = sa.Table(
|
||||
"cookbooks_to_tags",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("cookbook_id", guid.GUID, sa.ForeignKey("cookbooks.id"), index=True),
|
||||
sa.Column("tag_id", guid.GUID, sa.ForeignKey("tags.id"), index=True),
|
||||
)
|
||||
|
||||
cookbooks_to_tools = sa.Table(
|
||||
"cookbooks_to_tools",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("cookbook_id", guid.GUID, sa.ForeignKey("cookbooks.id"), index=True),
|
||||
sa.Column("tool_id", guid.GUID, sa.ForeignKey("tools.id"), index=True),
|
||||
)
|
||||
|
||||
plan_rules_to_categories = sa.Table(
|
||||
"plan_rules_to_categories",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("group_plan_rule_id", guid.GUID, sa.ForeignKey("group_meal_plan_rules.id"), index=True),
|
||||
sa.Column("category_id", guid.GUID, sa.ForeignKey("categories.id"), index=True),
|
||||
)
|
||||
|
||||
plan_rules_to_tags = sa.Table(
|
||||
"plan_rules_to_tags",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("plan_rule_id", guid.GUID, sa.ForeignKey("group_meal_plan_rules.id"), index=True),
|
||||
sa.Column("tag_id", guid.GUID, sa.ForeignKey("tags.id"), index=True),
|
||||
)
|
||||
|
||||
plan_rules_to_households = sa.Table(
|
||||
"plan_rules_to_households",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("group_plan_rule_id", guid.GUID, sa.ForeignKey("group_meal_plan_rules.id"), index=True),
|
||||
sa.Column("household_id", guid.GUID, sa.ForeignKey("households.id"), index=True),
|
||||
)
|
||||
|
||||
|
||||
class CookBook(SqlAlchemyBase):
|
||||
__tablename__ = "cookbooks"
|
||||
|
||||
id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
query_filter_string: orm.Mapped[str] = orm.mapped_column(sa.String, nullable=False, default="")
|
||||
|
||||
categories: orm.Mapped[list[Category]] = orm.relationship(
|
||||
Category, secondary=cookbooks_to_categories, single_parent=True
|
||||
)
|
||||
require_all_categories: orm.Mapped[bool | None] = orm.mapped_column(sa.Boolean, default=True)
|
||||
|
||||
tags: orm.Mapped[list[Tag]] = orm.relationship(Tag, secondary=cookbooks_to_tags, single_parent=True)
|
||||
require_all_tags: orm.Mapped[bool | None] = orm.mapped_column(sa.Boolean, default=True)
|
||||
|
||||
tools: orm.Mapped[list[Tool]] = orm.relationship(Tool, secondary=cookbooks_to_tools, single_parent=True)
|
||||
require_all_tools: orm.Mapped[bool | None] = orm.mapped_column(sa.Boolean, default=True)
|
||||
|
||||
|
||||
class GroupMealPlanRules(SqlAlchemyBase):
|
||||
__tablename__ = "group_meal_plan_rules"
|
||||
|
||||
id: orm.Mapped[guid.GUID] = orm.mapped_column(guid.GUID, primary_key=True, default=guid.GUID.generate)
|
||||
query_filter_string: orm.Mapped[str] = orm.mapped_column(sa.String, nullable=False, default="")
|
||||
|
||||
categories: orm.Mapped[list[Category]] = orm.relationship(Category, secondary=plan_rules_to_categories)
|
||||
tags: orm.Mapped[list[Tag]] = orm.relationship(Tag, secondary=plan_rules_to_tags)
|
||||
households: orm.Mapped[list["Household"]] = orm.relationship("Household", secondary=plan_rules_to_households)
|
||||
|
||||
|
||||
def migrate_cookbooks():
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
|
||||
cookbooks = session.query(CookBook).all()
|
||||
for cookbook in cookbooks:
|
||||
parts = []
|
||||
if cookbook.categories:
|
||||
relop = "CONTAINS ALL" if cookbook.require_all_categories else "IN"
|
||||
vals = ",".join([f'"{cat.id}"' for cat in cookbook.categories])
|
||||
parts.append(f"recipe_category.id {relop} [{vals}]")
|
||||
if cookbook.tags:
|
||||
relop = "CONTAINS ALL" if cookbook.require_all_tags else "IN"
|
||||
vals = ",".join([f'"{tag.id}"' for tag in cookbook.tags])
|
||||
parts.append(f"tags.id {relop} [{vals}]")
|
||||
if cookbook.tools:
|
||||
relop = "CONTAINS ALL" if cookbook.require_all_tools else "IN"
|
||||
vals = ",".join([f'"{tool.id}"' for tool in cookbook.tools])
|
||||
parts.append(f"tools.id {relop} [{vals}]")
|
||||
|
||||
cookbook.query_filter_string = " AND ".join(parts)
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
def migrate_mealplan_rules():
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
|
||||
rules = session.query(GroupMealPlanRules).all()
|
||||
for rule in rules:
|
||||
parts = []
|
||||
if rule.categories:
|
||||
vals = ",".join([f'"{cat.id}"' for cat in rule.categories])
|
||||
parts.append(f"recipe_category.id CONTAINS ALL [{vals}]")
|
||||
if rule.tags:
|
||||
vals = ",".join([f'"{tag.id}"' for tag in rule.tags])
|
||||
parts.append(f"tags.id CONTAINS ALL [{vals}]")
|
||||
if rule.households:
|
||||
vals = ",".join([f'"{household.id}"' for household in rule.households])
|
||||
parts.append(f"household_id IN [{vals}]")
|
||||
|
||||
rule.query_filter_string = " AND ".join(parts)
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("cookbooks", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("query_filter_string", sa.String(), nullable=False, server_default=""))
|
||||
|
||||
with op.batch_alter_table("group_meal_plan_rules", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("query_filter_string", sa.String(), nullable=False, server_default=""))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
migrate_cookbooks()
|
||||
migrate_mealplan_rules()
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("group_meal_plan_rules", schema=None) as batch_op:
|
||||
batch_op.drop_column("query_filter_string")
|
||||
|
||||
with op.batch_alter_table("cookbooks", schema=None) as batch_op:
|
||||
batch_op.drop_column("query_filter_string")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,5 +1,8 @@
|
||||
import json
|
||||
|
||||
from fastapi import FastAPI
|
||||
from freezegun import freeze_time
|
||||
|
||||
from mealie.app import app
|
||||
from mealie.core.config import determine_data_dir
|
||||
|
||||
@@ -36,11 +39,12 @@ HTML_TEMPLATE = """<!-- Custom HTML site displayed as the Home chapter -->
|
||||
HTML_PATH = DATA_DIR.parent.parent.joinpath("docs/docs/overrides/api.html")
|
||||
|
||||
|
||||
def generate_api_docs(my_app):
|
||||
def generate_api_docs(my_app: FastAPI):
|
||||
with open(HTML_PATH, "w") as fd:
|
||||
text = HTML_TEMPLATE.replace("MY_SPECIFIC_TEXT", json.dumps(my_app.openapi()))
|
||||
fd.write(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_api_docs(app)
|
||||
with freeze_time("2024-01-20T17:00:55Z"):
|
||||
generate_api_docs(app)
|
||||
|
||||
@@ -67,7 +67,7 @@ def rename_non_compliant_paths():
|
||||
kabab case.
|
||||
"""
|
||||
|
||||
ignore_files = ["DS_Store", ".gitkeep"]
|
||||
ignore_files = ["DS_Store", ".gitkeep", "af-ZA.json", "en-US.json"]
|
||||
|
||||
ignore_extensions = [".pyc", ".pyo", ".py"]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
from fastapi import FastAPI
|
||||
from jinja2 import Template
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from utils import PROJECT_DIR, CodeTemplates, HTTPRequest, RouteObject, RequestType
|
||||
from utils import PROJECT_DIR, CodeTemplates, HTTPRequest, RouteObject
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
|
||||
@@ -17,9 +17,16 @@ class PathObject(BaseModel):
|
||||
http_verbs: list[HTTPRequest]
|
||||
|
||||
|
||||
def get_path_objects(app: FastAPI):
|
||||
paths = []
|
||||
def force_include_in_schema(app: FastAPI):
|
||||
# clear schema cache
|
||||
app.openapi_schema = None
|
||||
for route in app.routes:
|
||||
route.include_in_schema = True
|
||||
|
||||
|
||||
def get_path_objects(app: FastAPI):
|
||||
force_include_in_schema(app)
|
||||
paths = []
|
||||
for key, value in app.openapi().items():
|
||||
if key == "paths":
|
||||
for key, value2 in value.items():
|
||||
|
||||
@@ -35,6 +35,7 @@ LOCALE_DATA: dict[str, LocaleData] = {
|
||||
"es-ES": LocaleData(name="Español (Spanish)"),
|
||||
"fi-FI": LocaleData(name="Suomi (Finnish)"),
|
||||
"fr-FR": LocaleData(name="Français (French)"),
|
||||
"fr-BE": LocaleData(name="Belge (Belgian)"),
|
||||
"gl-ES": LocaleData(name="Galego (Galician)"),
|
||||
"he-IL": LocaleData(name="עברית (Hebrew)", dir="rtl"),
|
||||
"hr-HR": LocaleData(name="Hrvatski (Croatian)"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
@@ -23,6 +24,11 @@ def render_python_template(template_file: Path | str, dest: Path, data: dict):
|
||||
|
||||
dest.write_text(text)
|
||||
|
||||
# lint/format file with Ruff
|
||||
log.info(f"Formatting {dest}")
|
||||
subprocess.run(["poetry", "run", "ruff", "check", str(dest), "--fix"])
|
||||
subprocess.run(["poetry", "run", "ruff", "format", str(dest)])
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodeSlicer:
|
||||
|
||||
@@ -173,7 +173,7 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||
"dateAdded": "2022-09-03",
|
||||
"dateUpdated": "2022-09-10T15:18:19.866085",
|
||||
"createdAt": "2022-09-03T18:31:17.488118",
|
||||
"updateAt": "2022-09-10T15:18:19.869630",
|
||||
"updatedAt": "2022-09-10T15:18:19.869630",
|
||||
"recipeInstructions": [
|
||||
{
|
||||
"id": "60ae53a3-b3ff-40ee-bae3-89fea0b1c637",
|
||||
|
||||
@@ -78,6 +78,7 @@ RUN echo "crfpp-container"
|
||||
# Production Image
|
||||
###############################################
|
||||
FROM python-base as production
|
||||
LABEL org.opencontainers.image.source="https://github.com/mealie-recipes/mealie"
|
||||
ENV PRODUCTION=true
|
||||
ENV TESTING=false
|
||||
|
||||
@@ -92,6 +93,9 @@ RUN apt-get update \
|
||||
libldap-2.5 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# create directory used for Docker Secrets
|
||||
RUN mkdir -p /run/secrets
|
||||
|
||||
# copying poetry and venv into image
|
||||
COPY --from=builder-base $POETRY_HOME $POETRY_HOME
|
||||
COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
mailpit:
|
||||
image: axllent/mailpit:latest
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: "3.4"
|
||||
services:
|
||||
mealie:
|
||||
container_name: mealie
|
||||
@@ -24,7 +23,6 @@ services:
|
||||
POSTGRES_SERVER: postgres
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_DB: mealie
|
||||
|
||||
# =====================================
|
||||
# Email Configuration
|
||||
# SMTP_HOST=
|
||||
|
||||
@@ -9,77 +9,7 @@ How exactly you need to modify it is of course highly contextual to the change y
|
||||
|
||||
## Using Alembic to generate upgrade script
|
||||
|
||||
In your dev container you can run something like (change the message) `task py:migrate "Add creation tag to group preferences"` to have Alembic generate an upgrade script for you.
|
||||
In your dev container you can run something like (change the message) `task py:migrate -- "Add creation tag to group preferences"` to have Alembic generate an upgrade script for you.
|
||||
|
||||
The script Alembic generates, will be limited! (Perhaps there's a way to resolve that? Haven't looked into it yet)
|
||||
For example, Alembic generated a script _similar_ to this (it has been modified already to have accurate foreign key names, for instance):
|
||||
|
||||
```Python
|
||||
"""Add creation tag to group preferences
|
||||
|
||||
Revision ID: 0ea6eb8eaa44
|
||||
Revises: ba1e4a6cfe99
|
||||
Create Date: 2024-01-04 12:40:03.062671
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0ea6eb8eaa44"
|
||||
down_revision = "ba1e4a6cfe99"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"group_preferences", sa.Column("recipe_creation_tag", mealie.db.migration_types.GUID(), nullable=True)
|
||||
)
|
||||
op.create_foreign_key("fk_groupprefs_tags", "group_preferences", "tags", ["recipe_creation_tag"], ["id"])
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint("fk_groupprefs_tags", "group_preferences", type_="foreignkey")
|
||||
op.drop_column("group_preferences", "recipe_creation_tag")
|
||||
### end Alembic commands ###
|
||||
```
|
||||
|
||||
But when trying to actually use that upgrade script, it becomes clear that our SQLite database doesn't like them. The minor modification needed looks like:
|
||||
|
||||
```Python
|
||||
"""Add creation tag to group preferences
|
||||
|
||||
Revision ID: 0ea6eb8eaa44
|
||||
Revises: ba1e4a6cfe99
|
||||
Create Date: 2024-01-04 12:40:03.062671
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
import mealie.db.migration_types
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0ea6eb8eaa44"
|
||||
down_revision = "ba1e4a6cfe99"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table("group_preferences", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("recipe_creation_tag", mealie.db.migration_types.GUID(), nullable=True))
|
||||
batch_op.create_foreign_key("fk_groupprefs_tags", "tags", ["recipe_creation_tag"], ["id"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("group_preferences", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("fk_groupprefs_tags", type_="foreignkey")
|
||||
batch_op.drop_column("recipe_creation_tag")
|
||||
```
|
||||
Alembic's script migration isn't perfect, so you will need to review which changes are generated. You will also need to make sure any custom operations work on both SQLite and Postgres.
|
||||
There are some known limitations with our migrations and Alembic's auto-generation, which is accounted for in `/alembic/env.py`. If any of your migrations overlap with the columns in `include_object`, you may need to manually adjust the migration.
|
||||
|
||||
59
docs/docs/contributors/developers-guide/migration-guide.md
Normal file
59
docs/docs/contributors/developers-guide/migration-guide.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Migration Guide
|
||||
|
||||
This guide is a reference for developers maintaining custom integrations with Mealie. While we aim to keep breaking changes to a minimum, major versions are likely to contain at least *some* breaking changes. To clarify: *most users do not need to worry about this, this is **only** for those maintaining integrations and/or leveraging the API*.
|
||||
|
||||
While this guide aims to simplify the migration process for developers, it's not necessarily a comprehensive list of breaking changes. Starting with v2, a comprehensive list of breaking changes are highlighted in the release notes.
|
||||
|
||||
## V1 → V2
|
||||
|
||||
The biggest change between V1 and V2 is the introduction of Households. For more information on how households work in relation to groups/users, check out the [Groups and Households](./features.md#groups-and-households) section in the Features guide.
|
||||
|
||||
### `updateAt` is now `updatedAt`
|
||||
|
||||
We have renamed the `updateAt` field to `updatedAt`. While the API will still accept `updateAt` as an alias, the API will return it as `updatedAt`. The field's behavior has otherwise been unchanged.
|
||||
|
||||
### Backend Endpoint Changes
|
||||
|
||||
These endpoints have moved, but are otherwise unchanged:
|
||||
|
||||
- `/recipes/create-url` -> `/recipes/create/url`
|
||||
- `/recipes/create-url/bulk` -> `/recipes/create/url/bulk`
|
||||
- `/recipes/create-from-zip` -> `/recipes/create/zip`
|
||||
- `/recipes/create-from-image` -> `/recipes/create/image`
|
||||
- `/groups/webhooks` -> `/households/webhooks`
|
||||
- `/groups/shopping/items` -> `/households/shopping/items`
|
||||
- `/groups/shopping/lists` -> `/households/shopping/lists`
|
||||
- `/groups/mealplans` -> `/households/mealplans`
|
||||
- `/groups/mealplans/rules` -> `/households/mealplans/rules`
|
||||
- `/groups/invitations` -> `/households/invitations`
|
||||
- `/groups/recipe-actions` -> `/households/recipe-actions`
|
||||
- `/groups/events/notifications` -> `/households/events/notifications`
|
||||
- `/groups/cookbooks` -> `/households/cookbooks`
|
||||
- `/explore/foods/{group_slug}` -> `/explore/groups/{group_slug}/foods`
|
||||
- `/explore/organizers/{group_slug}/categories` -> `/explore/groups/{group_slug}/categories`
|
||||
- `/explore/organizers/{group_slug}/tags` -> `/explore/groups/{group_slug}/tags`
|
||||
- `/explore/organizers/{group_slug}/tools` -> `/explore/groups/{group_slug}/tools`
|
||||
- `/explore/cookbooks/{group_slug}` -> `/explore/groups/{group_slug}/cookbooks`
|
||||
- `/explore/recipes/{group_slug}` -> `/explore/groups/{group_slug}/recipes`
|
||||
|
||||
`/groups/members` previously returned a `UserOut` object, but now returns a `UserSummary`. Should you need the full user information (username, email, etc.), rather than just the summary, see `/households/members` instead for the household members.
|
||||
`/groups/members` previously returned a list of users, but now returns paginated users (similar to all other list endpoints).
|
||||
|
||||
These endpoints have been completely removed:
|
||||
|
||||
- `/admin/analytics` (no longer used)
|
||||
- `/groups/permissions` (see household permissions)
|
||||
- `/groups/statistics` (see household statistics)
|
||||
- `/groups/categories` (see organizer endpoints)
|
||||
- `/recipes/summary/untagged` (no longer used)
|
||||
- `/recipes/summary/uncategorized` (no longer used)
|
||||
- `/users/group-users` (see `/groups/members` and `/households/members`)
|
||||
|
||||
### Frontend Links
|
||||
|
||||
These frontend pages have moved:
|
||||
|
||||
- `/group/mealplan/...` -> `/household/mealplan/...`
|
||||
- `/group/members` -> `/household/members`
|
||||
- `/group/notifiers` -> `/household/notifiers`
|
||||
- `/group/webhooks` -> `/household/webhooks`
|
||||
@@ -23,7 +23,7 @@ function import_from_file () {
|
||||
do
|
||||
echo $line
|
||||
curl -X 'POST' \
|
||||
"$3/api/recipes/create-url" \
|
||||
"$3/api/recipes/create/url" \
|
||||
-H "Authorization: Bearer $2" \
|
||||
-H 'accept: application/json' \
|
||||
-H 'Content-Type: application/json' \
|
||||
@@ -81,7 +81,7 @@ def import_from_file(input_file, token, mealie_url):
|
||||
data = {
|
||||
'url': line
|
||||
}
|
||||
response = requests.post(mealie_url + "/api/recipes/create-url", headers=headers, json=data)
|
||||
response = requests.post(mealie_url + "/api/recipes/create/url", headers=headers, json=data)
|
||||
print(response.text)
|
||||
|
||||
input_file="list"
|
||||
|
||||
@@ -18,32 +18,26 @@ Create an API token from Mealie's User Settings page (https://hay-kot.github.io/
|
||||
#### 2. Create Home Assistant Sensors
|
||||
|
||||
Create REST sensors in home assistant to get the details of today's meal.
|
||||
We will create sensors to get the name and ID of the first meal in today's meal plan (note that this may not be what is wanted if there is more than one meal planned for the day). We need the ID as well as the name to be able to retreive the image for the meal.
|
||||
We will create sensors to get the name and ID of the first meal in today's meal plan (note that this may not be what is wanted if there is more than one meal planned for the day). We need the ID as well as the name to be able to retrieve the image for the meal.
|
||||
|
||||
Make sure the url and port (`http://mealie:9000` ) matches your installation's address and _API_ port.
|
||||
|
||||
```yaml
|
||||
- platform: rest
|
||||
resource: "http://mealie:9000/api/groups/mealplans/today"
|
||||
method: GET
|
||||
name: Mealie todays meal
|
||||
headers:
|
||||
Authorization: Bearer <<API_TOKEN>>
|
||||
value_template: "{{ value_json[0].recipe.name }}"
|
||||
force_update: true
|
||||
scan_interval: 30
|
||||
```
|
||||
|
||||
```yaml
|
||||
- platform: rest
|
||||
resource: "http://mealie:9000/api/groups/mealplans/today"
|
||||
method: GET
|
||||
name: Mealie todays meal ID
|
||||
headers:
|
||||
Authorization: Bearer <<API_TOKEN>>
|
||||
value_template: "{{ value_json[0].recipe.id }}"
|
||||
force_update: true
|
||||
scan_interval: 30
|
||||
rest:
|
||||
- resource: "http://mealie:9000/api/groups/mealplans/today"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: Bearer <<API_TOKEN>>
|
||||
scan_interval: 3600
|
||||
sensor:
|
||||
- name: Mealie todays meal
|
||||
value_template: "{{ value_json[0]['recipe']['name'] }}"
|
||||
force_update: true
|
||||
unique_id: mealie_todays_meal
|
||||
- name: Mealie todays meal ID
|
||||
value_template: "{{ value_json[0]['recipe']['id'] }}"
|
||||
force_update: true
|
||||
unique_id: mealie_todays_meal_id
|
||||
```
|
||||
|
||||
#### 3. Create a Camera Entity
|
||||
|
||||
@@ -7,7 +7,9 @@ You can use bookmarklets to generate a bookmark that will take your current loca
|
||||
You can use a [bookmarklet generator site](https://caiorss.github.io/bookmarklet-maker/) and the code below to generate a bookmark for your site. Just change the `http://localhost:8080` to your sites web address and follow the instructions.
|
||||
|
||||
```js
|
||||
var url = document.URL;
|
||||
var url = document.URL.endsWith('/') ?
|
||||
document.URL.slice(0, -1) :
|
||||
document.URL;
|
||||
var mealie = "http://localhost:8080";
|
||||
var group_slug = "home" // Change this to your group slug. You can obtain this from your URL after logging-in to Mealie
|
||||
var use_keywords= "&use_keywords=1" // Optional - use keywords from recipe - update to "" if you don't want that
|
||||
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Don't forget to change the <code>mydomain.duckns</code> into your personal domain and the <code>duckdnstoken</code> into your token and remove the brackets.
|
||||
Don't forget to change the <code>mydomain.duckdns</code> into your personal domain and the <code>duckdnstoken</code> into your token and remove the brackets.
|
||||
|
||||
## Step 3: Change the config files
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# OpenID Connect (OIDC) Authentication
|
||||
|
||||
:octicons-tag-24: v2.0.0
|
||||
|
||||
!!! note
|
||||
Breaking changes to OIDC Authentication were introduced with Mealie v2. Please see the below for [migration steps](#migration-from-mealie-v1x).
|
||||
|
||||
Looking instead for the docs for Mealie :octicons-tag-24: v1.x? [Click here](./oidc.md)
|
||||
|
||||
Mealie supports 3rd party authentication via [OpenID Connect (OIDC)](https://openid.net/connect/), an identity layer built on top of OAuth2. OIDC is supported by many Identity Providers (IdP), including:
|
||||
|
||||
- [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect)
|
||||
- [Authelia](https://www.authelia.com/configuration/identity-providers/open-id-connect/)
|
||||
- [Keycloak](https://www.keycloak.org/docs/latest/securing_apps/#_oidc)
|
||||
- [Okta](https://www.okta.com/openid-connect/)
|
||||
|
||||
## Account Linking
|
||||
|
||||
Signing in with OAuth will automatically find your account in Mealie and link to it. If a user does not exist in Mealie, then one will be created (if enabled), but will be unable to log in with any other authentication method. An admin can configure another authentication method for such a user.
|
||||
|
||||
If a user previously accessed Mealie via credentials and you want to no longer allow users to log in with `LDAP` or `Mealie` credentials, then you can set the user's *Authentication Method* to `OIDC`. Conversely, if a user's auth method is not `OIDC`, then they can still log in with whatever their auth method is as well as OIDC.
|
||||
|
||||
## Provider Setup
|
||||
|
||||
Before you can start using OIDC Authentication, you must first configure a new client application in your identity provider. Your identity provider must support the OAuth **Authorization Code flow with PKCE**. The steps will vary by provider, but generally, the steps are as follows.
|
||||
|
||||
1. Create a new client application
|
||||
- The Provider type should be OIDC or OAuth2
|
||||
- The Grant type should be `Authorization Code`
|
||||
- The Client type should be `confidential` (you should have a **Client Secret**)
|
||||
|
||||
2. Configure redirect URI
|
||||
|
||||
The redirect URI(s) that are needed:
|
||||
|
||||
1. `http(s)://DOMAIN:PORT/login`
|
||||
2. `https(s)://DOMAIN:PORT/login?direct=1`
|
||||
1. This URI is only required if your IdP supports [RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) such as Keycloak. You may also be able to combine this into the previous URI by using a wildcard: `http(s)://DOMAIN:PORT/login*`
|
||||
|
||||
The redirect URI(s) should include any URL that Mealie is accessible from. Some examples include
|
||||
|
||||
http://localhost:9091/login
|
||||
https://mealie.example.com/login
|
||||
|
||||
3. Configure allowed scopes
|
||||
|
||||
The scopes required are `openid profile email`
|
||||
|
||||
If you plan to use the [groups](#groups) to configure access within Mealie, you will need to also add the scope defined by the `OIDC_GROUPS_CLAIM` environment variable. The default claim is `groups`
|
||||
|
||||
## Mealie Setup
|
||||
|
||||
Take the client id and your discovery URL and update your environment variables to include the required OIDC variables described in [Installation - Backend Configuration](../installation/backend-config.md#openid-connect-oidc).
|
||||
|
||||
### Groups
|
||||
|
||||
There are two (optional) [environment variables](../installation/backend-config.md#openid-connect-oidc) that can control which of the users in your IdP can log in to Mealie and what permissions they will have. Keep in mind that these groups **do not necessarily correspond to groups in Mealie**. The groups claim is configurable via the `OIDC_GROUPS_CLAIM` environment variable. The groups should be **defined in your IdP** and be returned in the configured claim value.
|
||||
|
||||
`OIDC_USER_GROUP`: Users must be a part of this group (within your IdP) to be able to log in.
|
||||
|
||||
`OIDC_ADMIN_GROUP`: Users that are in this group (within your IdP) will be made an **admin** in Mealie. Users in this group do not need to be in the `OIDC_USER_GROUP`
|
||||
|
||||
## Examples
|
||||
|
||||
Example configurations for several Identity Providers have been provided by the Community in the [GitHub Discussions](https://github.com/mealie-recipes/mealie/discussions/categories/oauth-provider-example).
|
||||
|
||||
If you don't see your provider and have successfully set it up, please consider [creating your own example](https://github.com/mealie-recipes/mealie/discussions/new?category=oauth-provider-example) so that others can have a smoother setup.
|
||||
|
||||
|
||||
## Migration from Mealie v1.x
|
||||
|
||||
**High level changes**
|
||||
|
||||
- A Client Secret is now required
|
||||
- CORS is no longer a requirement since all authentication happens server-side
|
||||
- A user will be successfully authenticated if they are part of *either* `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. Admins no longer need to be part of both groups
|
||||
- ID Token signing algorithm is now inferred using the `id_token_signing_alg_values_supported` metadata from the discovery URL
|
||||
|
||||
### Changes in your IdP
|
||||
|
||||
**Required**
|
||||
|
||||
- You must change the Mealie client in your IdP to be **confidential**. The option is different for every provider, but you need to obtain a **client secret**.
|
||||
|
||||
**Optional**
|
||||
|
||||
- You may now also remove the `OIDC_USER_GROUP` from your admin users if you so desire. Users within the `OIDC_ADMIN_GROUP` will now be able to successfully authenticate with only that group.
|
||||
- You may remove any CORS configuration. i.e. configured origins
|
||||
|
||||
### Changes in Mealie
|
||||
|
||||
**Required**
|
||||
|
||||
- After obtaining the **client secret** from your IdP, you must add it to Mealie using the `OIDC_CLIENT_SECRET` environment variable or via [docker secrets](../installation/backend-config.md#docker-secrets). This secret will not be logged on startup.
|
||||
|
||||
**Optional**
|
||||
|
||||
- Remove `OIDC_SIGNING_ALGORITHM` from your environment. It will no longer have any effect.
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
## How do I enable "smart" ingredient handling?
|
||||
|
||||
You might have noticed that scaling up a recipe or making a shopping list doesn't by default handle the ingredients in a way you might expect. Depending on your settings, scaling up might yield things like `2 1 cup broth` instead of `2 cup broth`. And making shopping lists from reciepes that have shared ingredients can yield multiple lines of the same ingredient. **But**, mealie has a mechanism to intelligently handle ingredients and make your day better. How?
|
||||
You might have noticed that scaling up a recipe or making a shopping list doesn't by default handle the ingredients in a way you might expect. Depending on your settings, scaling up might yield things like `2 1 cup broth` instead of `2 cup broth`. And, making shopping lists from recipes that have shared ingredients can yield multiple lines of the same ingredient. **But**, Mealie has a mechanism to intelligently handle ingredients and make your day better. How?
|
||||
### Set up your Foods and Units
|
||||
Do the following just **once**. Doing this applies to your whole group, so be careful.
|
||||
|
||||
1. Click on your name in the upper left corner to get to your settings
|
||||
2. In the bottom right, select `Manage Data`
|
||||
3. In the Management page, make sure that a little orange button says `Foods`
|
||||
4. If your Foods database is empty, click `Seed` and choose your language. You should end up with a list of foods. (Wait bit for seeding to happen, and try not to seed more than once or you will have duplicates)
|
||||
4. If your Foods database is empty, click `Seed` and choose your language. You should end up with a list of foods. (Wait a bit for seeding to happen, and try not to seed more than once or you will have duplicates)
|
||||
5. Click the little orange `Foods` button and now choose `Units`.
|
||||
6. Click `Seed` and choose your language. You should end up with a list of units (e.g. `tablespoon`)
|
||||
|
||||
@@ -33,9 +33,9 @@ Do the following for each recipe you want to intelligently handle ingredients.
|
||||
|
||||
Scaling up this recipe or adding it to a Shopping List will now smartly take care of ingredient amounts and duplicate combinations.
|
||||
|
||||
## Is it Safe to Upgrade Mealie?
|
||||
## Is it safe to upgrade Mealie?
|
||||
|
||||
Yes. If you are using the v1 branches (including beta), you can upgrade to the latest version of Mealie without performing a site Export/Restore. This process was required in previous versions of Mealie, however we've automated the database migration process to make it easier to upgrade. Not that if you were using the v0.5.x version, you CANNOT upgrade to the latest version automatically. You must follow the migration instructions in the documentation.
|
||||
Yes. If you are using the v1 branches (including beta), you can upgrade to the latest version of Mealie without performing a site Export/Restore. This process was required in previous versions of Mealie, however we've automated the database migration process to make it easier to upgrade. Note that if you were using the v0.5.x version, you CANNOT upgrade to the latest version automatically. You must follow the migration instructions in the documentation.
|
||||
|
||||
- [Migration From v0.5.x](./migrating-to-mealie-v1.md)
|
||||
|
||||
@@ -45,7 +45,7 @@ You can change the theme by settings the environment variables.
|
||||
|
||||
- [Backend Config - Themeing](./installation/backend-config.md#themeing)
|
||||
|
||||
## How can I change the Login Session Timeout?
|
||||
## How can I change the login session timeout?
|
||||
|
||||
Login session can be configured by setting the `TOKEN_TIME` variable on the backend container.
|
||||
|
||||
@@ -53,7 +53,7 @@ Login session can be configured by setting the `TOKEN_TIME` variable on the back
|
||||
|
||||
## Can I serve Mealie on a subpath?
|
||||
|
||||
No. Due to limitations from the Javascript Framework, mealie doesn't support serving Mealie on a subpath.
|
||||
No. Due to limitations from the JavaScript Framework, Mealie doesn't support serving Mealie on a subpath.
|
||||
|
||||
## Can I install Mealie without docker?
|
||||
|
||||
@@ -98,13 +98,14 @@ python /app/mealie/scripts/change_password.py
|
||||
|
||||
Follow the [steps above](#how-can-i-change-my-password) for changing your password. You will be prompted if you would like to switch your authentication method back to local auth so you can log in again.
|
||||
|
||||
## How do private groups and recipes work?
|
||||
## How do private groups, households, and recipes work?
|
||||
|
||||
Managing private groups and recipes can be confusing. The following diagram and notes should help explain how they work to determine if a recipe can be shared publicly.
|
||||
|
||||
- Private links that are generated from the recipe page using the `Share` button bypass all group and recipe permissions
|
||||
- Private groups block all access to recipes, including those that are public, except as noted above.
|
||||
- Groups with "Allow users outside of your group to see your recipes" disabled block all access to recipes, except as noted above.
|
||||
- Private households, similar to private groups, block all access to recipes, except as noted above.
|
||||
- Households with "Allow users outside of your group to see your recipes" disabled block all access to recipes, except as noted above.
|
||||
- Private recipes block all access to the recipe from public links. This does not affect Private Links.
|
||||
|
||||
```mermaid
|
||||
@@ -112,7 +113,8 @@ stateDiagram-v2
|
||||
r1: Request Access
|
||||
p1: Using Private Link?
|
||||
p2: Is Group Private?
|
||||
p3: Is Recipe Private?
|
||||
p3: Is Household Private?
|
||||
p4: Is Recipe Private?
|
||||
s1: Deny Access
|
||||
n1: Allow Access
|
||||
|
||||
@@ -125,13 +127,16 @@ stateDiagram-v2
|
||||
p2 --> p3: No
|
||||
|
||||
p3 --> s1: Yes
|
||||
p3 --> n1: No
|
||||
p3 --> p4: No
|
||||
|
||||
p4 --> s1: Yes
|
||||
p4 --> n1: No
|
||||
```
|
||||
|
||||
For more information, check out the [Permissions and Public Access guide](./usage/permissions-and-public-access.md).
|
||||
For more information on public access, check out the [Permissions and Public Access guide](./usage/permissions-and-public-access.md). For more information on groups vs. households, check out the [Groups and Households](./features.md#groups-and-households) section in the Features guide.
|
||||
|
||||
## Can I use fail2ban with mealie?
|
||||
Yes, mealie is configured to properly forward external IP addresses into the `mealie.log` logfile. Note that due to restrictions in docker, IP address forwarding only works on Linux.
|
||||
## Can I use fail2ban with Mealie?
|
||||
Yes, Mealie is configured to properly forward external IP addresses into the `mealie.log` logfile. Note that due to restrictions in docker, IP address forwarding only works on Linux.
|
||||
|
||||
Your fail2ban usage should look like the following:
|
||||
```
|
||||
@@ -139,13 +144,21 @@ Use datepattern : %d-%b-%y %H:%M:%S : Day-MON-Year2 24hour:Minute:Second
|
||||
Use failregex line : ^ERROR:\s+Incorrect username or password from <HOST>
|
||||
```
|
||||
|
||||
## Why An API?
|
||||
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based on Meal Plan data to remind you to defrost the chicken, marinade the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
|
||||
## Why an API?
|
||||
An API allows integration into applications like [Home Assistant](https://www.home-assistant.io/) that can act as notification engines to provide custom notifications based on Meal Plan data to remind you to defrost the chicken, marinate the steak, or start the CrockPot. Additionally, you can access nearly any backend service via the API giving you total control to extend the application. To explore the API spin up your server and navigate to http://yourserver.com/docs for interactive API documentation.
|
||||
|
||||
## Why a Database?
|
||||
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project, it is a valid concern to be worried about your data. Mealie specifically addresses this concern by provided automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in control of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
|
||||
## Why a database?
|
||||
Some users of static-site generator applications like ChowDown have expressed concerns about their data being stuck in a database. Considering this is a new project, it is a valid concern to be worried about your data. Mealie specifically addresses this concern by providing automatic daily backups that export your data in json, plain-text markdown files, and/or custom Jinja2 templates. **This puts you in control of how your data is represented** when exported from Mealie, which means you can easily migrate to any other service provided Mealie doesn't work for you.
|
||||
|
||||
As to why we need a database?
|
||||
|
||||
- **Developer Experience:** Without a database, a lot of the work to maintain your data is taken on by the developer instead of a battle-tested platform for storing data.
|
||||
- **Multi User Support:** With a solid database as backend storage for your data, Mealie can better support multi-user sites and avoid read/write access errors when multiple actions are taken at the same time.
|
||||
|
||||
## Why is there no "Keep Screen Alive" button when I access a recipe?
|
||||
You've perhaps visited the Mealie Demo and noticed that it had a "Keep Screen Alive" button, but it doesn't show up in your own Mealie instance.
|
||||
There are typically two possible reasons for this:
|
||||
1. You're accessing your Mealie instance without using HTTPS. The Wake Lock API is only available if HTTPS is used. Read more here: https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API
|
||||
2. You're accessing your Mealie instance on a browser which doesn't support the API. You can test this here: https://vueuse.org/core/useWakeLock/#demo
|
||||
|
||||
Solving the above points will most likely resolve your issues. However, if you're still having problems, you are welcome to create an issue. Just remember to add that you've tried the above two options first in your description.
|
||||
|
||||
@@ -35,7 +35,6 @@ Mealie has a robust and flexible recipe organization system with a few different
|
||||
|
||||
#### Categories
|
||||
|
||||
|
||||
Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, or **Side**.
|
||||
|
||||
[Categories Demo](https://demo.mealie.io/g/home/recipes/categories){ .md-button .md-button--primary }
|
||||
@@ -73,13 +72,13 @@ Mealie uses a calendar like view to help you plan your meals. It shows you the p
|
||||
!!! tip
|
||||
You can also add a "Note" type entry to your meal-plan when you want to include something that might not have a specific recipes. This is great for leftovers, or for ordering out.
|
||||
|
||||
[Mealplanner Demo](https://demo.mealie.io/group/mealplan/planner/view){ .md-button .md-button--primary }
|
||||
[Mealplanner Demo](https://demo.mealie.io/household/mealplan/planner/view){ .md-button .md-button--primary }
|
||||
|
||||
### Planner Rules
|
||||
|
||||
The meal planner has the concept of plan rules. These offer a flexible way to use your organizers to customize how a random recipe is inserted into your meal plan. You can set rules to restrict the pool of recipes based on the Tags and/or Categories of a recipe. Additionally, since meal plans have a Breakfast, Lunch, Dinner, and Snack labels, you can specifically set a rule to be active for a **specific meal type** or even a **specific day of the week.**
|
||||
|
||||
[Planner Settings Demo](https://demo.mealie.io/group/mealplan/settings){ .md-button .md-button--primary }
|
||||
[Planner Settings Demo](https://demo.mealie.io/household/mealplan/settings){ .md-button .md-button--primary }
|
||||
|
||||
## Shopping Lists
|
||||
|
||||
@@ -105,22 +104,22 @@ Notifiers use the [Apprise library](https://github.com/caronc/apprise/wiki), whi
|
||||
- `json` and `jsons`
|
||||
- `xml` and `xmls`
|
||||
|
||||
[Notifiers Demo](https://demo.mealie.io/group/notifiers){ .md-button .md-button--primary }
|
||||
[Notifiers Demo](https://demo.mealie.io/household/notifiers){ .md-button .md-button--primary }
|
||||
|
||||
### Webhooks
|
||||
|
||||
Unlike notifiers, which are event-driven notifications, Webhooks allow you to send scheduled notifications to your desired endpoint. Webhooks are sent on the day of a scheduled mealplan, at the specified time, and contain the mealplan data in the request.
|
||||
|
||||
[Webhooks Demo](https://demo.mealie.io/group/webhooks){ .md-button .md-button--primary }
|
||||
[Webhooks Demo](https://demo.mealie.io/household/webhooks){ .md-button .md-button--primary }
|
||||
|
||||
### Recipe Actions
|
||||
|
||||
Recipe Actions are custom actions you can add to all recipes in Mealie. This is a great way to add custom integrations that are fired manually. There are two types of recipe actions:
|
||||
|
||||
1. link - these actions will take you directly to an external page
|
||||
1. link - these actions will take you directly to an external page. Merge fields can be used to customize the URL for each recipe
|
||||
2. post - these actions will send a `POST` request to the specified URL, with the recipe JSON in the request body. These can be used, for instance, to manually trigger a webhook in Home Assistant
|
||||
|
||||
Recipe Action URLs can include merge fields to inject the current recipe's data. For instance, you can use the following URL to include a Google search with the recipe's slug:
|
||||
When using the "link" action type, Recipe Action URLs can include merge fields to inject the current recipe's data. For instance, you can use the following URL to include a Google search with the recipe's slug:
|
||||
```
|
||||
https://www.google.com/search?q=${slug}
|
||||
```
|
||||
@@ -164,6 +163,46 @@ Managing a robust collection of recipes inevitable requires a lot of data. Meali
|
||||
|
||||
[Data Management Demo](https://demo.mealie.io/group/data/foods){ .md-button .md-button--primary }
|
||||
|
||||
## Groups and Households
|
||||
|
||||
Mealie lets you fully customize how you organize your users. You can use Groups to host multiple instances (or tenants) of Mealie which are completely isolated from each other. Within each Group you can organize users into Households which allow users to share recipes, but keep other items separate (e.g. meal plans and shopping lists).
|
||||
|
||||
### Groups
|
||||
|
||||
Groups are fully isolated instances of Mealie. Think of a goup as a completely separate, fully self-contained site. There is no data shared between groups. Each group has its own users, recipes, tags, categories, etc. A user logged-in to one group cannot make any changes to another.
|
||||
|
||||
Common use cases for groups include:
|
||||
- Hosting multiple instances of Mealie for others who want to keep their data private and secure
|
||||
- Creating completely isolated recipe pools
|
||||
|
||||
### Households
|
||||
|
||||
Households are subdivisions within a single Group. Households maintain their own users and settings, while sharing their recipes with other households. Households also share organizers (tags, categories, etc.) with the entire group. Meal Plans, Shopping Lists, and Integrations are only accessible within a household.
|
||||
|
||||
Common use cases for households include:
|
||||
- Sharing a common recipe pool amongst families
|
||||
- Maintaining separate meal plans and shopping lists from other households
|
||||
- Maintaining separate integrations and customizations from other households
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
mealie[(Mealie)] ==> groups
|
||||
|
||||
%% Groups
|
||||
groups((Groups)) --> ingredients & organizers
|
||||
groups((Groups)) ====> households
|
||||
ingredients("Ingredients<br/>(Foods, Units, Labels)")
|
||||
organizers("Organizers<br/>(Categories, Tags, Tools)")
|
||||
|
||||
%% Households
|
||||
households((Households)) --> recipes & mealplans & shoppinglists & integrations
|
||||
|
||||
recipes(Recipes & Cookbooks)
|
||||
mealplans(Meal Plans)
|
||||
shoppinglists(Shopping Lists)
|
||||
integrations("Integrations<br/>(Notifiers, Webhooks)")
|
||||
```
|
||||
|
||||
## Server Administration
|
||||
|
||||
### Site Settings
|
||||
@@ -172,11 +211,13 @@ The site settings page contains general information about your installation like
|
||||
|
||||
[Settings Demo](https://demo.mealie.io/admin/site-settings){ .md-button .md-button--primary }
|
||||
|
||||
### Users and Group
|
||||
### Users, Households, and Groups
|
||||
|
||||
There is a small management area for users and groups that allows you to create, edit, and delete users and groups.
|
||||
There is a small management area for users, households, and groups.
|
||||
|
||||
[Users Demo](https://demo.mealie.io/admin/manage/users){ .md-button .md-button--primary }
|
||||
[Households Demo](https://demo.mealie.io/admin/manage/households){ .md-button .md-button--primary }
|
||||
[Groups Demo](https://demo.mealie.io/admin/manage/groups){ .md-button .md-button--primary }
|
||||
|
||||
### Backups
|
||||
|
||||
|
||||
@@ -4,22 +4,22 @@
|
||||
|
||||
### General
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ----------------------------- | :-------------------: | ----------------------------------------------------------------------------------- |
|
||||
| PUID | 911 | UserID permissions between host OS and container |
|
||||
| PGID | 911 | GroupID permissions between host OS and container |
|
||||
| DEFAULT_GROUP | Home | The default group for users |
|
||||
| BASE_URL | http://localhost:8080 | Used for Notifications |
|
||||
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid |
|
||||
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
|
||||
| API_DOCS | True | Turns on/off access to the API documentation locally. |
|
||||
| TZ | UTC | Must be set to get correct date/time on the server |
|
||||
| ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token |
|
||||
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
|
||||
| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug, trace) |
|
||||
| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run the daily tasks. |
|
||||
| Variables | Default | Description |
|
||||
| ----------------------------- | :-------------------: | --------------------------------------------------------------------------------------------------------- |
|
||||
| PUID | 911 | UserID permissions between host OS and container |
|
||||
| PGID | 911 | GroupID permissions between host OS and container |
|
||||
| DEFAULT_GROUP | Home | The default group for users |
|
||||
| BASE_URL | http://localhost:8080 | Used for Notifications |
|
||||
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid |
|
||||
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
|
||||
| API_DOCS | True | Turns on/off access to the API documentation locally |
|
||||
| TZ | UTC | Must be set to get correct date/time on the server |
|
||||
| ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token |
|
||||
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
|
||||
| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug) |
|
||||
| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run daily server tasks, in HH:MM format. Use the server's local time, *not* UTC |
|
||||
|
||||
<super>\*</super> Starting in v1.4.0 this was changed to default to `false` as apart of a security review of the application.
|
||||
<super>\*</super> Starting in v1.4.0 this was changed to default to `false` as part of a security review of the application.
|
||||
|
||||
### Security
|
||||
|
||||
@@ -58,7 +58,7 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
|
||||
|
||||
| Variables | Default | Description |
|
||||
| --------------- | :-----: | ----------------------------------------------------------------------------- |
|
||||
| UVICORN_WORKERS | 1 | Sets the number of works for the web server [more info here][unicorn_workers] |
|
||||
| UVICORN_WORKERS | 1 | Sets the number of workers for the web server. [More info here][unicorn_workers] |
|
||||
|
||||
### LDAP
|
||||
|
||||
@@ -82,7 +82,7 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
|
||||
|
||||
:octicons-tag-24: v1.4.0
|
||||
|
||||
For usage, see [Usage - OpenID Connect](../authentication/oidc.md)
|
||||
For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ---------------------- | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
@@ -90,12 +90,12 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc.md)
|
||||
| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC |
|
||||
| OIDC_CONFIGURATION_URL | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration |
|
||||
| OIDC_CLIENT_ID | None | The client id of your configured client in your provider |
|
||||
| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate, regardless of the `OIDC_ADMIN_GROUP`. For more information see [this page](../authentication/oidc.md#groups) |
|
||||
| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be made an admin. For more information see [this page](../authentication/oidc.md#groups) |
|
||||
| OIDC_CLIENT_SECRET <br/> :octicons-tag-24: v2.0.0 | None | The client secret of your configured client in your provider|
|
||||
| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate. For more information see [this page](../authentication/oidc-v2.md#groups) |
|
||||
| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be able to successfully authenticate *and* be made an admin. For more information see [this page](../authentication/oidc-v2.md#groups) |
|
||||
| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed an you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL |
|
||||
| OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with <OIDC_PROVIDER_NAME\>" |
|
||||
| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked |
|
||||
| OIDC_SIGNING_ALGORITHM | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||
| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") |
|
||||
| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** |
|
||||
| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
||||
@@ -104,18 +104,22 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc.md)
|
||||
|
||||
:octicons-tag-24: v1.7.0
|
||||
|
||||
Mealie supports various integrations using OpenAI. To enable OpenAI, [you must provide your OpenAI API key](https://platform.openai.com/api-keys). You can tweak how OpenAI is used using these backend settings. Please note that while OpenAI usage is optimized to reduce API costs, you're unlikely to be able to use OpenAI features with the free tier limits.
|
||||
Mealie supports various integrations using OpenAI. For more information, check out our [OpenAI documentation](./open-ai.md).
|
||||
For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values as JSON encoded strings (e.g. `OPENAI_CUSTOM_PARAMS='{"k1": "v1", "k2": "v2"}'`)
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ------------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| OPENAI_BASE_URL | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
|
||||
| OPENAI_API_KEY | None | Your OpenAI API Key. Enables OpenAI-related features |
|
||||
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
|
||||
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
|
||||
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
|
||||
| OPENAI_REQUEST_TIMEOUT | 10 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
|
||||
| Variables | Default | Description |
|
||||
| ---------------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| OPENAI_BASE_URL | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
|
||||
| OPENAI_API_KEY | None | Your OpenAI API Key. Enables OpenAI-related features |
|
||||
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
|
||||
| OPENAI_CUSTOM_HEADERS | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
|
||||
| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
|
||||
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
|
||||
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
|
||||
| OPENAI_REQUEST_TIMEOUT | 60 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
|
||||
|
||||
### Themeing
|
||||
### Theming
|
||||
|
||||
Setting the following environmental variables will change the theme of the frontend. Note that the themes are the same for all users. This is a break-change when migration from v0.x.x -> 1.x.x.
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
|
||||
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
|
||||
|
||||
1. Take a backup just in case!
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v1.0.0-RC1.1`
|
||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v1.12.0`
|
||||
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
|
||||
4. Restart the container
|
||||
|
||||
@@ -58,7 +58,7 @@ The following steps were tested on a Ubuntu 20.04 server, but should work for mo
|
||||
4. Create a docker-compose.yaml file in the mealie directory: `touch docker-compose.yaml`
|
||||
5. Use the text editor of your choice to edit the file and copy the contents of the docker-compose template for the deployment type you want to use: `nano docker-compose.yaml` or `vi docker-compose.yaml`
|
||||
|
||||
## Step 2: Customizing The `docker-compose.yaml` files.
|
||||
## Step 3: Customizing The `docker-compose.yaml` files.
|
||||
|
||||
After you've decided setup the files it's important to set a few ENV variables to ensure that you can use all the features of Mealie. I recommend that you verify and check that:
|
||||
|
||||
@@ -67,7 +67,7 @@ After you've decided setup the files it's important to set a few ENV variables t
|
||||
- [x] You've set the [`BASE_URL`](./backend-config.md#general) variable.
|
||||
- [x] You've set the `DEFAULT_EMAIL` and `DEFAULT_GROUP` variable.
|
||||
|
||||
## Step 3: Startup
|
||||
## Step 4: Startup
|
||||
|
||||
After you've configured your database and updated the `docker-compose.yaml` files, you can start Mealie by running the following command in the directory where you've added your `docker-compose.yaml`.
|
||||
|
||||
@@ -87,11 +87,11 @@ You should see the containers start up without error. You should now be able to
|
||||
|
||||
**Password:** MyPassword
|
||||
|
||||
## Step 4: Validate Installation
|
||||
## Step 5: Validate Installation
|
||||
|
||||
After the startup is complete, you should see a login screen. Use the default credentials above to log in and navigate to `/admin/site-settings`. Here, you'll find a summary of your configuration details and their respective status. Before proceeding, you should validate that the configuration is correct. For any warnings or errors the page will display an error and notify you of what you need to verify.
|
||||
|
||||
## Step 5: Backup
|
||||
## Step 6: Backup
|
||||
|
||||
While v1.0.0 is a great step to data-stability and security, it's not a backup. Mealie provides a full site data backup mechanism through the UI.
|
||||
|
||||
@@ -101,7 +101,7 @@ These backups are just plain .zip files that you can download from the UI or acc
|
||||
|
||||
### Docker Tags
|
||||
|
||||
See all available tags on [GitHub](https://github.com/mealie-recipes/mealie/pkgs/container/mealie). We do not currently publish new images to Dockerhub.
|
||||
See all available tags on [GitHub](https://github.com/mealie-recipes/mealie/pkgs/container/mealie).
|
||||
|
||||
`ghcr.io/mealie-recipes/mealie:nightly`
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# OpenAI Integration
|
||||
|
||||
:octicons-tag-24: v1.7.0
|
||||
|
||||
Mealie's OpenAI integration enables several features and enhancements throughout the application. To enable OpenAI features, you must have an account with OpenAI and configure Mealie to use the OpenAI API key (for more information, check out the [backend configuration](./backend-config.md#openai)).
|
||||
|
||||
## Configuration
|
||||
|
||||
For most users, supplying the OpenAI API key is all you need to do; you will use the regular OpenAI service with the default language model. Note that while OpenAI has a free tier, it's not sufficiently capable for Mealie (or most other production use cases). For more information, check out [OpenAI's rate limits](https://platform.openai.com/docs/guides/rate-limits). If you deposit $5 into your OpenAI account, you will be permanently bumped up to Tier 1, which is sufficient for Mealie. Cost per-request is dependant on many factors, but Mealie tries to keep token counts conservative.
|
||||
|
||||
Alternatively, if you have another service you'd like to use in-place of OpenAI, you can configure Mealie to use that instead, as long as it has an OpenAI-compatible API. For instance, a common self-hosted alternative to OpenAI is [Ollama](https://ollama.com/). To use Ollama with Mealie, change your `OPENAI_BASE_URL` to `http://localhost:11434/v1` (where `http://localhost:11434` is wherever you're hosting Ollama, and `/v1` enables the OpenAI-compatible endpoints). Note that you *must* provide an API key, even though it is ultimately ignored by Ollama.
|
||||
|
||||
If you wish to disable image recognition features (to save costs, or because your custom model doesn't support them) you can set `OPENAI_ENABLE_IMAGE_SERVICES` to `False`. For more information on what configuration options are available, check out the [backend configuration](./backend-config.md#openai).
|
||||
|
||||
## OpenAI Features
|
||||
- The OpenAI Ingredient Parser can be used as an alternative to the NLP and Brute Force parsers. Simply choose the OpenAI parser while parsing ingredients (:octicons-tag-24: v1.7.0)
|
||||
- When importing a recipe via URL, if the default recipe scraper is unable to read the recipe data from a webpage, the webpage contents will be parsed by OpenAI (:octicons-tag-24: v1.9.0)
|
||||
- You can import an image of a written recipe, which is sent to OpenAI and imported into Mealie. The recipe can be hand-written or typed, as long as the text is in the picture. You can also optionally have OpenAI translate the recipe into your own language (:octicons-tag-24: v1.12.0)
|
||||
@@ -7,7 +7,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.10.1 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.12.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
- mealie-data:/app/data/
|
||||
environment:
|
||||
# Set Backend ENV Variables Here
|
||||
ALLOW_SIGNUP: true
|
||||
ALLOW_SIGNUP: "false"
|
||||
PUID: 1000
|
||||
PGID: 1000
|
||||
TZ: America/Anchorage
|
||||
|
||||
@@ -18,7 +18,7 @@ Use your best judgement when deciding what to do.
|
||||
|
||||
By default, the API is **not** rate limited. This leaves Mealie open to a potential **Denial of Service Attack**. While it's possible to perform a **Denial of Service Attack** on any endpoint, there are a few key endpoints that are more vulnerable than others.
|
||||
|
||||
- `/api/recipes/create-url`
|
||||
- `/api/recipes/create/url`
|
||||
- `/api/recipes/{id}/image`
|
||||
|
||||
These endpoints are used to scrape data based off a user provided URL. It is possible for a malicious user to issue multiple requests to download an arbitrarily large external file (e.g a Debian ISO) and sufficiently saturate a CPU assigned to the container. While we do implement some protections against this by chunking the response, and using a timeout strategy, it's still possible to overload the CPU if an attacker issues multiple requests concurrently.
|
||||
@@ -33,7 +33,7 @@ If you'd like to mitigate this risk, we suggest that you rate limit the API in g
|
||||
|
||||
## Server Side Request Forgery
|
||||
|
||||
- `/api/recipes/create-url`
|
||||
- `/api/recipes/create/url`
|
||||
- `/api/recipes/{id}/image`
|
||||
|
||||
Given the nature of these APIs it's possible to perform a **Server Side Request Forgery** attack. This is where a malicious user can issue a request to an internal network resource, and potentially exfiltrate data. We _do_ perform some checks to mitigate access to resources within your network but at the end of the day, users of Mealie are allowed to trigger HTTP requests on **your server**.
|
||||
|
||||
@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.10.1 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v1.12.0 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
- mealie-data:/app/data/
|
||||
environment:
|
||||
# Set Backend ENV Variables Here
|
||||
ALLOW_SIGNUP: true
|
||||
ALLOW_SIGNUP: "false"
|
||||
PUID: 1000
|
||||
PGID: 1000
|
||||
TZ: America/Anchorage
|
||||
|
||||
@@ -11,7 +11,8 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
|
||||
- 🕸 Import recipes from around the web by URL
|
||||
- 📱 Progressive Web App
|
||||
- 📆 Create Meal Plans
|
||||
- 🛒 Generate shopping lists
|
||||
- 🛒 Generate Shopping Lists
|
||||
- 🏠 Separate Users into Households and share Recipes
|
||||
- 🐳 Easy setup with Docker
|
||||
- 🎨 Customize your interface with color themed layouts
|
||||
- 🌍 localized in many languages
|
||||
@@ -27,7 +28,7 @@ Mealie is a self hosted recipe manager and meal planner with a RestAPI backend a
|
||||
- Copy Me That
|
||||
- Paprika
|
||||
- Tandoor Recipes
|
||||
- Random meal plan generation
|
||||
- Random Meal Plan generation
|
||||
- Advanced rule configuration to fine tune random recipes
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
- Create a Backup and Download from the UI
|
||||
- Upgrade
|
||||
|
||||
## Upgrading to Mealie v1
|
||||
If you are upgrading from pre-v1.0.0 to v1.0.0, make sure you read [Migrating to Mealie v1](./migrating-to-mealie-v1.md)!
|
||||
## Upgrading to Mealie v1 or later
|
||||
If you are upgrading from pre-v1.0.0 to v1.0.0 or later (v2.0.0, etc.), make sure you read [Migrating to Mealie v1](./migrating-to-mealie-v1.md)!
|
||||
|
||||
## Backing Up Your Data
|
||||
|
||||
[See Backups and Restore Section](../getting-started/usage/backups-and-restoring.md) for details on backing up your data
|
||||
|
||||
## Docker
|
||||
For all setups using Docker the updating process looks something like this
|
||||
For all setups using Docker, the updating process looks something like this:
|
||||
|
||||
- Stop the container using `docker compose down`
|
||||
- If you are not using the latest tag, change the version (image tag) in your docker-compose file
|
||||
|
||||
@@ -19,9 +19,10 @@ Administrators can navigate to the Settings page and access the User Management
|
||||
|
||||
## Public Recipe Access
|
||||
|
||||
By default, groups are set to private, meaning only logged-in users may access the group. In order for a recipe to be viewable by public (not logged-in) users, two criteria must be met:
|
||||
By default, groups and households are set to private, meaning only logged-in users may access the group/household. In order for a recipe to be viewable by public (not logged-in) users, three criteria must be met:
|
||||
|
||||
1. The group must not be private, *and* the group setting for allowing users outside of your group to see your recipes must be enabled. These can be toggled on the Group Settings page
|
||||
1. The group must not be private
|
||||
2. The household must not be private, *and* the household setting for allowing users outside of your group to see your recipes must be enabled. These can be toggled on the Household Settings page
|
||||
2. The recipe must be set to public. This can be toggled for each recipe individually, or in bulk using the Recipe Data Management page
|
||||
|
||||
Additionally, if the group is not private, public users can view all public group data (public recipes, public cookbooks, etc.) from the home page ([e.g. the demo home page](https://demo.mealie.io/g/home)).
|
||||
@@ -32,7 +33,8 @@ More broadly, here are the rules for how recipe access is determined:
|
||||
|
||||
- Private links that are generated from the recipe page using the `Share` button bypass all group and recipe permissions
|
||||
- Private groups block all access to recipes, including those that are public, except as noted above.
|
||||
- Groups with "Allow users outside of your group to see your recipes" disabled block all access to recipes, except as noted above.
|
||||
- Private households, similar to private groups, block all access to recipes, except as noted above.
|
||||
- Households with "Allow users outside of your group to see your recipes" disabled block all access to recipes, except as noted above.
|
||||
- Private recipes block all access to the recipe from public links. This does not affect Private Links.
|
||||
|
||||
```mermaid
|
||||
@@ -40,7 +42,8 @@ stateDiagram-v2
|
||||
r1: Request Access
|
||||
p1: Using Private Link?
|
||||
p2: Is Group Private?
|
||||
p3: Is Recipe Private?
|
||||
p3: Is Household Private?
|
||||
p4: Is Recipe Private?
|
||||
s1: Deny Access
|
||||
n1: Allow Access
|
||||
|
||||
@@ -53,5 +56,8 @@ stateDiagram-v2
|
||||
p2 --> p3: No
|
||||
|
||||
p3 --> s1: Yes
|
||||
p3 --> n1: No
|
||||
p3 --> p4: No
|
||||
|
||||
p4 --> s1: Yes
|
||||
p4 --> n1: No
|
||||
```
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -74,13 +74,14 @@ nav:
|
||||
- Backend Configuration: "documentation/getting-started/installation/backend-config.md"
|
||||
- Security: "documentation/getting-started/installation/security.md"
|
||||
- Logs: "documentation/getting-started/installation/logs.md"
|
||||
- OpenAI: "documentation/getting-started/installation/open-ai.md"
|
||||
- Usage:
|
||||
- Backup and Restoring: "documentation/getting-started/usage/backups-and-restoring.md"
|
||||
- Permissions and Public Access: "documentation/getting-started/usage/permissions-and-public-access.md"
|
||||
|
||||
- Authentication:
|
||||
- LDAP: "documentation/getting-started/authentication/ldap.md"
|
||||
- OpenID Connect: "documentation/getting-started/authentication/oidc.md"
|
||||
- OpenID Connect: "documentation/getting-started/authentication/oidc-v2.md"
|
||||
|
||||
- Community Guides:
|
||||
- iOS Shortcuts: "documentation/community-guide/ios.md"
|
||||
@@ -100,5 +101,6 @@ nav:
|
||||
- Dev Getting Started: "contributors/developers-guide/starting-dev-server.md"
|
||||
- Database Changes: "contributors/developers-guide/database-changes.md"
|
||||
- Maintainers Guide: "contributors/developers-guide/maintainers.md"
|
||||
- Migration Guide: "contributors/developers-guide/migration-guide.md"
|
||||
- Guides:
|
||||
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-text v-if="cookbook">
|
||||
<v-card-text v-if="cookbook" class="px-1">
|
||||
<v-text-field v-model="cookbook.name" :label="$t('cookbook.cookbook-name')"></v-text-field>
|
||||
<v-textarea v-model="cookbook.description" auto-grow :rows="2" :label="$t('recipe.description')"></v-textarea>
|
||||
<RecipeOrganizerSelector v-model="cookbook.categories" selector-type="categories" />
|
||||
<RecipeOrganizerSelector v-model="cookbook.tags" selector-type="tags" />
|
||||
<RecipeOrganizerSelector v-model="cookbook.tools" selector-type="tools" />
|
||||
<QueryFilterBuilder
|
||||
:field-defs="fieldDefs"
|
||||
:initial-query-filter="cookbook.queryFilter"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<v-switch v-model="cookbook.public" hide-details single-line>
|
||||
<template #label>
|
||||
{{ $t('cookbook.public-cookbook') }}
|
||||
@@ -14,33 +16,19 @@
|
||||
</HelpIcon>
|
||||
</template>
|
||||
</v-switch>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-subtitle-1 d-flex align-center mb-0 pb-0">
|
||||
{{ $t('cookbook.filter-options') }}
|
||||
<HelpIcon right small class="ml-2">
|
||||
{{ $t('cookbook.filter-options-description') }}
|
||||
</HelpIcon>
|
||||
</h3>
|
||||
<v-switch v-model="cookbook.requireAllCategories" class="mt-0" hide-details single-line>
|
||||
<template #label> {{ $t('cookbook.require-all-categories') }} </template>
|
||||
</v-switch>
|
||||
<v-switch v-model="cookbook.requireAllTags" hide-details single-line>
|
||||
<template #label> {{ $t('cookbook.require-all-tags') }} </template>
|
||||
</v-switch>
|
||||
<v-switch v-model="cookbook.requireAllTools" hide-details single-line>
|
||||
<template #label> {{ $t('cookbook.require-all-tools') }} </template>
|
||||
</v-switch>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||
import { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeOrganizerSelector },
|
||||
components: { QueryFilterBuilder },
|
||||
props: {
|
||||
cookbook: {
|
||||
type: Object as () => ReadCookBook,
|
||||
@@ -51,5 +39,50 @@ export default defineComponent({
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { i18n } = useContext();
|
||||
|
||||
function handleInput(value: string | undefined) {
|
||||
props.cookbook.queryFilterString = value || "";
|
||||
}
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.tc("category.categories"),
|
||||
type: Organizer.Category,
|
||||
},
|
||||
{
|
||||
name: "tags.id",
|
||||
label: i18n.tc("tag.tags"),
|
||||
type: Organizer.Tag,
|
||||
},
|
||||
{
|
||||
name: "tools.id",
|
||||
label: i18n.tc("tool.tools"),
|
||||
type: Organizer.Tool,
|
||||
},
|
||||
{
|
||||
name: "household_id",
|
||||
label: i18n.tc("household.households"),
|
||||
type: Organizer.Household,
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
label: i18n.tc("general.date-created"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
label: i18n.tc("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
handleInput,
|
||||
fieldDefs,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
<BaseDialog
|
||||
v-if="editTarget"
|
||||
v-model="dialogStates.edit"
|
||||
:width="650"
|
||||
width="100%"
|
||||
max-width="1100px"
|
||||
:icon="$globals.icons.pages"
|
||||
:title="$t('general.edit')"
|
||||
:submit-icon="$globals.icons.save"
|
||||
:submit-text="$tc('general.save')"
|
||||
:submit-disabled="!editTarget.queryFilterString"
|
||||
@submit="editCookbook"
|
||||
>
|
||||
<v-card-text>
|
||||
|
||||
@@ -2,30 +2,11 @@
|
||||
<div v-if="preferences">
|
||||
<BaseCardSectionTitle :title="$tc('group.general-preferences')"></BaseCardSectionTitle>
|
||||
<v-checkbox v-model="preferences.privateGroup" class="mt-n4" :label="$t('group.private-group')"></v-checkbox>
|
||||
<v-select
|
||||
v-model="preferences.firstDayOfWeek"
|
||||
:prepend-icon="$globals.icons.calendarWeekBegin"
|
||||
:items="allDays"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
:label="$t('settings.first-day-of-week')"
|
||||
/>
|
||||
|
||||
<BaseCardSectionTitle class="mt-5" :title="$tc('group.group-recipe-preferences')"></BaseCardSectionTitle>
|
||||
<template v-for="(_, key) in preferences">
|
||||
<v-checkbox
|
||||
v-if="labels[key]"
|
||||
:key="key"
|
||||
v-model="preferences[key]"
|
||||
class="mt-n4"
|
||||
:label="labels[key]"
|
||||
></v-checkbox>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, computed } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -35,48 +16,6 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
|
||||
const labels = {
|
||||
recipePublic: i18n.tc("group.allow-users-outside-of-your-group-to-see-your-recipes"),
|
||||
recipeShowNutrition: i18n.tc("group.show-nutrition-information"),
|
||||
recipeShowAssets: i18n.tc("group.show-recipe-assets"),
|
||||
recipeLandscapeView: i18n.tc("group.default-to-landscape-view"),
|
||||
recipeDisableComments: i18n.tc("group.disable-users-from-commenting-on-recipes"),
|
||||
recipeDisableAmount: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food"),
|
||||
};
|
||||
|
||||
const allDays = [
|
||||
{
|
||||
name: i18n.t("general.sunday"),
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.monday"),
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.tuesday"),
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.wednesday"),
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.thursday"),
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.friday"),
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.saturday"),
|
||||
value: 6,
|
||||
},
|
||||
];
|
||||
|
||||
const preferences = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
@@ -87,8 +26,6 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
return {
|
||||
allDays,
|
||||
labels,
|
||||
preferences,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<v-select
|
||||
v-model="selected"
|
||||
:items="households"
|
||||
:label="label"
|
||||
:hint="description"
|
||||
:persistent-hint="!!description"
|
||||
item-text="name"
|
||||
:multiple="multiselect"
|
||||
:prepend-inner-icon="$globals.icons.household"
|
||||
return-object
|
||||
>
|
||||
<template #selection="data">
|
||||
<v-chip
|
||||
:key="data.index"
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
small
|
||||
close
|
||||
label
|
||||
color="accent"
|
||||
dark
|
||||
@click:close="removeByIndex(data.index)"
|
||||
>
|
||||
{{ data.item.name || data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, useContext } from "@nuxtjs/composition-api";
|
||||
import { useHouseholdStore } from "~/composables/store/use-household-store";
|
||||
|
||||
interface HouseholdLike {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Array as () => HouseholdLike[],
|
||||
required: true,
|
||||
},
|
||||
multiselect: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const selected = computed({
|
||||
get: () => props.value,
|
||||
set: (val) => {
|
||||
context.emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (selected.value === undefined) {
|
||||
selected.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
const { i18n } = useContext();
|
||||
const label = computed(
|
||||
() => props.multiselect ? i18n.tc("household.households") : i18n.tc("household.household")
|
||||
);
|
||||
|
||||
const { store: households } = useHouseholdStore();
|
||||
function removeByIndex(index: number) {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||
selected.value = [...newSelected];
|
||||
}
|
||||
|
||||
return {
|
||||
selected,
|
||||
label,
|
||||
households,
|
||||
removeByIndex,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -39,7 +39,7 @@
|
||||
import { computed, defineComponent, reactive, ref, toRefs, useContext } from "@nuxtjs/composition-api";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
|
||||
import { ShoppingListSummary } from "~/lib/api/types/group";
|
||||
import { ShoppingListSummary } from "~/lib/api/types/household";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
export interface ContextMenuItem {
|
||||
@@ -5,8 +5,13 @@
|
||||
<v-select v-model="inputEntryType" :items="MEAL_TYPE_OPTIONS" :label="$t('meal-plan.meal-type')"></v-select>
|
||||
</div>
|
||||
|
||||
<RecipeOrganizerSelector v-model="inputCategories" selector-type="categories" />
|
||||
<RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
|
||||
<div class="mb-5">
|
||||
<QueryFilterBuilder
|
||||
:field-defs="fieldDefs"
|
||||
:initial-query-filter="queryFilter"
|
||||
@input="handleQueryFilterInput"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- TODO: proper pluralization of inputDay -->
|
||||
{{ $t('meal-plan.this-rule-will-apply', {
|
||||
@@ -18,12 +23,14 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { RecipeTag, RecipeCategory } from "~/lib/api/types/recipe";
|
||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||
import { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import { QueryFilterJSON } from "~/lib/api/types/response";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeOrganizerSelector,
|
||||
QueryFilterBuilder,
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
@@ -34,13 +41,13 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: "unset",
|
||||
},
|
||||
categories: {
|
||||
type: Array as () => RecipeCategory[],
|
||||
default: () => [],
|
||||
queryFilterString: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
tags: {
|
||||
type: Array as () => RecipeTag[],
|
||||
default: () => [],
|
||||
queryFilter: {
|
||||
type: Object as () => QueryFilterJSON,
|
||||
default: null,
|
||||
},
|
||||
showHelp: {
|
||||
type: Boolean,
|
||||
@@ -87,31 +94,65 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
const inputCategories = computed({
|
||||
const inputQueryFilterString = computed({
|
||||
get: () => {
|
||||
return props.categories;
|
||||
return props.queryFilterString;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:categories", val);
|
||||
context.emit("update:query-filter-string", val);
|
||||
},
|
||||
});
|
||||
|
||||
const inputTags = computed({
|
||||
get: () => {
|
||||
return props.tags;
|
||||
function handleQueryFilterInput(value: string | undefined) {
|
||||
inputQueryFilterString.value = value || "";
|
||||
};
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.tc("category.categories"),
|
||||
type: Organizer.Category,
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:tags", val);
|
||||
{
|
||||
name: "tags.id",
|
||||
label: i18n.tc("tag.tags"),
|
||||
type: Organizer.Tag,
|
||||
},
|
||||
});
|
||||
{
|
||||
name: "tools.id",
|
||||
label: i18n.tc("tool.tools"),
|
||||
type: Organizer.Tool,
|
||||
},
|
||||
{
|
||||
name: "household_id",
|
||||
label: i18n.tc("household.households"),
|
||||
type: Organizer.Household,
|
||||
},
|
||||
{
|
||||
name: "last_made",
|
||||
label: i18n.tc("general.last-made"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
label: i18n.tc("general.date-created"),
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
label: i18n.tc("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
MEAL_TYPE_OPTIONS,
|
||||
MEAL_DAY_OPTIONS,
|
||||
inputDay,
|
||||
inputEntryType,
|
||||
inputCategories,
|
||||
inputTags,
|
||||
inputQueryFilterString,
|
||||
handleQueryFilterInput,
|
||||
fieldDefs,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -18,8 +18,6 @@
|
||||
icon: $globals.icons.testTube,
|
||||
text: $tc('general.test'),
|
||||
event: 'test',
|
||||
// TODO: There is no functionality hooked up to this. Enable it when there is
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.save,
|
||||
@@ -37,7 +35,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
|
||||
import { ReadWebhook } from "~/lib/api/types/group";
|
||||
import { ReadWebhook } from "~/lib/api/types/household";
|
||||
import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
|
||||
|
||||
export default defineComponent({
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div v-if="preferences">
|
||||
<BaseCardSectionTitle class="mt-10" :title="$tc('household.household-preferences')"></BaseCardSectionTitle>
|
||||
<div class="mb-6">
|
||||
<v-checkbox
|
||||
v-model="preferences.privateHousehold"
|
||||
hide-details
|
||||
dense
|
||||
:label="$t('household.private-household')"
|
||||
/>
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("household.private-household-description") }}
|
||||
</p>
|
||||
<DocLink class="mt-2" link="/documentation/getting-started/faq/#how-do-private-groups-and-recipes-work" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<v-checkbox
|
||||
v-model="preferences.lockRecipeEditsFromOtherHouseholds"
|
||||
hide-details
|
||||
dense
|
||||
:label="$t('household.lock-recipe-edits-from-other-households')"
|
||||
/>
|
||||
<div class="ml-8">
|
||||
<p class="text-subtitle-2 my-0 py-0">
|
||||
{{ $t("household.lock-recipe-edits-from-other-households-description") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<v-select
|
||||
v-model="preferences.firstDayOfWeek"
|
||||
:prepend-icon="$globals.icons.calendarWeekBegin"
|
||||
:items="allDays"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
:label="$t('settings.first-day-of-week')"
|
||||
/>
|
||||
|
||||
<BaseCardSectionTitle class="mt-5" :title="$tc('household.household-recipe-preferences')"></BaseCardSectionTitle>
|
||||
<div class="preference-container">
|
||||
<div v-for="p in recipePreferences" :key="p.key">
|
||||
<v-checkbox
|
||||
v-model="preferences[p.key]"
|
||||
hide-details
|
||||
dense
|
||||
:label="p.label"
|
||||
/>
|
||||
<p class="ml-8 text-subtitle-2 my-0 py-0">
|
||||
{{ p.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
|
||||
import { ReadHouseholdPreferences } from "~/lib/api/types/household";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { i18n } = useContext();
|
||||
|
||||
type Preference = {
|
||||
key: keyof ReadHouseholdPreferences;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const recipePreferences: Preference[] = [
|
||||
{
|
||||
key: "recipePublic",
|
||||
label: i18n.tc("group.allow-users-outside-of-your-group-to-see-your-recipes"),
|
||||
description: i18n.tc("group.allow-users-outside-of-your-group-to-see-your-recipes-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeShowNutrition",
|
||||
label: i18n.tc("group.show-nutrition-information"),
|
||||
description: i18n.tc("group.show-nutrition-information-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeShowAssets",
|
||||
label: i18n.tc("group.show-recipe-assets"),
|
||||
description: i18n.tc("group.show-recipe-assets-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeLandscapeView",
|
||||
label: i18n.tc("group.default-to-landscape-view"),
|
||||
description: i18n.tc("group.default-to-landscape-view-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeDisableComments",
|
||||
label: i18n.tc("group.disable-users-from-commenting-on-recipes"),
|
||||
description: i18n.tc("group.disable-users-from-commenting-on-recipes-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeDisableAmount",
|
||||
label: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food"),
|
||||
description: i18n.tc("group.disable-organizing-recipe-ingredients-by-units-and-food-description"),
|
||||
},
|
||||
];
|
||||
|
||||
const allDays = [
|
||||
{
|
||||
name: i18n.t("general.sunday"),
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.monday"),
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.tuesday"),
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.wednesday"),
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.thursday"),
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.friday"),
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.saturday"),
|
||||
value: 6,
|
||||
},
|
||||
];
|
||||
|
||||
const preferences = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
allDays,
|
||||
preferences,
|
||||
recipePreferences,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.preference-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
max-width: 600px;
|
||||
}
|
||||
</style>
|
||||
622
frontend/components/Domain/QueryFilterBuilder.vue
Normal file
622
frontend/components/Domain/QueryFilterBuilder.vue
Normal file
@@ -0,0 +1,622 @@
|
||||
<template>
|
||||
<v-card class="ma-0" style="overflow-x: auto;">
|
||||
<v-card-text class="ma-0 pa-0">
|
||||
<v-container fluid class="ma-0 pa-0">
|
||||
<draggable
|
||||
:value="fields"
|
||||
handle=".handle"
|
||||
v-bind="{
|
||||
animation: 200,
|
||||
group: 'recipe-instructions',
|
||||
ghostClass: 'ghost',
|
||||
}"
|
||||
@start="drag = true"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<v-row
|
||||
v-for="(field, index) in fields"
|
||||
:key="index"
|
||||
class="d-flex flex-nowrap"
|
||||
style="max-width: 100%;"
|
||||
>
|
||||
<v-col
|
||||
:cols="attrs.fields.icon.cols"
|
||||
:class="attrs.col.class"
|
||||
:style="attrs.fields.icon.style"
|
||||
>
|
||||
<v-icon
|
||||
class="handle"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
</v-icon>
|
||||
</v-col>
|
||||
<v-col
|
||||
:cols="attrs.fields.logicalOperator.cols"
|
||||
:class="attrs.col.class"
|
||||
:style="attrs.fields.logicalOperator.style"
|
||||
>
|
||||
<v-select
|
||||
v-if="index"
|
||||
v-model="field.logicalOperator"
|
||||
:items="[logOps.AND, logOps.OR]"
|
||||
item-text="label"
|
||||
item-value="value"
|
||||
@input="setLogicalOperatorValue(field, index, $event)"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<span :class="attrs.select.textClass" style="width: 100%;">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="showAdvanced"
|
||||
:cols="attrs.fields.leftParens.cols"
|
||||
:class="attrs.col.class"
|
||||
:style="attrs.fields.leftParens.style"
|
||||
>
|
||||
<v-select
|
||||
v-model="field.leftParenthesis"
|
||||
:items="['', '(', '((', '(((']"
|
||||
@input="setLeftParenthesisValue(field, index, $event)"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<span :class="attrs.select.textClass" style="width: 100%;">
|
||||
{{ item }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col
|
||||
:cols="attrs.fields.fieldName.cols"
|
||||
:class="attrs.col.class"
|
||||
:style="attrs.fields.fieldName.style"
|
||||
>
|
||||
<v-select
|
||||
v-model="field.label"
|
||||
:items="fieldDefs"
|
||||
item-text="label"
|
||||
@change="setField(index, $event)"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<span :class="attrs.select.textClass" style="width: 100%;">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col
|
||||
:cols="attrs.fields.relationalOperator.cols"
|
||||
:class="attrs.col.class"
|
||||
:style="attrs.fields.relationalOperator.style"
|
||||
>
|
||||
<v-select
|
||||
v-if="field.type !== 'boolean'"
|
||||
v-model="field.relationalOperatorValue"
|
||||
:items="field.relationalOperatorOptions"
|
||||
item-text="label"
|
||||
item-value="value"
|
||||
@input="setRelationalOperatorValue(field, index, $event)"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<span :class="attrs.select.textClass" style="width: 100%;">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col
|
||||
:cols="attrs.fields.fieldValue.cols"
|
||||
:class="attrs.col.class"
|
||||
:style="attrs.fields.fieldValue.style"
|
||||
>
|
||||
<v-select
|
||||
v-if="field.fieldOptions"
|
||||
v-model="field.values"
|
||||
:items="field.fieldOptions"
|
||||
item-text="label"
|
||||
item-value="value"
|
||||
multiple
|
||||
@input="setFieldValues(field, index, $event)"
|
||||
/>
|
||||
<v-text-field
|
||||
v-else-if="field.type === 'string'"
|
||||
v-model="field.value"
|
||||
@input="setFieldValue(field, index, $event)"
|
||||
/>
|
||||
<v-text-field
|
||||
v-else-if="field.type === 'number'"
|
||||
v-model="field.value"
|
||||
type="number"
|
||||
@input="setFieldValue(field, index, $event)"
|
||||
/>
|
||||
<v-checkbox
|
||||
v-else-if="field.type === 'boolean'"
|
||||
v-model="field.value"
|
||||
@change="setFieldValue(field, index, $event)"
|
||||
/>
|
||||
<v-menu
|
||||
v-else-if="field.type === 'date'"
|
||||
v-model="datePickers[index]"
|
||||
:close-on-content-click="false"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
max-width="290px"
|
||||
min-width="auto"
|
||||
>
|
||||
<template #activator="{ on, attrs: menuAttrs }">
|
||||
<v-text-field
|
||||
v-model="field.value"
|
||||
persistent-hint
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
v-bind="menuAttrs"
|
||||
readonly
|
||||
v-on="on"
|
||||
/>
|
||||
</template>
|
||||
<v-date-picker
|
||||
v-model="field.value"
|
||||
no-title
|
||||
:first-day-of-week="firstDayOfWeek"
|
||||
:local="$i18n.locale"
|
||||
@input="setFieldValue(field, index, $event)"
|
||||
/>
|
||||
</v-menu>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Category"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Category"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
@input="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Tag"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Tag"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
@input="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Tool"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Tool"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
@input="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Food"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Food"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
@input="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Household"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Household"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
@input="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="showAdvanced"
|
||||
:cols="attrs.fields.rightParens.cols"
|
||||
:class="attrs.col.class"
|
||||
:style="attrs.fields.rightParens.style"
|
||||
>
|
||||
<v-select
|
||||
v-model="field.rightParenthesis"
|
||||
:items="['', ')', '))', ')))']"
|
||||
@input="setRightParenthesisValue(field, index, $event)"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<span :class="attrs.select.textClass" style="width: 100%;">
|
||||
{{ item }}
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col
|
||||
:cols="attrs.fields.fieldActions.cols"
|
||||
:class="attrs.col.class"
|
||||
:style="attrs.fields.fieldActions.style"
|
||||
>
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $tc('general.delete'),
|
||||
event: 'delete',
|
||||
disabled: fields.length === 1,
|
||||
}
|
||||
]"
|
||||
class="my-auto"
|
||||
@delete="removeField(index)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</draggable>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-container fluid class="d-flex justify-end pa-0">
|
||||
<v-checkbox
|
||||
v-model="showAdvanced"
|
||||
hide-details
|
||||
:label="$tc('general.show-advanced')"
|
||||
class="my-auto mr-4"
|
||||
/>
|
||||
<BaseButton create :text="$tc('general.add-field')" @click="addField(fieldDefs[0])" />
|
||||
</v-container>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import draggable from "vuedraggable";
|
||||
import { computed, defineComponent, reactive, ref, toRefs, watch } from "@nuxtjs/composition-api";
|
||||
import { useHouseholdSelf } from "~/composables/use-households";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
|
||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
import { Field, FieldDefinition, FieldValue, OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
draggable,
|
||||
RecipeOrganizerSelector,
|
||||
},
|
||||
props: {
|
||||
fieldDefs: {
|
||||
type: Array as () => FieldDefinition[],
|
||||
required: true,
|
||||
},
|
||||
initialQueryFilter: {
|
||||
type: Object as () => QueryFilterJSON | null,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
setup(props, context) {
|
||||
const { household } = useHouseholdSelf();
|
||||
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
showAdvanced: false,
|
||||
qfValid: false,
|
||||
datePickers: [] as boolean[],
|
||||
drag: false,
|
||||
});
|
||||
|
||||
const storeMap = {
|
||||
[Organizer.Category]: useCategoryStore(),
|
||||
[Organizer.Tag]: useTagStore(),
|
||||
[Organizer.Tool]: useToolStore(),
|
||||
[Organizer.Food]: useFoodStore(),
|
||||
[Organizer.Household]: useHouseholdStore(),
|
||||
};
|
||||
|
||||
function onDragEnd(event: any) {
|
||||
state.drag = false;
|
||||
|
||||
const oldIndex: number = event.oldIndex;
|
||||
const newIndex: number = event.newIndex;
|
||||
state.datePickers[oldIndex] = false;
|
||||
state.datePickers[newIndex] = false;
|
||||
|
||||
const field = fields.value.splice(oldIndex, 1)[0];
|
||||
fields.value.splice(newIndex, 0, field);
|
||||
}
|
||||
|
||||
const fields = ref<Field[]>([]);
|
||||
|
||||
function addField(field: FieldDefinition) {
|
||||
fields.value.push(getFieldFromFieldDef(field));
|
||||
state.datePickers.push(false);
|
||||
};
|
||||
|
||||
function setField(index: number, fieldLabel: string) {
|
||||
state.datePickers[index] = false;
|
||||
const fieldDef = props.fieldDefs.find((fieldDef) => fieldDef.label === fieldLabel);
|
||||
if (!fieldDef) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resetValue = (fieldDef.type !== fields.value[index].type) || (fieldDef.fieldOptions !== fields.value[index].fieldOptions);
|
||||
const updatedField = {...fields.value[index], ...fieldDef};
|
||||
|
||||
// we have to set this explicitly since it might be undefined
|
||||
updatedField.fieldOptions = fieldDef.fieldOptions;
|
||||
|
||||
fields.value.splice(index, 1, getFieldFromFieldDef(updatedField, resetValue));
|
||||
}
|
||||
|
||||
function setLeftParenthesisValue(field: Field, index: number, value: string) {
|
||||
fields.value.splice(index, 1, {
|
||||
...field,
|
||||
leftParenthesis: value,
|
||||
});
|
||||
}
|
||||
|
||||
function setRightParenthesisValue(field: Field, index: number, value: string) {
|
||||
fields.value.splice(index, 1, {
|
||||
...field,
|
||||
rightParenthesis: value,
|
||||
});
|
||||
}
|
||||
|
||||
function setLogicalOperatorValue(field: Field, index: number, value: LogicalOperator | undefined) {
|
||||
if (!value) {
|
||||
value = logOps.value.AND.value;
|
||||
}
|
||||
|
||||
fields.value.splice(index, 1, {
|
||||
...field,
|
||||
logicalOperator: value ? logOps.value[value] : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function setRelationalOperatorValue(field: Field, index: number, value: RelationalKeyword | RelationalOperator) {
|
||||
fields.value.splice(index, 1, {
|
||||
...field,
|
||||
relationalOperatorValue: relOps.value[value],
|
||||
});
|
||||
}
|
||||
|
||||
function setFieldValue(field: Field, index: number, value: FieldValue) {
|
||||
state.datePickers[index] = false;
|
||||
fields.value.splice(index, 1, {
|
||||
...field,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
function setFieldValues(field: Field, index: number, values: FieldValue[]) {
|
||||
fields.value.splice(index, 1, {
|
||||
...field,
|
||||
values,
|
||||
});
|
||||
}
|
||||
|
||||
function setOrganizerValues(field: Field, index: number, values: OrganizerBase[]) {
|
||||
setFieldValues(field, index, values.map((value) => value.id.toString()));
|
||||
}
|
||||
|
||||
function removeField(index: number) {
|
||||
fields.value.splice(index, 1);
|
||||
state.datePickers.splice(index, 1);
|
||||
};
|
||||
|
||||
watch(
|
||||
// Toggling showAdvanced changes the builder logic without changing the field values,
|
||||
// so we need to manually trigger reactivity to re-run the builder.
|
||||
() => state.showAdvanced,
|
||||
() => {
|
||||
if (fields.value?.length) {
|
||||
fields.value = [...fields.value];
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => fields.value,
|
||||
(newFields) => {
|
||||
newFields.forEach((field, index) => {
|
||||
const updatedField = getFieldFromFieldDef(field);
|
||||
fields.value[index] = updatedField;
|
||||
});
|
||||
|
||||
const qf = buildQueryFilterString(fields.value, state.showAdvanced);
|
||||
if (qf) {
|
||||
console.debug(`Set query filter: ${qf}`);
|
||||
}
|
||||
state.qfValid = !!qf;
|
||||
|
||||
context.emit("input", qf || undefined);
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
},
|
||||
);
|
||||
|
||||
async function hydrateOrganizers(field: Field, index: number) {
|
||||
if (!field.values?.length || !isOrganizerType(field.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.organizers = [];
|
||||
|
||||
const { store, actions } = storeMap[field.type];
|
||||
if (!store.value.length) {
|
||||
await actions.refresh();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
const organizers = field.values.map((value) => store.value.find((organizer) => organizer.id === value));
|
||||
field.organizers = organizers.filter((organizer) => organizer !== undefined) as OrganizerBase[];
|
||||
setOrganizerValues(field, index, field.organizers);
|
||||
}
|
||||
|
||||
function initFieldsError(error = "") {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
fields.value = [];
|
||||
if (props.fieldDefs.length) {
|
||||
addField(props.fieldDefs[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFields() {
|
||||
if (!props.initialQueryFilter?.parts?.length) {
|
||||
return initFieldsError();
|
||||
};
|
||||
|
||||
const initFields: Field[] = [];
|
||||
let error = false;
|
||||
props.initialQueryFilter.parts.forEach((part: QueryFilterJSONPart, index: number) => {
|
||||
const fieldDef = props.fieldDefs.find((fieldDef) => fieldDef.name === part.attributeName);
|
||||
if (!fieldDef) {
|
||||
error = true;
|
||||
return initFieldsError(`Invalid query filter; unknown attribute name "${part.attributeName || ""}"`);
|
||||
}
|
||||
|
||||
const field = getFieldFromFieldDef(fieldDef);
|
||||
field.leftParenthesis = part.leftParenthesis || field.leftParenthesis;
|
||||
field.rightParenthesis = part.rightParenthesis || field.rightParenthesis;
|
||||
field.logicalOperator = part.logicalOperator ?
|
||||
logOps.value[part.logicalOperator] : field.logicalOperator;
|
||||
field.relationalOperatorValue = part.relationalOperator ?
|
||||
relOps.value[part.relationalOperator] : field.relationalOperatorValue;
|
||||
|
||||
if (field.leftParenthesis || field.rightParenthesis) {
|
||||
state.showAdvanced = true;
|
||||
}
|
||||
|
||||
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
|
||||
if (typeof part.value === "string") {
|
||||
field.values = part.value ? [part.value] : [];
|
||||
} else {
|
||||
field.values = part.value || [];
|
||||
}
|
||||
|
||||
if (isOrganizerType(field.type)) {
|
||||
hydrateOrganizers(field, index);
|
||||
}
|
||||
|
||||
} else if (field.type === "boolean") {
|
||||
const boolString = part.value || "false";
|
||||
field.value = (
|
||||
boolString[0].toLowerCase() === "t" ||
|
||||
boolString[0].toLowerCase() === "y" ||
|
||||
boolString[0] === "1"
|
||||
);
|
||||
} else if (field.type === "number") {
|
||||
field.value = Number(part.value as string || "0");
|
||||
if (isNaN(field.value)) {
|
||||
error = true;
|
||||
return initFieldsError(`Invalid query filter; invalid number value "${(part.value || "").toString()}"`);
|
||||
}
|
||||
} else if (field.type === "date") {
|
||||
field.value = part.value as string || "";
|
||||
const date = new Date(field.value);
|
||||
if (isNaN(date.getTime())) {
|
||||
error = true;
|
||||
return initFieldsError(`Invalid query filter; invalid date value "${(part.value || "").toString()}"`);
|
||||
}
|
||||
} else {
|
||||
field.value = part.value as string || "";
|
||||
}
|
||||
|
||||
initFields.push(field);
|
||||
});
|
||||
|
||||
if (initFields.length && !error) {
|
||||
fields.value = initFields;
|
||||
} else {
|
||||
initFieldsError();
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
initializeFields();
|
||||
} catch (error) {
|
||||
initFieldsError(`Error initializing fields: ${(error || "").toString()}`);
|
||||
}
|
||||
|
||||
|
||||
const attrs = computed(() => {
|
||||
const baseColMaxWidth = 55;
|
||||
const attrs = {
|
||||
col: {
|
||||
class: "d-flex justify-center align-end field-col pa-1",
|
||||
},
|
||||
select: {
|
||||
textClass: "d-flex justify-center text-center",
|
||||
},
|
||||
fields: {
|
||||
icon: {
|
||||
cols: 1,
|
||||
style: "width: fit-content;",
|
||||
},
|
||||
leftParens: {
|
||||
cols: state.showAdvanced ? 1 : 0,
|
||||
style: `min-width: ${state.showAdvanced ? baseColMaxWidth : 0}px;`,
|
||||
},
|
||||
logicalOperator: {
|
||||
cols: 1,
|
||||
style: `min-width: ${baseColMaxWidth}px;`,
|
||||
},
|
||||
fieldName: {
|
||||
cols: state.showAdvanced ? 2 : 3,
|
||||
style: `min-width: ${state.showAdvanced ? baseColMaxWidth * 2 : baseColMaxWidth * 3}px;`,
|
||||
},
|
||||
relationalOperator: {
|
||||
cols: 2,
|
||||
style: `min-width: ${baseColMaxWidth * 2}px;`,
|
||||
},
|
||||
fieldValue: {
|
||||
cols: state.showAdvanced ? 3 : 4,
|
||||
style: `min-width: ${state.showAdvanced ? baseColMaxWidth * 2 : baseColMaxWidth * 3}px;`,
|
||||
},
|
||||
rightParens: {
|
||||
cols: state.showAdvanced ? 1 : 0,
|
||||
style: `min-width: ${state.showAdvanced ? baseColMaxWidth : 0}px;`,
|
||||
},
|
||||
fieldActions: {
|
||||
cols: 1,
|
||||
style: `min-width: ${baseColMaxWidth}px;`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return attrs;
|
||||
})
|
||||
|
||||
return {
|
||||
Organizer,
|
||||
...toRefs(state),
|
||||
logOps,
|
||||
relOps,
|
||||
attrs,
|
||||
firstDayOfWeek,
|
||||
onDragEnd,
|
||||
// Fields
|
||||
fields,
|
||||
addField,
|
||||
setField,
|
||||
setLeftParenthesisValue,
|
||||
setRightParenthesisValue,
|
||||
setLogicalOperatorValue,
|
||||
setRelationalOperatorValue,
|
||||
setFieldValue,
|
||||
setFieldValues,
|
||||
setOrganizerValues,
|
||||
removeField,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
font-size: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -21,31 +21,23 @@
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<div v-if="!open" class="custom-btn-group ma-1">
|
||||
<RecipeFavoriteBadge v-if="loggedIn" class="mx-1" color="info" button-style :recipe-id="recipe.id" show-always />
|
||||
<RecipeTimelineBadge v-if="loggedIn" button-style :slug="recipe.slug" :recipe-name="recipe.name" />
|
||||
<RecipeFavoriteBadge v-if="loggedIn" class="ml-1" color="info" button-style :recipe-id="recipe.id" show-always />
|
||||
<RecipeTimelineBadge v-if="loggedIn" button-style class="ml-1" :slug="recipe.slug" :recipe-name="recipe.name" />
|
||||
<div v-if="loggedIn">
|
||||
<v-tooltip v-if="!locked" bottom color="info">
|
||||
<v-tooltip v-if="canEdit" bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on" @click="$emit('edit', true)">
|
||||
<v-btn fab small class="ml-1" color="info" v-bind="attrs" v-on="on" @click="$emit('edit', true)">
|
||||
<v-icon> {{ $globals.icons.edit }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("general.edit") }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip v-else bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on">
|
||||
<v-icon> {{ $globals.icons.lock }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span> {{ $t("recipe.locked-by-owner") }} </span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
<RecipeTimerMenu
|
||||
fab
|
||||
color="info"
|
||||
class="mr-1"
|
||||
class="ml-1"
|
||||
/>
|
||||
|
||||
<RecipeContextMenu
|
||||
@@ -72,6 +64,7 @@
|
||||
share: loggedIn,
|
||||
recipeActions: true,
|
||||
}"
|
||||
class="ml-1"
|
||||
@print="$emit('print')"
|
||||
/>
|
||||
</div>
|
||||
@@ -135,7 +128,7 @@ export default defineComponent({
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
locked: {
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
:recipe-id="recipeId"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: true,
|
||||
edit: false,
|
||||
download: true,
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<v-img
|
||||
v-if="!fallBackImage"
|
||||
:height="height"
|
||||
min-height="125"
|
||||
max-height="fill-height"
|
||||
:src="getImage(recipeId)"
|
||||
@click="$emit('click')"
|
||||
@load="fallBackImage = false"
|
||||
@@ -52,8 +54,8 @@ export default defineComponent({
|
||||
default: null,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
type: [Number, String],
|
||||
default: "fill-height",
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
|
||||
@@ -1,80 +1,86 @@
|
||||
<template>
|
||||
<v-expand-transition>
|
||||
<v-card
|
||||
:ripple="false"
|
||||
:class="isFlat ? 'mx-auto flat' : 'mx-auto'"
|
||||
:style="{ cursor }"
|
||||
hover
|
||||
:to="$listeners.selected ? undefined : recipeRoute"
|
||||
@click="$emit('selected')"
|
||||
>
|
||||
<v-img v-if="vertical" class="rounded-sm">
|
||||
<RecipeCardImage
|
||||
:icon-size="100"
|
||||
:height="150"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
small
|
||||
:image-version="image"
|
||||
/>
|
||||
</v-img>
|
||||
<v-list-item three-line :class="vertical ? 'px-2' : 'px-0'">
|
||||
<slot v-if="!vertical" name="avatar">
|
||||
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0">
|
||||
<RecipeCardImage
|
||||
:icon-size="100"
|
||||
:height="125"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
small
|
||||
:image-version="image"
|
||||
/>
|
||||
</v-list-item-avatar>
|
||||
</slot>
|
||||
<v-list-item-content class="py-0">
|
||||
<v-list-item-title class="mt-3 mb-1">{{ name }} </v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<SafeMarkdown :source="description" />
|
||||
</v-list-item-subtitle>
|
||||
<div class="d-flex flex-wrap justify-end align-center">
|
||||
<slot name="actions">
|
||||
<RecipeFavoriteBadge v-if="isOwnGroup && showRecipeContent" :recipe-id="recipeId" show-always />
|
||||
<RecipeRating
|
||||
:class="isOwnGroup ? 'ml-auto' : 'ml-auto pb-2'"
|
||||
:value="rating"
|
||||
:recipe-id="recipeId"
|
||||
<div :style="`height: ${height}`">
|
||||
<v-expand-transition>
|
||||
<v-card
|
||||
:ripple="false"
|
||||
:class="isFlat ? 'mx-auto flat' : 'mx-auto'"
|
||||
:style="{ cursor }"
|
||||
hover
|
||||
:to="$listeners.selected ? undefined : recipeRoute"
|
||||
@click="$emit('selected')"
|
||||
>
|
||||
<v-img v-if="vertical" class="rounded-sm">
|
||||
<RecipeCardImage
|
||||
:icon-size="100"
|
||||
:height="height"
|
||||
:slug="slug"
|
||||
:recipe-id="recipeId"
|
||||
small
|
||||
:image-version="image"
|
||||
/>
|
||||
</v-img>
|
||||
<v-list-item three-line :class="vertical ? 'px-2' : 'px-0'">
|
||||
<slot v-if="!vertical" name="avatar">
|
||||
<v-list-item-avatar tile :height="height" width="125" class="v-mobile-img rounded-sm my-0">
|
||||
<RecipeCardImage
|
||||
:icon-size="100"
|
||||
:height="height"
|
||||
:slug="slug"
|
||||
:small="true"
|
||||
:recipe-id="recipeId"
|
||||
:image-version="image"
|
||||
small
|
||||
/>
|
||||
<v-spacer></v-spacer>
|
||||
</v-list-item-avatar>
|
||||
</slot>
|
||||
<v-list-item-content class="py-0">
|
||||
<v-list-item-title class="mt-1 mb-1 text-top">{{ name }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="ma-0 text-top">
|
||||
<SafeMarkdown :source="description" />
|
||||
</v-list-item-subtitle>
|
||||
<div class="d-flex flex-wrap justify-start ma-0">
|
||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" url-prefix="tags" />
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-end align-center">
|
||||
<slot name="actions">
|
||||
<RecipeFavoriteBadge v-if="isOwnGroup && showRecipeContent" :recipe-id="recipeId" show-always />
|
||||
<RecipeRating
|
||||
v-if="showRecipeContent"
|
||||
:class="isOwnGroup ? 'ml-auto' : 'ml-auto pb-2'"
|
||||
:value="rating"
|
||||
:recipe-id="recipeId"
|
||||
:slug="slug"
|
||||
:small="true"
|
||||
/>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<!-- We also add padding to the v-rating above to compensate -->
|
||||
<RecipeContextMenu
|
||||
v-if="isOwnGroup && showRecipeContent"
|
||||
:slug="slug"
|
||||
:menu-icon="$globals.icons.dotsHorizontal"
|
||||
:name="name"
|
||||
:recipe-id="recipeId"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: true,
|
||||
download: true,
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
}"
|
||||
@deleted="$emit('delete', slug)"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<slot />
|
||||
</v-card>
|
||||
</v-expand-transition>
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<!-- We also add padding to the v-rating above to compensate -->
|
||||
<RecipeContextMenu
|
||||
v-if="isOwnGroup && showRecipeContent"
|
||||
:slug="slug"
|
||||
:menu-icon="$globals.icons.dotsHorizontal"
|
||||
:name="name"
|
||||
:recipe-id="recipeId"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: false,
|
||||
download: true,
|
||||
mealplanner: true,
|
||||
shoppingList: true,
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
}"
|
||||
@deleted="$emit('delete', slug)"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<slot />
|
||||
</v-card>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -83,6 +89,7 @@ import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||
import RecipeRating from "./RecipeRating.vue";
|
||||
import RecipeChips from "./RecipeChips.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineComponent({
|
||||
@@ -91,6 +98,7 @@ export default defineComponent({
|
||||
RecipeContextMenu,
|
||||
RecipeRating,
|
||||
RecipeCardImage,
|
||||
RecipeChips,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
@@ -114,6 +122,10 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: "abc123",
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
recipeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -126,6 +138,14 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 150,
|
||||
},
|
||||
imageHeight: {
|
||||
type: [Number, String],
|
||||
default: "fill-height",
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<ContextMenu
|
||||
v-if="!$vuetify.breakpoint.xsOnly"
|
||||
v-if="!$vuetify.breakpoint.smAndDown"
|
||||
:items="[
|
||||
{
|
||||
title: $tc('general.toggle-view'),
|
||||
@@ -69,50 +69,52 @@
|
||||
@toggle-dense-view="toggleMobileCards()"
|
||||
/>
|
||||
</v-app-bar>
|
||||
<div v-if="recipes" class="mt-2">
|
||||
<v-row v-if="!useMobileCards">
|
||||
<v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
|
||||
<v-lazy>
|
||||
<RecipeCard
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
:recipe-id="recipe.id"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else dense>
|
||||
<v-col
|
||||
v-for="recipe in recipes"
|
||||
:key="recipe.name"
|
||||
cols="12"
|
||||
:sm="singleColumn ? '12' : '12'"
|
||||
:md="singleColumn ? '12' : '6'"
|
||||
:lg="singleColumn ? '12' : '4'"
|
||||
:xl="singleColumn ? '12' : '3'"
|
||||
>
|
||||
<v-lazy>
|
||||
<RecipeCardMobile
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
:recipe-id="recipe.id"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div v-if="recipes && ready">
|
||||
<div class="mt-2">
|
||||
<v-row v-if="!useMobileCards">
|
||||
<v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
|
||||
<v-lazy>
|
||||
<RecipeCard
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
:recipe-id="recipe.id"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else dense>
|
||||
<v-col
|
||||
v-for="recipe in recipes"
|
||||
:key="recipe.name"
|
||||
cols="12"
|
||||
:sm="singleColumn ? '12' : '12'"
|
||||
:md="singleColumn ? '12' : '6'"
|
||||
:lg="singleColumn ? '12' : '4'"
|
||||
:xl="singleColumn ? '12' : '3'"
|
||||
>
|
||||
<v-lazy>
|
||||
<RecipeCardMobile
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
:recipe-id="recipe.id"
|
||||
/>
|
||||
</v-lazy>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<v-card v-intersect="infiniteScroll"></v-card>
|
||||
<v-fade-transition>
|
||||
<AppLoader v-if="loading" :loading="loading" />
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
<v-card v-intersect="infiniteScroll"></v-card>
|
||||
<v-fade-transition>
|
||||
<AppLoader v-if="loading" :loading="loading" />
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -223,36 +225,42 @@ export default defineComponent({
|
||||
|
||||
const queryFilter = computed(() => {
|
||||
const orderBy = props.query?.orderBy || preferences.value.orderBy;
|
||||
return preferences.value.filterNull && orderBy ? `${orderBy} IS NOT NULL` : null;
|
||||
const orderByFilter = preferences.value.filterNull && orderBy ? `${orderBy} IS NOT NULL` : null;
|
||||
|
||||
if (props.query.queryFilter && orderByFilter) {
|
||||
return `(${props.query.queryFilter}) AND ${orderByFilter}`;
|
||||
} else if (props.query.queryFilter) {
|
||||
return props.query.queryFilter;
|
||||
} else {
|
||||
return orderByFilter;
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchRecipes(pageCount = 1) {
|
||||
return await fetchMore(
|
||||
page.value,
|
||||
// we double-up the first call to avoid a bug with large screens that render the entire first page without scrolling, preventing additional loading
|
||||
perPage * pageCount,
|
||||
props.query?.orderBy || preferences.value.orderBy,
|
||||
props.query?.orderDirection || preferences.value.orderDirection,
|
||||
props.query,
|
||||
// filter out recipes that have a null value for the property we're sorting by
|
||||
// we use a computed queryFilter to filter out recipes that have a null value for the property we're sorting by
|
||||
queryFilter.value
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.query) {
|
||||
await initRecipes();
|
||||
ready.value = true;
|
||||
}
|
||||
await initRecipes();
|
||||
ready.value = true;
|
||||
});
|
||||
|
||||
let lastQuery: string | undefined;
|
||||
let lastQuery: string | undefined = JSON.stringify(props.query);
|
||||
watch(
|
||||
() => props.query,
|
||||
async (newValue: RecipeSearchQuery | undefined) => {
|
||||
const newValueString = JSON.stringify(newValue)
|
||||
if (newValue && (!ready.value || lastQuery !== newValueString)) {
|
||||
if (lastQuery !== newValueString) {
|
||||
lastQuery = newValueString;
|
||||
ready.value = false;
|
||||
await initRecipes();
|
||||
ready.value = true;
|
||||
}
|
||||
@@ -261,8 +269,12 @@ export default defineComponent({
|
||||
|
||||
async function initRecipes() {
|
||||
page.value = 1;
|
||||
const newRecipes = await fetchRecipes(2);
|
||||
if (!newRecipes.length) {
|
||||
hasMore.value = true;
|
||||
|
||||
// we double-up the first call to avoid a bug with large screens that render
|
||||
// the entire first page without scrolling, preventing additional loading
|
||||
const newRecipes = await fetchRecipes(page.value + 1);
|
||||
if (newRecipes.length < perPage) {
|
||||
hasMore.value = false;
|
||||
}
|
||||
|
||||
@@ -274,7 +286,7 @@ export default defineComponent({
|
||||
|
||||
const infiniteScroll = useThrottleFn(() => {
|
||||
useAsync(async () => {
|
||||
if (!ready.value || !hasMore.value || loading.value) {
|
||||
if (!hasMore.value || loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -282,9 +294,10 @@ export default defineComponent({
|
||||
page.value = page.value + 1;
|
||||
|
||||
const newRecipes = await fetchRecipes();
|
||||
if (!newRecipes.length) {
|
||||
if (newRecipes.length < perPage) {
|
||||
hasMore.value = false;
|
||||
} else {
|
||||
}
|
||||
if (newRecipes.length) {
|
||||
context.emit(APPEND_RECIPES_EVENT, newRecipes);
|
||||
}
|
||||
|
||||
@@ -337,7 +350,7 @@ export default defineComponent({
|
||||
);
|
||||
break;
|
||||
case EVENTS.updated:
|
||||
setter("update_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending, "desc", false);
|
||||
setter("updated_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending, "desc", false);
|
||||
break;
|
||||
case EVENTS.lastMade:
|
||||
setter(
|
||||
@@ -379,6 +392,7 @@ export default defineComponent({
|
||||
displayTitleIcon,
|
||||
EVENTS,
|
||||
infiniteScroll,
|
||||
ready,
|
||||
loading,
|
||||
navigateRandom,
|
||||
preferences,
|
||||
|
||||
@@ -138,11 +138,11 @@ import RecipeDialogShare from "./RecipeDialogShare.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useGroupRecipeActions } from "~/composables/use-group-recipe-actions";
|
||||
import { useGroupSelf } from "~/composables/use-groups";
|
||||
import { useHouseholdSelf } from "~/composables/use-households";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { usePlanTypeOptions } from "~/composables/use-group-mealplan";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import { GroupRecipeActionOut, ShoppingListSummary } from "~/lib/api/types/group";
|
||||
import { GroupRecipeActionOut, ShoppingListSummary } from "~/lib/api/types/household";
|
||||
import { PlanEntryType } from "~/lib/api/types/meal-plan";
|
||||
import { useAxiosDownloader } from "~/composables/api/use-axios-download";
|
||||
|
||||
@@ -254,14 +254,14 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const { i18n, $auth, $globals } = useContext();
|
||||
const { group } = useGroupSelf();
|
||||
const { household } = useHouseholdSelf();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
return group.value?.preferences?.firstDayOfWeek || 0;
|
||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
@@ -376,7 +376,7 @@ export default defineComponent({
|
||||
const response = await groupRecipeActionsStore.execute(action, props.recipe);
|
||||
|
||||
if (action.actionType === "post") {
|
||||
if (!response || (response.status >= 200 && response.status < 300)) {
|
||||
if (!response?.error) {
|
||||
alert.success(i18n.tc("events.message-sent"));
|
||||
} else {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user