mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-27 10:13:11 -05:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe0cdc8a4 | ||
|
|
8b07f4e9e0 | ||
|
|
445fed853f | ||
|
|
3cc3825bbe | ||
|
|
23026a4cc4 | ||
|
|
b558a50a7d | ||
|
|
8866256a21 | ||
|
|
7bcbe0464a | ||
|
|
f471e38d88 | ||
|
|
561f586dd1 | ||
|
|
38e4afcc3c | ||
|
|
c44edf97ae | ||
|
|
d0cc0a089a | ||
|
|
7188e58f4c | ||
|
|
40b78e427d | ||
|
|
c8e239bd81 | ||
|
|
cc35a4be19 | ||
|
|
b6111afe69 | ||
|
|
88baa46a33 | ||
|
|
732a2cbc51 | ||
|
|
34a10f375f | ||
|
|
f000dffde2 | ||
|
|
abcf40899f | ||
|
|
d6794cba7d | ||
|
|
016108d35f | ||
|
|
9b41990ea9 | ||
|
|
3cadc3d04b | ||
|
|
874bea7fa4 | ||
|
|
ce48ae61c7 | ||
|
|
c746f7f4f8 | ||
|
|
7f67f844bc | ||
|
|
079ebd8ee1 | ||
|
|
138093d062 | ||
|
|
99cf606a45 | ||
|
|
41e079d423 | ||
|
|
f35e9c20d6 | ||
|
|
51893e89cd | ||
|
|
76830802cc | ||
|
|
29db7f8a67 | ||
|
|
22a517b9c0 | ||
|
|
2ffaecb7b7 | ||
|
|
bfc28fc506 | ||
|
|
6ee4fc2ea6 | ||
|
|
567bff8b42 | ||
|
|
3a5f99919b | ||
|
|
d0f9917c6f | ||
|
|
d1fe1f44b8 | ||
|
|
b00b0c8af4 | ||
|
|
c6403a7998 | ||
|
|
a76f472aa4 | ||
|
|
6bc7c4ceb0 | ||
|
|
cf45fa0015 | ||
|
|
5b15ffdf5f | ||
|
|
53f15b17ea | ||
|
|
cbcbc3a339 | ||
|
|
9a616910f3 | ||
|
|
0167f2f1ca | ||
|
|
b689c4715b | ||
|
|
d0f89956f4 | ||
|
|
25988836c0 | ||
|
|
5f25b2492e | ||
|
|
4562a8ca10 | ||
|
|
80468d0c47 | ||
|
|
9efd9399d9 | ||
|
|
8d0604da3a | ||
|
|
9818d567b9 | ||
|
|
a4a33af1c3 | ||
|
|
965fa81d9b | ||
|
|
0eb1e952b2 | ||
|
|
021a9025da | ||
|
|
fe69114b0b | ||
|
|
296e24b119 | ||
|
|
4b0e9c0d76 | ||
|
|
9c1d0d9ec9 | ||
|
|
e7cb7c6cfd |
2
.github/workflows/dockerbuild.prod.yml
vendored
2
.github/workflows/dockerbuild.prod.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Docker Build Production
|
||||
name: Docker Build Dev
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"python.formatting.provider": "black",
|
||||
"python.pythonPath": ".venv/bin/python3.9",
|
||||
"python.pythonPath": ".venv/bin/python3.8",
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.autoComplete.extraPaths": ["mealie", "mealie/mealie"],
|
||||
|
||||
45
.vscode/tasks.json
vendored
45
.vscode/tasks.json
vendored
@@ -13,42 +13,15 @@
|
||||
"group": "test"
|
||||
},
|
||||
{
|
||||
"label": "Production: Build and Start Docker Compose",
|
||||
"command": "./dev/scripts/docker-compose.sh",
|
||||
"type": "shell",
|
||||
"args": [],
|
||||
"problemMatcher": ["$tsc"],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"group": "test"
|
||||
},
|
||||
{
|
||||
"label": "Dev: Start local Backend",
|
||||
"command": "../${config:python.pythonPath}",
|
||||
"args": ["app.py"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/mealie/"
|
||||
},
|
||||
"type": "shell",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"group": "groupA"
|
||||
"label": "Production: Build and Start Docker Compose",
|
||||
"command": "./dev/scripts/docker-compose.sh",
|
||||
"type": "shell",
|
||||
"args": [],
|
||||
"problemMatcher": ["$tsc"],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"group": "test"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Dev: Start local Frontend",
|
||||
"command": "npm run serve",
|
||||
"type": "shell",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/frontend/"
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"group": "groupA"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ RUN npm install
|
||||
COPY ./frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
FROM python:3.9-alpine
|
||||
FROM python:3.8-alpine
|
||||
|
||||
RUN apk add --no-cache libxml2-dev libxslt-dev libxml2
|
||||
ENV ENV prod
|
||||
@@ -17,6 +17,7 @@ COPY ./pyproject.toml /app/
|
||||
RUN apk add --update --no-cache --virtual .build-deps \
|
||||
curl \
|
||||
g++ \
|
||||
py-lxml \
|
||||
python3-dev \
|
||||
musl-dev \
|
||||
gcc \
|
||||
@@ -34,5 +35,5 @@ COPY --from=build-stage /app/dist /app/dist
|
||||
RUN rm -rf /app/test /app/.temp
|
||||
|
||||
|
||||
VOLUME [ "/app/data/" ]
|
||||
VOLUME [ "/app_data/" ]
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
|
||||
33
Dockerfile.arm
Normal file
33
Dockerfile.arm
Normal file
@@ -0,0 +1,33 @@
|
||||
FROM node:lts-alpine as build-stage
|
||||
WORKDIR /app
|
||||
COPY ./frontend/package*.json ./
|
||||
RUN npm install
|
||||
COPY ./frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
FROM mrnr91/uvicorn-gunicorn-fastapi:python3.8
|
||||
|
||||
|
||||
COPY ./requirements.txt /app/requirements.txt
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y python-pip python-dev git curl --no-install-recommends
|
||||
|
||||
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \
|
||||
cd /usr/local/bin && \
|
||||
ln -s /opt/poetry/bin/poetry && \
|
||||
poetry config virtualenvs.create false
|
||||
|
||||
COPY ./pyproject.toml ./app/poetry.lock* /app/
|
||||
|
||||
COPY ./mealie /app
|
||||
RUN poetry install --no-root --no-dev
|
||||
COPY --from=build-stage /app/dist /app/dist
|
||||
RUN rm -rf /app/test /app/.temp
|
||||
|
||||
ENV ENV prod
|
||||
ENV APP_MODULE "app:app"
|
||||
|
||||
VOLUME [ "/app/data" ]
|
||||
1
dev/scripts/buildx.sh
Normal file
1
dev/scripts/buildx.sh
Normal file
@@ -0,0 +1 @@
|
||||
docker buildx build .
|
||||
0
dev/scripts/scrape_recipe.py
Executable file → Normal file
0
dev/scripts/scrape_recipe.py
Executable file → Normal file
17
dev/scripts/start-dev-svr.ps1
Normal file
17
dev/scripts/start-dev-svr.ps1
Normal file
@@ -0,0 +1,17 @@
|
||||
$CWD = Get-Location
|
||||
|
||||
$pyFolder = Join-Path -Path $CWD -ChildPath "mealie"
|
||||
$pyVenv = Join-Path -Path $CWD -ChildPath "/venv/Scripts/python.exe"
|
||||
$pyScript = Join-Path -Path $CWD -ChildPath "/mealie/app.py"
|
||||
|
||||
$pythonCommand = "powershell.exe -NoExit -Command $pyVenv $pyScript"
|
||||
|
||||
$vuePath = Join-Path -Path $CWD -ChildPath "/frontend"
|
||||
$npmCommand = "powershell.exe -NoExit -Command npm run serve"
|
||||
|
||||
wt -d $pyFolder "powershell.exe" $pythonCommand `; split-pane -d $vuePath "powershell.exe" $npmCommand
|
||||
|
||||
Start-Process chrome "http://127.0.0.1:8000/docs"
|
||||
Start-Process chrome "http://127.0.0.1:8080
|
||||
"
|
||||
|
||||
16
docker-compose.arm.yml
Normal file
16
docker-compose.arm.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
# Use root/example as user/password credentials
|
||||
# Frontend/Backend Served via the same Uvicorn Server
|
||||
version: "3.1"
|
||||
services:
|
||||
mealie:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: Dockerfile.arm
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
- 9090:80
|
||||
environment:
|
||||
db_type: sql
|
||||
volumes:
|
||||
- ./mealie/data/:/app/data
|
||||
@@ -10,3 +10,6 @@ services:
|
||||
- 9090:80
|
||||
environment:
|
||||
db_type: sqlite
|
||||
|
||||
# volumes:
|
||||
# - ./mealie/data/:/app/data
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
# Release Notes
|
||||
|
||||
## v0.3.0 - Draft!
|
||||
|
||||
### Features and Improvements
|
||||
- Open search with `/` hotkey!
|
||||
- Unified and improved snackbar notifications
|
||||
- Recipe Viewer
|
||||
- Categories, Tags, and Notes will not be displayed below the steps on smaller screens
|
||||
- Recipe Editor
|
||||
- Text areas now auto grow to fit content
|
||||
- Description, Steps, and Notes support Markdown! This includes inline html in Markdown.
|
||||
|
||||
### Development / Misc
|
||||
- Added async file response for images, downloading files.
|
||||
- Breakup recipe view component
|
||||
|
||||
## v0.2.0 - Now with Test!
|
||||
This is, what I think, is a big release! Tons of new features and some great quality of life improvements with some additional features. You may find that I made promises to include some fixes/features in v0.2.0. The short of is I greatly underestimated the work needed to refactor the database to a usable state and integrate categories in a way that is useful for users. This shouldn't be taken as a sign that I'm dropping those feature requests or ignoring them. I felt it was better to push a release in the current state rather than drag on development to try and fulfil all of the promises I made.
|
||||
|
||||
@@ -71,6 +56,7 @@ This is, what I think, is a big release! Tons of new features and some great qua
|
||||
!!! error "Breaking Changes"
|
||||
- API endpoints have been refactored to adhear to a more consistent standard. This is a WIP and more changes are likely to occur.
|
||||
- Officially Dropped MongoDB Support
|
||||
- Mounting volume moved to different internal location due to development issues. New volume should be mounted as `mealie/data:/app_data/`. Volume mounts need to be changed.
|
||||
- Database Breaks! We have not yet implemented a database migration service. As such, upgrades cannot be done by simply pulling the image. You must first export your recipes, update your deployment, and then import your recipes. This pattern is likely to be how upgrades take place prior to v1.0. After v1.0 migrations will be done automatically.
|
||||
|
||||
## v0.1.0 - Initial Beta
|
||||
|
||||
@@ -14,16 +14,7 @@ There are VSCode tasks created in the .vscode folder. You can use these to quick
|
||||
|
||||
|
||||
## Without Docker
|
||||
Prerequisites
|
||||
|
||||
- Python 3.8+
|
||||
- Poetry
|
||||
- Nodejs
|
||||
- npm
|
||||
|
||||
change directories into the mealie directory and run poetry install. cd into the frontend directory and run npm install. After installing dependencies, you can use vscode tasks to run the front and backend server. Use the command pallette to access the tasks.
|
||||
|
||||
Alternatively you can run `npm run serve` in the frontend directory and `python app.py` in the mealie directory to get everything up and running for development.
|
||||
?? TODO
|
||||
|
||||
## Trouble Shooting
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Installation
|
||||
To deploy docker on your local network it is highly recommended to use docker to deploy the image straight from dockerhub. Using the docker-compose below you should be able to get a stack up and running easily by changing a few default values and deploying. Currently only SQLite is supported. Postrgres support is planned, however for most loads you may find SQLite performant enough.
|
||||
To deploy docker on your local network it is highly recommended to use docker to deploy the image straight from dockerhub. Using the docker-compose below you should be able to get a stack up and running easily by changing a few default values and deploying. Currently MongoDB and SQLite are supported. MongoDB support will be dropped in v0.2.0 so it is recommended to go with SQLite for new deployments. Postrgres support is planned, however for most loads you may find SQLite performant enough for most use cases.
|
||||
|
||||
|
||||
[Get Docker](https://docs.docker.com/get-docker/)
|
||||
@@ -14,7 +14,7 @@ Deployment with the Docker CLI can be done with `docker run` and specify the dat
|
||||
docker run \
|
||||
-e db_type='sqlite' \
|
||||
-p 9000:80 \
|
||||
-v `pwd`:'/app/data/' \
|
||||
-v `pwd`:'/app_data/' \
|
||||
hkotel/mealie:latest
|
||||
|
||||
```
|
||||
@@ -35,7 +35,7 @@ services:
|
||||
db_type: sqlite
|
||||
TZ: America/Anchorage
|
||||
volumes:
|
||||
- ./mealie/data/:/app/data
|
||||
- ./mealie/data/:/app_data
|
||||
|
||||
```
|
||||
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
| db_type | sqlite | The database type to be used. Current Options 'sqlite' |
|
||||
| mealie_port | 9000 | The port exposed by mealie. **do not change this if you're running in docker** If you'd like to use another port, map 9000 to another port of the host. |
|
||||
| api_docs | True | Turns on/off access to the API documentation locally. |
|
||||
| TZ | UTC | You should set your time zone accordingly so the date/time features work correctly |
|
||||
| TZ | | You should set your time zone accordingly so the date/time features work correctly |
|
||||
|
||||
|
||||
## Deployed as a Python Application
|
||||
|
||||
@@ -1 +1 @@
|
||||
VUE_APP_API_BASE_URL=http://localhost:9000
|
||||
VUE_APP_API_BASE_URL=http://10.10.10.12:9921
|
||||
502
frontend/package-lock.json
generated
502
frontend/package-lock.json
generated
@@ -4,37 +4,6 @@
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@adapttive/vue-markdown": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@adapttive/vue-markdown/-/vue-markdown-3.0.3.tgz",
|
||||
"integrity": "sha512-nopu1itoXJ5CwXe70dACSAq4n17nybBRZqXDu2ZyHC+vjVNjPiSp4kuvdBZHKMVHBEC7YakW4b9Mfkr2Wvybfw==",
|
||||
"requires": {
|
||||
"highlight.js": "^10.4.0",
|
||||
"markdown-it": "^12.0.3",
|
||||
"markdown-it-abbr": "^1.0.4",
|
||||
"markdown-it-deflist": "^2.1.0",
|
||||
"markdown-it-emoji": "^2.0.0",
|
||||
"markdown-it-external-preview": "^1.0.4",
|
||||
"markdown-it-footnote": "^3.0.2",
|
||||
"markdown-it-ins": "^3.0.0",
|
||||
"markdown-it-katex": "npm:@iktakahiro/markdown-it-katex@^4.0.1",
|
||||
"markdown-it-mark": "^3.0.0",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"markdown-it-toc-and-anchor": "^4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"markdown-it-katex": {
|
||||
"version": "npm:@iktakahiro/markdown-it-katex@4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@iktakahiro/markdown-it-katex/-/markdown-it-katex-4.0.1.tgz",
|
||||
"integrity": "sha512-kGFooO7fIOgY34PSG8ZNVsUlKhhNoqhzW2kq94TNGa8COzh73PO4KsEoPOsQVG1mEAe8tg7GqG0FoVao0aMHaw==",
|
||||
"requires": {
|
||||
"katex": "^0.12.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npm.taobao.org/@babel/code-frame/download/@babel/code-frame-7.10.4.tgz?cache=0&sync_timestamp=1593522948158&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fcode-frame%2Fdownload%2F%40babel%2Fcode-frame-7.10.4.tgz",
|
||||
@@ -1385,11 +1354,6 @@
|
||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
|
||||
"dev": true
|
||||
},
|
||||
"@smartweb/vue-flash-message": {
|
||||
"version": "0.6.10",
|
||||
"resolved": "https://registry.npmjs.org/@smartweb/vue-flash-message/-/vue-flash-message-0.6.10.tgz",
|
||||
"integrity": "sha512-ceDUUzXI6FDscev36kZQvc2BO+MayOt6uJ2HSh9zoOkfa0PVIhmaoB56InlTTsK7MmlSIvPJpRB+Habdx3MtNw=="
|
||||
},
|
||||
"@soda/friendly-errors-webpack-plugin": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
|
||||
@@ -1481,14 +1445,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/concat-stream": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz",
|
||||
"integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/connect": {
|
||||
"version": "3.4.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
||||
@@ -1531,14 +1487,6 @@
|
||||
"@types/range-parser": "*"
|
||||
}
|
||||
},
|
||||
"@types/form-data": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
|
||||
"integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/glob": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
|
||||
@@ -1596,7 +1544,8 @@
|
||||
"@types/node": {
|
||||
"version": "14.14.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz",
|
||||
"integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A=="
|
||||
"integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
"version": "2.4.0",
|
||||
@@ -1613,7 +1562,8 @@
|
||||
"@types/qs": {
|
||||
"version": "6.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
|
||||
"integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ=="
|
||||
"integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/range-parser": {
|
||||
"version": "1.2.3",
|
||||
@@ -2042,6 +1992,51 @@
|
||||
"unique-filename": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ssri": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.0.tgz",
|
||||
"integrity": "sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"figgy-pudding": "^3.5.1",
|
||||
"minipass": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -2058,6 +2053,16 @@
|
||||
"minipass": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
|
||||
@@ -2074,6 +2079,18 @@
|
||||
"terser": "^4.6.12",
|
||||
"webpack-sources": "^1.4.3"
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
|
||||
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,11 +2572,6 @@
|
||||
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
|
||||
"dev": true
|
||||
},
|
||||
"asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
@@ -2658,7 +2670,8 @@
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
|
||||
"dev": true
|
||||
},
|
||||
"atob": {
|
||||
"version": "2.1.2",
|
||||
@@ -2800,7 +2813,8 @@
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true
|
||||
},
|
||||
"batch": {
|
||||
"version": "0.6.1",
|
||||
@@ -2841,16 +2855,6 @@
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
@@ -2977,11 +2981,6 @@
|
||||
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
|
||||
"dev": true
|
||||
},
|
||||
"browser-or-node": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-1.3.0.tgz",
|
||||
"integrity": "sha512-0F2z/VSnLbmEeBcUrSuDH5l0HxTXdQQzLjkmBR4cYfvg1zJrKSlmIZFqyFR8oX0NrwPhy3c3HQ6i3OxMbew4Tg=="
|
||||
},
|
||||
"browserify-aes": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
|
||||
@@ -3101,7 +3100,8 @@
|
||||
"buffer-from": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
|
||||
"dev": true
|
||||
},
|
||||
"buffer-indexof": {
|
||||
"version": "1.1.1",
|
||||
@@ -3270,7 +3270,8 @@
|
||||
"caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
@@ -3553,17 +3554,6 @@
|
||||
"integrity": "sha1-ovSEN6LKqaIkNueUvwceyeYc7fY=",
|
||||
"dev": true
|
||||
},
|
||||
"clipboard": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz",
|
||||
"integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"good-listener": "^1.2.2",
|
||||
"select": "^1.1.2",
|
||||
"tiny-emitter": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"clipboardy": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",
|
||||
@@ -3724,6 +3714,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
@@ -3731,7 +3722,8 @@
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npm.taobao.org/commander/download/commander-2.20.3.tgz?cache=0&sync_timestamp=1605992628233&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcommander%2Fdownload%2Fcommander-2.20.3.tgz",
|
||||
"integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM="
|
||||
"integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=",
|
||||
"dev": true
|
||||
},
|
||||
"commondir": {
|
||||
"version": "1.0.1",
|
||||
@@ -3802,6 +3794,7 @@
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
@@ -4036,7 +4029,8 @@
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
|
||||
"dev": true
|
||||
},
|
||||
"cosmiconfig": {
|
||||
"version": "5.2.1",
|
||||
@@ -4637,13 +4631,8 @@
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"delegate": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
|
||||
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
|
||||
"optional": true
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||
"dev": true
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
@@ -4952,7 +4941,8 @@
|
||||
"entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
|
||||
"dev": true
|
||||
},
|
||||
"errno": {
|
||||
"version": "0.1.8",
|
||||
@@ -5633,13 +5623,6 @@
|
||||
"schema-utils": "^2.5.0"
|
||||
}
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"filesize": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
|
||||
@@ -5786,6 +5769,7 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
@@ -5908,11 +5892,6 @@
|
||||
"has-symbols": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"get-port": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
|
||||
"integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw="
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||
@@ -5988,15 +5967,6 @@
|
||||
"slash": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"good-listener": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
|
||||
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"delegate": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
|
||||
@@ -6149,7 +6119,8 @@
|
||||
"highlight.js": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.5.0.tgz",
|
||||
"integrity": "sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw=="
|
||||
"integrity": "sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==",
|
||||
"dev": true
|
||||
},
|
||||
"hmac-drbg": {
|
||||
"version": "1.0.1",
|
||||
@@ -6329,17 +6300,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"http-basic": {
|
||||
"version": "8.1.3",
|
||||
"resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz",
|
||||
"integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==",
|
||||
"requires": {
|
||||
"caseless": "^0.12.0",
|
||||
"concat-stream": "^1.6.2",
|
||||
"http-response-object": "^3.0.1",
|
||||
"parse-cache-control": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"http-deceiver": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
|
||||
@@ -6396,21 +6356,6 @@
|
||||
"micromatch": "^3.1.10"
|
||||
}
|
||||
},
|
||||
"http-response-object": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz",
|
||||
"integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==",
|
||||
"requires": {
|
||||
"@types/node": "^10.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "10.17.51",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.51.tgz",
|
||||
"integrity": "sha512-KANw+MkL626tq90l++hGelbl67irOJzGhUJk6a1Bt8QHOeh9tztJx+L0AqttraWKinmZn7Qi5lJZJzx45Gq0dg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
@@ -6455,7 +6400,8 @@
|
||||
"ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true
|
||||
},
|
||||
"iferr": {
|
||||
"version": "0.1.5",
|
||||
@@ -6555,7 +6501,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Finherits%2Fdownload%2Finherits-2.0.4.tgz",
|
||||
"integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w="
|
||||
"integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=",
|
||||
"dev": true
|
||||
},
|
||||
"inquirer": {
|
||||
"version": "7.3.3",
|
||||
@@ -7024,7 +6971,8 @@
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
||||
"dev": true
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
@@ -7222,14 +7170,6 @@
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"katex": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.12.0.tgz",
|
||||
"integrity": "sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==",
|
||||
"requires": {
|
||||
"commander": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
@@ -7277,14 +7217,6 @@
|
||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||
"dev": true
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz",
|
||||
"integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==",
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"loader-fs-cache": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz",
|
||||
@@ -7478,108 +7410,6 @@
|
||||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.4.tgz",
|
||||
"integrity": "sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~2.1.0",
|
||||
"linkify-it": "^3.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"markdown-it-abbr": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz",
|
||||
"integrity": "sha1-1mtTZFIcuz3Yqlna37ovtoZcj9g="
|
||||
},
|
||||
"markdown-it-deflist": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.1.0.tgz",
|
||||
"integrity": "sha512-3OuqoRUlSxJiuQYu0cWTLHNhhq2xtoSFqsZK8plANg91+RJQU1ziQ6lA2LzmFAEes18uPBsHZpcX6We5l76Nzg=="
|
||||
},
|
||||
"markdown-it-emoji": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-2.0.0.tgz",
|
||||
"integrity": "sha512-39j7/9vP/CPCKbEI44oV8yoPJTpvfeReTn/COgRhSpNrjWF3PfP/JUxxB0hxV6ynOY8KH8Y8aX9NMDdo6z+6YQ=="
|
||||
},
|
||||
"markdown-it-external-preview": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-external-preview/-/markdown-it-external-preview-1.0.4.tgz",
|
||||
"integrity": "sha512-kuhuUeL1vmunHdzzUKTSec3Xl30vBbeDu/fgnlPvvQIRcOnWlCK+6pT2ov9R5igaJ0oXS6GMiyDlE7QiL7bd8w==",
|
||||
"requires": {
|
||||
"browser-or-node": "^1.3.0",
|
||||
"buffer": "^6.0.3",
|
||||
"prismjs": "^1.22.0",
|
||||
"sync-request": "^6.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"markdown-it-footnote": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.2.tgz",
|
||||
"integrity": "sha512-JVW6fCmZWjvMdDQSbOT3nnOQtd9iAXmw7hTSh26+v42BnvXeVyGMDBm5b/EZocMed2MbCAHiTX632vY0FyGB8A=="
|
||||
},
|
||||
"markdown-it-ins": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-ins/-/markdown-it-ins-3.0.1.tgz",
|
||||
"integrity": "sha512-32SSfZqSzqyAmmQ4SHvhxbFqSzPDqsZgMHDwxqPzp+v+t8RsmqsBZRG+RfRQskJko9PfKC2/oxyOs4Yg/CfiRw=="
|
||||
},
|
||||
"markdown-it-mark": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz",
|
||||
"integrity": "sha512-HyxjAu6BRsdt6Xcv6TKVQnkz/E70TdGXEFHRYBGLncRE9lBFwDNLVtFojKxjJWgJ+5XxUwLaHXy+2sGBbDn+4A=="
|
||||
},
|
||||
"markdown-it-sub": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz",
|
||||
"integrity": "sha1-N1/WAm6ufdywEkl/ZBEZXqHjr+g="
|
||||
},
|
||||
"markdown-it-sup": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz",
|
||||
"integrity": "sha1-y5yf+RpSVawI8/09YyhuFd8KH8M="
|
||||
},
|
||||
"markdown-it-task-lists": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
|
||||
"integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="
|
||||
},
|
||||
"markdown-it-toc-and-anchor": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-toc-and-anchor/-/markdown-it-toc-and-anchor-4.2.0.tgz",
|
||||
"integrity": "sha512-DusSbKtg8CwZ92ztN7bOojDpP4h0+w7BVOPuA3PHDIaabMsERYpwsazLYSP/UlKedoQjOz21mwlai36TQ04EpA==",
|
||||
"requires": {
|
||||
"clone": "^2.1.0",
|
||||
"uslug": "^1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
|
||||
}
|
||||
}
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||
@@ -7597,11 +7427,6 @@
|
||||
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
|
||||
"dev": true
|
||||
},
|
||||
"mdurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -7707,12 +7532,14 @@
|
||||
"mime-db": {
|
||||
"version": "1.45.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
|
||||
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w=="
|
||||
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
|
||||
"dev": true
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.28",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
|
||||
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "1.45.0"
|
||||
}
|
||||
@@ -7937,13 +7764,6 @@
|
||||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
||||
@@ -8486,11 +8306,6 @@
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"parse-cache-control": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
|
||||
"integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104="
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
|
||||
@@ -9350,14 +9165,6 @@
|
||||
"renderkid": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"prismjs": {
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz",
|
||||
"integrity": "sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==",
|
||||
"requires": {
|
||||
"clipboard": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
@@ -9367,7 +9174,8 @@
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true
|
||||
},
|
||||
"progress": {
|
||||
"version": "2.0.3",
|
||||
@@ -9375,14 +9183,6 @@
|
||||
"integrity": "sha1-foz42PW48jnBvGi+tOt4Vn1XLvg=",
|
||||
"dev": true
|
||||
},
|
||||
"promise": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz",
|
||||
"integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==",
|
||||
"requires": {
|
||||
"asap": "~2.0.6"
|
||||
}
|
||||
},
|
||||
"promise-inflight": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
||||
@@ -9569,6 +9369,7 @@
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -9909,7 +9710,8 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
},
|
||||
"safe-regex": {
|
||||
"version": "1.1.0",
|
||||
@@ -9973,12 +9775,6 @@
|
||||
"ajv-keywords": "^3.5.2"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
|
||||
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=",
|
||||
"optional": true
|
||||
},
|
||||
"select-hose": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||
@@ -10732,6 +10528,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -10837,24 +10634,6 @@
|
||||
"util.promisify": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"sync-request": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz",
|
||||
"integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==",
|
||||
"requires": {
|
||||
"http-response-object": "^3.0.1",
|
||||
"sync-rpc": "^1.2.1",
|
||||
"then-request": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"sync-rpc": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz",
|
||||
"integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==",
|
||||
"requires": {
|
||||
"get-port": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npm.taobao.org/table/download/table-5.4.6.tgz?cache=0&sync_timestamp=1605825218994&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftable%2Fdownload%2Ftable-5.4.6.tgz",
|
||||
@@ -10998,31 +10777,6 @@
|
||||
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
||||
"dev": true
|
||||
},
|
||||
"then-request": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz",
|
||||
"integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==",
|
||||
"requires": {
|
||||
"@types/concat-stream": "^1.6.0",
|
||||
"@types/form-data": "0.0.33",
|
||||
"@types/node": "^8.0.0",
|
||||
"@types/qs": "^6.2.31",
|
||||
"caseless": "~0.12.0",
|
||||
"concat-stream": "^1.6.0",
|
||||
"form-data": "^2.2.0",
|
||||
"http-basic": "^8.1.1",
|
||||
"http-response-object": "^3.0.1",
|
||||
"promise": "^8.0.0",
|
||||
"qs": "^6.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "8.10.66",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz",
|
||||
"integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
@@ -11089,12 +10843,6 @@
|
||||
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
|
||||
"dev": true
|
||||
},
|
||||
"tiny-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
|
||||
"optional": true
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npm.taobao.org/tmp/download/tmp-0.0.33.tgz",
|
||||
@@ -11247,12 +10995,8 @@
|
||||
"typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"uc.micro": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.10",
|
||||
@@ -11354,11 +11098,6 @@
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"dev": true
|
||||
},
|
||||
"unorm": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
|
||||
"integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA=="
|
||||
},
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@@ -11482,14 +11221,6 @@
|
||||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
|
||||
"dev": true
|
||||
},
|
||||
"uslug": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/uslug/-/uslug-1.0.4.tgz",
|
||||
"integrity": "sha1-uaIvCRTgqGFAYz2swwLl9PpFBnc=",
|
||||
"requires": {
|
||||
"unorm": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"util": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
|
||||
@@ -11510,7 +11241,8 @@
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
||||
"dev": true
|
||||
},
|
||||
"util.promisify": {
|
||||
"version": "1.0.1",
|
||||
@@ -12103,11 +11835,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
|
||||
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"nan": "^2.12.1"
|
||||
}
|
||||
"optional": true
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "3.1.0",
|
||||
@@ -12409,11 +12137,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
|
||||
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"nan": "^2.12.1"
|
||||
}
|
||||
"optional": true
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "3.1.0",
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adapttive/vue-markdown": "^3.0.3",
|
||||
"@smartweb/vue-flash-message": "^0.6.10",
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.8.2",
|
||||
"fuse.js": "^6.4.6",
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-app-bar clipped-left dense app color="primary" dark class="d-print-none">
|
||||
<router-link to="/">
|
||||
<v-btn icon>
|
||||
<v-icon size="40"> mdi-silverware-variant </v-icon>
|
||||
</v-btn>
|
||||
</router-link>
|
||||
|
||||
<v-btn @click="$router.push('/')" icon>
|
||||
<v-icon size="40"> mdi-silverware-variant </v-icon>
|
||||
</v-btn>
|
||||
<div btn class="pl-2">
|
||||
<v-toolbar-title style="cursor: pointer" @click="$router.push('/')"
|
||||
>Mealie
|
||||
</v-toolbar-title>
|
||||
<v-toolbar-title @click="$router.push('/')">Mealie</v-toolbar-title>
|
||||
</div>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-expand-x-transition>
|
||||
<SearchBar
|
||||
ref="mainSearchBar"
|
||||
class="mt-7"
|
||||
v-if="search"
|
||||
:show-results="true"
|
||||
@@ -35,7 +29,6 @@
|
||||
<SnackBar />
|
||||
<router-view></router-view>
|
||||
</v-container>
|
||||
<FlashMessage :position="'right bottom'"></FlashMessage>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
@@ -61,13 +54,6 @@ export default {
|
||||
this.search = false;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
window.addEventListener("keyup", e => {
|
||||
if (e.key == "/") {
|
||||
this.search = !this.search;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$store.dispatch("initTheme");
|
||||
@@ -108,34 +94,5 @@ export default {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.notify-info-color {
|
||||
border: 1px, solid, var(--v-info-base) !important;
|
||||
border-left: 3px, solid, var(--v-info-base) !important;
|
||||
background-color: var(--v-info-base) !important;
|
||||
}
|
||||
|
||||
.notify-warning-color {
|
||||
border: 1px, solid, var(--v-warning-base) !important;
|
||||
border-left: 3px, solid, var(--v-warning-base) !important;
|
||||
background-color: var(--v-warning-base) !important;
|
||||
}
|
||||
|
||||
.notify-error-color {
|
||||
border: 1px, solid, var(--v-error-base) !important;
|
||||
border-left: 3px, solid, var(--v-error-base) !important;
|
||||
background-color: var(--v-error-base) !important;
|
||||
}
|
||||
|
||||
.notify-success-color {
|
||||
border: 1px, solid, var(--v-success-base) !important;
|
||||
border-left: 3px, solid, var(--v-success-base) !important;
|
||||
background-color: var(--v-success-base) !important;
|
||||
}
|
||||
|
||||
.notify-base {
|
||||
color: white !important;
|
||||
margin-right: 60px;
|
||||
margin-bottom: -5px;
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,9 +6,8 @@ import themes from "./api/themes";
|
||||
import migration from "./api/migration";
|
||||
import myUtils from "./api/upload";
|
||||
import category from "./api/category";
|
||||
import meta from "./api/meta";
|
||||
|
||||
// import api from "@/api";
|
||||
// import api from "../api";
|
||||
|
||||
export default {
|
||||
recipes: recipe,
|
||||
@@ -19,5 +18,4 @@ export default {
|
||||
migrations: migration,
|
||||
utils: myUtils,
|
||||
categories: category,
|
||||
meta: meta,
|
||||
};
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
const baseURL = "/api/";
|
||||
import axios from "axios";
|
||||
import utils from "@/utils";
|
||||
import store from "../store/store";
|
||||
|
||||
// look for data.snackbar in response
|
||||
function processResponse(response) {
|
||||
try {
|
||||
utils.notify.show(response.data.snackbar.text, response.data.snackbar.type);
|
||||
store.commit("setSnackBar", {
|
||||
text: response.data.snackbar.text,
|
||||
type: response.data.snackbar.type,
|
||||
});
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const apiReq = {
|
||||
post: async function(url, data) {
|
||||
let response = await axios.post(url, data).catch(function(error) {
|
||||
post: async function (url, data) {
|
||||
let response = await axios.post(url, data).catch(function (error) {
|
||||
if (error.response) {
|
||||
processResponse(error.response);
|
||||
return error.response;
|
||||
@@ -24,19 +27,8 @@ const apiReq = {
|
||||
return response;
|
||||
},
|
||||
|
||||
put: async function(url, data) {
|
||||
let response = await axios.put(url, data).catch(function(error) {
|
||||
if (error.response) {
|
||||
processResponse(error.response);
|
||||
return response;
|
||||
} else return;
|
||||
});
|
||||
processResponse(response);
|
||||
return response;
|
||||
},
|
||||
|
||||
get: async function(url, data) {
|
||||
let response = await axios.get(url, data).catch(function(error) {
|
||||
put: async function (url, data) {
|
||||
let response = await axios.put(url, data).catch(function (error) {
|
||||
if (error.response) {
|
||||
processResponse(error.response);
|
||||
return response;
|
||||
@@ -46,8 +38,19 @@ const apiReq = {
|
||||
return response;
|
||||
},
|
||||
|
||||
delete: async function(url, data) {
|
||||
let response = await axios.delete(url, data).catch(function(error) {
|
||||
get: async function (url, data) {
|
||||
let response = await axios.get(url, data).catch(function (error) {
|
||||
if (error.response) {
|
||||
processResponse(error.response);
|
||||
return response;
|
||||
} else return;
|
||||
});
|
||||
// processResponse(response);
|
||||
return response;
|
||||
},
|
||||
|
||||
delete: async function (url, data) {
|
||||
let response = await axios.delete(url, data).catch(function (error) {
|
||||
if (error.response) {
|
||||
processResponse(error.response);
|
||||
return response;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
|
||||
const prefix = baseURL + "debug";
|
||||
|
||||
const debugURLs = {
|
||||
version: `${prefix}/version`,
|
||||
};
|
||||
|
||||
export default {
|
||||
async get_version() {
|
||||
let response = await apiReq.get(debugURLs.version);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -1,14 +1,14 @@
|
||||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
|
||||
const prefix = baseURL + "themes";
|
||||
const prefix = baseURL + "themes/";
|
||||
|
||||
const settingsURLs = {
|
||||
allThemes: `${baseURL}themes`,
|
||||
specificTheme: themeName => `${prefix}/${themeName}`,
|
||||
createTheme: `${prefix}/create`,
|
||||
updateTheme: themeName => `${prefix}/${themeName}`,
|
||||
deleteTheme: themeName => `${prefix}/${themeName}`,
|
||||
specificTheme: (themeName) => `${prefix}themes/${themeName}`,
|
||||
createTheme: `${prefix}themes/create`,
|
||||
updateTheme: (themeName) => `${prefix}themes/${themeName}`,
|
||||
deleteTheme: (themeName) => `${prefix}themes/${themeName}`,
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -33,7 +33,6 @@ export default {
|
||||
colors: colors,
|
||||
};
|
||||
let response = await apiReq.put(settingsURLs.updateTheme(themeName), body);
|
||||
console.log(response.data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { apiReq } from "./api-utils";
|
||||
|
||||
export default {
|
||||
// import api from "@/api";
|
||||
// import api from "../api";
|
||||
async uploadFile(url, fileObject) {
|
||||
let response = await apiReq.post(url, fileObject, {
|
||||
headers: {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "@/utils";
|
||||
import utils from "../../utils";
|
||||
import SearchDialog from "../UI/SearchDialog";
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import utils from "@/utils";
|
||||
import api from "../../api";
|
||||
import utils from "../../utils";
|
||||
import MealPlanCard from "./MealPlanCard";
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -85,8 +85,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import utils from "@/utils";
|
||||
import api from "../../api";
|
||||
import utils from "../../utils";
|
||||
import MealPlanCard from "./MealPlanCard";
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -12,8 +12,28 @@
|
||||
></v-file-input>
|
||||
</v-col>
|
||||
<v-col cols="3"></v-col>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
label="Total Time"
|
||||
v-model="value.totalTime"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col
|
||||
><v-text-field
|
||||
label="Prep Time"
|
||||
v-model="value.prepTime"
|
||||
></v-text-field
|
||||
></v-col>
|
||||
<v-col
|
||||
><v-text-field
|
||||
label="Cook Time / Perform Time"
|
||||
v-model="value.performTime"
|
||||
></v-text-field
|
||||
></v-col>
|
||||
</v-row>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
:label="$t('recipe.total-time')"
|
||||
@@ -41,23 +61,21 @@
|
||||
>
|
||||
</v-text-field>
|
||||
<v-textarea
|
||||
auto-grow
|
||||
min-height="100"
|
||||
height="100"
|
||||
:label="$t('recipe.description')"
|
||||
v-model="value.description"
|
||||
>
|
||||
</v-textarea>
|
||||
<div class="my-2"></div>
|
||||
<v-row dense disabled>
|
||||
<v-col sm="4">
|
||||
<v-col sm="5">
|
||||
<v-text-field
|
||||
:label="$t('recipe.servings')"
|
||||
v-model="value.recipeYield"
|
||||
class="rounded-sm"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col></v-col>
|
||||
<v-rating
|
||||
class="mr-2 align-end"
|
||||
color="secondary darken-1"
|
||||
@@ -188,7 +206,6 @@
|
||||
</v-row>
|
||||
|
||||
<v-textarea
|
||||
auto-grow
|
||||
:label="$t('recipe.note')"
|
||||
v-model="value.notes[index]['text']"
|
||||
>
|
||||
@@ -221,18 +238,17 @@
|
||||
elevation="0"
|
||||
@click="removeStep(index)"
|
||||
>
|
||||
<v-icon color="error">mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
{{ $t("recipe.step-index", { step: index + 1 }) }}
|
||||
</v-card-title>
|
||||
<v-icon color="error">mdi-delete</v-icon> </v-btn
|
||||
>{{
|
||||
$t("recipe.step-index", { step: index + 1 })
|
||||
}}</v-card-title
|
||||
>
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
auto-grow
|
||||
dense
|
||||
v-model="value.recipeInstructions[index]['text']"
|
||||
:key="generateKey('instructions', index)"
|
||||
>
|
||||
</v-textarea>
|
||||
></v-textarea>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
@@ -254,8 +270,8 @@
|
||||
|
||||
<script>
|
||||
import draggable from "vuedraggable";
|
||||
import api from "@/api";
|
||||
import utils from "@/utils";
|
||||
import api from "../../../api";
|
||||
import utils from "../../../utils";
|
||||
import BulkAdd from "./BulkAdd";
|
||||
import ExtrasEditor from "./ExtrasEditor";
|
||||
export default {
|
||||
@@ -272,8 +288,8 @@ export default {
|
||||
drag: false,
|
||||
fileObject: null,
|
||||
rules: {
|
||||
required: v => !!v || this.$i18n.t("recipe.key-name-required"),
|
||||
whiteSpace: v =>
|
||||
required: (v) => !!v || this.$i18n.t("recipe.key-name-required"),
|
||||
whiteSpace: (v) =>
|
||||
!v ||
|
||||
v.split(" ").length <= 1 ||
|
||||
this.$i18n.t("recipe.no-white-space-allowed"),
|
||||
@@ -290,7 +306,7 @@ export default {
|
||||
methods: {
|
||||
async getCategories() {
|
||||
let response = await api.categories.get_all();
|
||||
this.categories = response.map(cat => cat.name);
|
||||
this.categories = response.map((cat) => cat.name);
|
||||
},
|
||||
uploadImage() {
|
||||
this.$emit("upload", this.fileObject);
|
||||
@@ -337,7 +353,7 @@ export default {
|
||||
|
||||
appendSteps(steps) {
|
||||
let processSteps = [];
|
||||
steps.forEach(element => {
|
||||
steps.forEach((element) => {
|
||||
processSteps.push({ text: element });
|
||||
});
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "@/utils";
|
||||
import utils from "../../utils";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
||||
183
frontend/src/components/Recipe/RecipeViewer.vue
Normal file
183
frontend/src/components/Recipe/RecipeViewer.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-title class="headline">
|
||||
{{ name }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
{{ description }}
|
||||
<div class="my-2"></div>
|
||||
<v-row dense disabled>
|
||||
<v-col>
|
||||
<v-btn
|
||||
v-if="yields"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ yields }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-rating
|
||||
class="mr-2 align-end static"
|
||||
color="secondary darken-1"
|
||||
background-color="secondary lighten-3"
|
||||
length="5"
|
||||
:value="rating"
|
||||
></v-rating>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="4" lg="4">
|
||||
<h2 class="mb-4">{{$t('recipe.ingredients')}}</h2>
|
||||
<div
|
||||
v-for="(ingredient, index) in ingredients"
|
||||
:key="generateKey('ingredient', index)"
|
||||
>
|
||||
<v-checkbox
|
||||
hide-details
|
||||
class="ingredients"
|
||||
:label="ingredient"
|
||||
color="secondary"
|
||||
>
|
||||
</v-checkbox>
|
||||
</div>
|
||||
|
||||
<div v-if="categories[0]">
|
||||
<h2 class="mt-4">{{$t('recipe.categories')}}</h2>
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
color="accent"
|
||||
dark
|
||||
v-for="category in categories"
|
||||
:key="category"
|
||||
>
|
||||
{{ category }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div v-if="tags[0]">
|
||||
<h2 class="mt-4">{{$t('recipe.tags')}}</h2>
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
color="accent"
|
||||
dark
|
||||
v-for="tag in tags"
|
||||
:key="tag"
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<h2 v-if="notes[0]" class="my-4">{{$t('recipe.notes')}}</h2>
|
||||
<v-card
|
||||
class="mt-1"
|
||||
v-for="(note, index) in notes"
|
||||
:key="generateKey('note', index)"
|
||||
>
|
||||
<v-card-title> {{ note.title }}</v-card-title>
|
||||
<v-card-text>
|
||||
{{ note.text }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-divider class="my-divider" :vertical="true"></v-divider>
|
||||
|
||||
<v-col cols="12" sm="12" md="8" lg="8">
|
||||
<h2 class="mb-4">{{$t('recipe.instructions')}}</h2>
|
||||
<v-hover
|
||||
v-for="(step, index) in instructions"
|
||||
:key="generateKey('step', index)"
|
||||
v-slot="{ hover }"
|
||||
>
|
||||
<v-card
|
||||
class="ma-1"
|
||||
:class="[{ 'on-hover': hover }, isDisabled(index)]"
|
||||
:elevation="hover ? 12 : 2"
|
||||
@click="toggleDisabled(index)"
|
||||
>
|
||||
<v-card-title>{{ $t('recipe.step-index', {step: index + 1}) }}</v-card-title>
|
||||
<v-card-text>{{ step.text }}</v-card-text>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col></v-col>
|
||||
|
||||
<v-btn
|
||||
v-if="orgURL"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
:href="orgURL"
|
||||
color="secondary darken-1"
|
||||
target="_blank"
|
||||
class="rounded-sm mr-4"
|
||||
>
|
||||
{{$t('recipe.original-url')}}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "../../utils";
|
||||
export default {
|
||||
props: {
|
||||
name: String,
|
||||
description: String,
|
||||
ingredients: Array,
|
||||
instructions: Array,
|
||||
categories: Array,
|
||||
tags: Array,
|
||||
notes: Array,
|
||||
rating: Number,
|
||||
yields: String,
|
||||
orgURL: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
disabledSteps: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleDisabled(stepIndex) {
|
||||
if (this.disabledSteps.includes(stepIndex)) {
|
||||
let index = this.disabledSteps.indexOf(stepIndex);
|
||||
if (index !== -1) {
|
||||
this.disabledSteps.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
this.disabledSteps.push(stepIndex);
|
||||
}
|
||||
},
|
||||
isDisabled(stepIndex) {
|
||||
if (this.disabledSteps.includes(stepIndex)) {
|
||||
return "disabled-card";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
},
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.static {
|
||||
pointer-events: none;
|
||||
}
|
||||
.my-divider {
|
||||
margin: 0 -1px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
|
||||
<div
|
||||
v-for="(ingredient, index) in ingredients"
|
||||
:key="generateKey('ingredient', index)"
|
||||
>
|
||||
<v-checkbox
|
||||
hide-details
|
||||
class="ingredients"
|
||||
:label="ingredient"
|
||||
color="secondary"
|
||||
>
|
||||
</v-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "@/utils";
|
||||
export default {
|
||||
props: {
|
||||
ingredients: Array,
|
||||
},
|
||||
methods: {
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 v-if="notes[0]" class="my-4">{{ $t("recipe.notes") }}</h2>
|
||||
<v-card
|
||||
class="mt-1"
|
||||
v-for="(note, index) in notes"
|
||||
:key="generateKey('note', index)"
|
||||
>
|
||||
<v-card-title> {{ note.title }}</v-card-title>
|
||||
<v-card-text>
|
||||
<vue-markdown :source="note.text"> </vue-markdown>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import utils from "@/utils";
|
||||
export default {
|
||||
props: {
|
||||
notes: Array,
|
||||
},
|
||||
components: {
|
||||
VueMarkdown,
|
||||
},
|
||||
methods: {
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<div v-if="items[0]">
|
||||
<h2 class="mt-4">{{ title }}</h2>
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
color="accent"
|
||||
dark
|
||||
v-for="category in items"
|
||||
:key="category"
|
||||
>
|
||||
{{ category }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: Array,
|
||||
title: String,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,67 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
|
||||
<v-hover
|
||||
v-for="(step, index) in steps"
|
||||
:key="generateKey('step', index)"
|
||||
v-slot="{ hover }"
|
||||
>
|
||||
<v-card
|
||||
class="ma-1"
|
||||
:class="[{ 'on-hover': hover }, isDisabled(index)]"
|
||||
:elevation="hover ? 12 : 2"
|
||||
@click="toggleDisabled(index)"
|
||||
>
|
||||
<v-card-title>{{
|
||||
$t("recipe.step-index", { step: index + 1 })
|
||||
}}</v-card-title>
|
||||
<v-card-text>
|
||||
<vue-markdown :source="step.text"> </vue-markdown>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import utils from "@/utils";
|
||||
export default {
|
||||
props: {
|
||||
steps: Array,
|
||||
},
|
||||
components: {
|
||||
VueMarkdown,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
disabledSteps: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleDisabled(stepIndex) {
|
||||
if (this.disabledSteps.includes(stepIndex)) {
|
||||
let index = this.disabledSteps.indexOf(stepIndex);
|
||||
if (index !== -1) {
|
||||
this.disabledSteps.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
this.disabledSteps.push(stepIndex);
|
||||
}
|
||||
},
|
||||
isDisabled(stepIndex) {
|
||||
if (this.disabledSteps.includes(stepIndex)) {
|
||||
return "disabled-card";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
},
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,130 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-title class="headline">
|
||||
{{ name }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<vue-markdown :source="description"> </vue-markdown>
|
||||
<v-row dense disabled>
|
||||
<v-col>
|
||||
<v-btn
|
||||
v-if="yields"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ yields }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-rating
|
||||
class="mr-2 align-end static"
|
||||
color="secondary darken-1"
|
||||
background-color="secondary lighten-3"
|
||||
length="5"
|
||||
:value="rating"
|
||||
></v-rating>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="4" lg="4">
|
||||
<Ingredients :ingredients="ingredients" />
|
||||
<div v-if="medium">
|
||||
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
|
||||
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
|
||||
<Notes :notes="notes" />
|
||||
</div>
|
||||
</v-col>
|
||||
<v-divider
|
||||
v-if="medium"
|
||||
class="my-divider"
|
||||
:vertical="true"
|
||||
></v-divider>
|
||||
|
||||
<v-col cols="12" sm="12" md="8" lg="8">
|
||||
<Steps :steps="instructions" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div v-if="!medium">
|
||||
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
|
||||
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
|
||||
<Notes :notes="notes" />
|
||||
</div>
|
||||
<v-row class="mt-2 mb-1">
|
||||
<v-col></v-col>
|
||||
<v-btn
|
||||
v-if="orgURL"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
:href="orgURL"
|
||||
color="secondary darken-1"
|
||||
target="_blank"
|
||||
class="rounded-sm mr-4"
|
||||
>
|
||||
{{ $t("recipe.original-url") }}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import utils from "@/utils";
|
||||
import RecipeChips from "./RecipeChips";
|
||||
import Steps from "./Steps";
|
||||
import Notes from "./Notes";
|
||||
import Ingredients from "./Ingredients";
|
||||
export default {
|
||||
components: {
|
||||
VueMarkdown,
|
||||
RecipeChips,
|
||||
Steps,
|
||||
Notes,
|
||||
Ingredients,
|
||||
},
|
||||
props: {
|
||||
name: String,
|
||||
description: String,
|
||||
ingredients: Array,
|
||||
instructions: Array,
|
||||
categories: Array,
|
||||
tags: Array,
|
||||
notes: Array,
|
||||
rating: Number,
|
||||
yields: String,
|
||||
orgURL: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
disabledSteps: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
medium() {
|
||||
return this.$vuetify.breakpoint.mdAndUp;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.static {
|
||||
pointer-events: none;
|
||||
}
|
||||
.my-divider {
|
||||
margin: 0 -1px;
|
||||
}
|
||||
</style>
|
||||
@@ -38,8 +38,8 @@
|
||||
|
||||
<script>
|
||||
import ImportDialog from "./ImportDialog";
|
||||
import api from "@/api";
|
||||
import utils from "@/utils";
|
||||
import api from "../../../api";
|
||||
import utils from "../../../utils";
|
||||
export default {
|
||||
props: {
|
||||
backups: Array,
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
|
||||
<script>
|
||||
import ImportDialog from "./ImportDialog";
|
||||
import api from "@/api";
|
||||
import utils from "@/utils";
|
||||
import api from "../../../api";
|
||||
import utils from "../../../utils";
|
||||
export default {
|
||||
props: {
|
||||
backups: Array,
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import api from "../../../api";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
methods: {
|
||||
async getAvailableBackups() {
|
||||
let response = await api.backups.requestAvailable();
|
||||
response.templates.forEach(element => {
|
||||
response.templates.forEach((element) => {
|
||||
this.availableTemplates.push(element);
|
||||
});
|
||||
},
|
||||
@@ -101,6 +101,7 @@ export default {
|
||||
templates: this.selectedTemplates,
|
||||
};
|
||||
|
||||
|
||||
await api.backups.create(data);
|
||||
this.loading = false;
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import api from "../../../api";
|
||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
import UploadBtn from "../../UI/UploadBtn";
|
||||
import AvailableBackupCard from "./AvailableBackupCard";
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import api from "../../../api";
|
||||
import draggable from "vuedraggable";
|
||||
|
||||
export default {
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
|
||||
<script>
|
||||
import UploadBtn from "../../UI/UploadBtn";
|
||||
import utils from "@/utils";
|
||||
import api from "@/api";
|
||||
import utils from "../../../utils";
|
||||
import api from "../../../api";
|
||||
export default {
|
||||
props: {
|
||||
folder: String,
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<script>
|
||||
import MigrationCard from "./MigrationCard";
|
||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
import api from "@/api";
|
||||
import api from "../../../api";
|
||||
export default {
|
||||
components: {
|
||||
MigrationCard,
|
||||
@@ -78,7 +78,7 @@ export default {
|
||||
},
|
||||
async getAvailableMigrations() {
|
||||
let response = await api.migrations.getMigrations();
|
||||
response.forEach(element => {
|
||||
response.forEach((element) => {
|
||||
if (element.type === "nextcloud") {
|
||||
this.migrations.nextcloud.availableImports = element.files;
|
||||
} else if (element.type === "chowdown") {
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
return-object
|
||||
v-model="selectedTheme"
|
||||
@change="themeSelected"
|
||||
:rules="[v => !!v || $t('settings.theme.theme-is-required')]"
|
||||
:rules="[(v) => !!v || $t('settings.theme.theme-is-required')]"
|
||||
required
|
||||
>
|
||||
</v-select>
|
||||
@@ -136,7 +136,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import api from "../../../api";
|
||||
import ColorPickerDialog from "./ColorPickerDialog";
|
||||
import NewThemeDialog from "./NewThemeDialog";
|
||||
import Confirmation from "../../UI/Confirmation";
|
||||
@@ -186,7 +186,7 @@ export default {
|
||||
//Change to default if deleting current theme.
|
||||
if (
|
||||
!this.availableThemes.some(
|
||||
theme => theme.name === this.selectedTheme.name
|
||||
(theme) => theme.name === this.selectedTheme.name
|
||||
)
|
||||
) {
|
||||
await this.$store.dispatch("resetTheme");
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import api from "../../../api";
|
||||
import TimePickerDialog from "./TimePickerDialog";
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import api from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
||||
@@ -44,14 +44,14 @@
|
||||
color="primary"
|
||||
block="block"
|
||||
type="submit"
|
||||
>{{ $t("login.sign-in") }}</v-btn
|
||||
>{{$t('login.sign-in')}}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
v-else
|
||||
block="block"
|
||||
type="submit"
|
||||
@click.prevent="options.isLoggingIn = true"
|
||||
>{{ $t("login.sign-up") }}</v-btn
|
||||
>{{$t('login.sign-up')}}</v-btn
|
||||
>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
@@ -72,7 +72,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import api from "../../api";
|
||||
export default {
|
||||
props: {},
|
||||
data() {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "@/utils";
|
||||
import utils from "../../utils";
|
||||
export default {
|
||||
props: {
|
||||
name: String,
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
hide-no-data
|
||||
cache-items
|
||||
solo
|
||||
autofocus
|
||||
auto-select-first
|
||||
>
|
||||
<template
|
||||
v-if="showResults"
|
||||
@@ -45,7 +43,7 @@
|
||||
|
||||
<script>
|
||||
import Fuse from "fuse.js";
|
||||
import utils from "@/utils";
|
||||
import utils from "../../utils";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import api from "../../api";
|
||||
export default {
|
||||
props: {
|
||||
url: String,
|
||||
|
||||
@@ -5,9 +5,7 @@ import store from "./store/store";
|
||||
import VueRouter from "vue-router";
|
||||
import { routes } from "./routes";
|
||||
import i18n from "./i18n";
|
||||
import FlashMessage from "@smartweb/vue-flash-message";
|
||||
|
||||
Vue.use(FlashMessage);
|
||||
Vue.config.productionTip = false;
|
||||
Vue.use(VueRouter);
|
||||
|
||||
@@ -16,16 +14,16 @@ const router = new VueRouter({
|
||||
mode: process.env.NODE_ENV === "production" ? "history" : "hash",
|
||||
});
|
||||
|
||||
const vueApp = new Vue({
|
||||
new Vue({
|
||||
vuetify,
|
||||
store,
|
||||
router,
|
||||
i18n,
|
||||
render: h => h(App),
|
||||
render: (h) => h(App),
|
||||
}).$mount("#app");
|
||||
|
||||
// Truncate
|
||||
let truncate = function(text, length, clamp) {
|
||||
let truncate = function (text, length, clamp) {
|
||||
clamp = clamp || "...";
|
||||
let node = document.createElement("div");
|
||||
node.innerHTML = text;
|
||||
@@ -33,12 +31,11 @@ let truncate = function(text, length, clamp) {
|
||||
return content.length > length ? content.slice(0, length) + clamp : content;
|
||||
};
|
||||
|
||||
let titleCase = function(value) {
|
||||
return value.replace(/(?:^|\s|-)\S/g, x => x.toUpperCase());
|
||||
let titleCase = function (value) {
|
||||
return value.replace(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
|
||||
};
|
||||
|
||||
Vue.filter("truncate", truncate);
|
||||
Vue.filter("titleCase", titleCase);
|
||||
|
||||
export { vueApp };
|
||||
export { router };
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import api from "../api";
|
||||
import CardSection from "../components/UI/CardSection";
|
||||
import CategorySidebar from "../components/UI/CategorySidebar";
|
||||
export default {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import api from "../api";
|
||||
import CardSection from "../components/UI/CardSection";
|
||||
import CategorySidebar from "../components/UI/CategorySidebar";
|
||||
export default {
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async buildPage() {
|
||||
this.homeCategories.forEach(async element => {
|
||||
this.homeCategories.forEach(async (element) => {
|
||||
let recipes = await this.getRecipeByCategory(element.slug);
|
||||
recipes.position = element.position;
|
||||
this.recipeByCategory.push(recipes);
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import utils from "@/utils";
|
||||
import api from "../api";
|
||||
import utils from "../utils";
|
||||
import NewMeal from "../components/MealPlan/MealPlanNew";
|
||||
import EditPlan from "../components/MealPlan/MealPlanEditor";
|
||||
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import utils from "@/utils";
|
||||
import api from "../api";
|
||||
import utils from "../utils";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import api from "../api";
|
||||
|
||||
import RecipeEditor from "../components/Recipe/RecipeEditor";
|
||||
import VJsoneditor from "v-jsoneditor";
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import utils from "@/utils";
|
||||
import api from "../api";
|
||||
import utils from "../utils";
|
||||
import VJsoneditor from "v-jsoneditor";
|
||||
import RecipeViewer from "../components/Recipe/RecipeViewer";
|
||||
import RecipeEditor from "../components/Recipe/RecipeEditor";
|
||||
@@ -107,7 +107,7 @@ export default {
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route: function() {
|
||||
$route: function () {
|
||||
this.getRecipeDetails();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -44,7 +44,6 @@ import General from "../components/Settings/General";
|
||||
import Webhooks from "../components/Settings/Webhook";
|
||||
import Theme from "../components/Settings/Theme";
|
||||
import Migration from "../components/Settings/Migration";
|
||||
import api from "@/api";
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
@@ -58,13 +57,11 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
latestVersion: null,
|
||||
version: null,
|
||||
version: "v0.1.0",
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
mounted() {
|
||||
this.getVersion();
|
||||
let versionData = await api.meta.get_version();
|
||||
this.version = versionData.version;
|
||||
},
|
||||
computed: {
|
||||
newVersion() {
|
||||
|
||||
@@ -6,8 +6,6 @@ Vue.use(Vuetify);
|
||||
const vuetify = new Vuetify({
|
||||
theme: {
|
||||
dark: false,
|
||||
options: { customProperties: true },
|
||||
|
||||
themes: {
|
||||
light: {
|
||||
primary: "#E58325",
|
||||
|
||||
@@ -8,7 +8,7 @@ import AllRecipesPage from "./pages/AllRecipesPage";
|
||||
import CategoryPage from "./pages/CategoryPage";
|
||||
import MeaplPlanPage from "./pages/MealPlanPage";
|
||||
import MealPlanThisWeekPage from "./pages/MealPlanThisWeekPage";
|
||||
import api from "@/api";
|
||||
import api from "./api";
|
||||
|
||||
export const routes = [
|
||||
{ path: "/", component: HomePage },
|
||||
@@ -24,7 +24,7 @@ export const routes = [
|
||||
{
|
||||
path: "/meal-plan/today",
|
||||
beforeEnter: async (_to, _from, next) => {
|
||||
await todaysMealRoute().then(redirect => {
|
||||
await todaysMealRoute().then((redirect) => {
|
||||
next(redirect);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import api from "@/api";
|
||||
import api from "../../api";
|
||||
|
||||
const state = {
|
||||
showRecent: true,
|
||||
@@ -30,10 +30,10 @@ const actions = {
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getShowRecent: state => state.showRecent,
|
||||
getShowLimit: state => state.showLimit,
|
||||
getCategories: state => state.categories,
|
||||
getHomeCategories: state => state.homeCategories,
|
||||
getShowRecent: (state) => state.showRecent,
|
||||
getShowLimit: (state) => state.showLimit,
|
||||
getCategories: (state) => state.categories,
|
||||
getHomeCategories: (state) => state.homeCategories,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import api from "@/api";
|
||||
import api from "../../api";
|
||||
import Vuetify from "../../plugins/vuetify";
|
||||
|
||||
function inDarkMode(payload) {
|
||||
@@ -60,9 +60,9 @@ const actions = {
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getActiveTheme: state => state.activeTheme,
|
||||
getDarkMode: state => state.darkMode,
|
||||
getIsDark: state => state.isDark,
|
||||
getActiveTheme: (state) => state.activeTheme,
|
||||
getDarkMode: (state) => state.darkMode,
|
||||
getIsDark: (state) => state.isDark,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import api from "@/api";
|
||||
import api from "../api";
|
||||
import createPersistedState from "vuex-persistedstate";
|
||||
import userSettings from "./modules/userSettings";
|
||||
import language from "./modules/language";
|
||||
@@ -64,11 +64,11 @@ const store = new Vuex.Store({
|
||||
|
||||
getters: {
|
||||
//
|
||||
getSnackText: state => state.snackText,
|
||||
getSnackActive: state => state.snackActive,
|
||||
getSnackType: state => state.snackType,
|
||||
getSnackText: (state) => state.snackText,
|
||||
getSnackActive: (state) => state.snackActive,
|
||||
getSnackType: (state) => state.snackType,
|
||||
|
||||
getRecentRecipes: state => state.recentRecipes,
|
||||
getRecentRecipes: (state) => state.recentRecipes,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
// import utils from "@/utils";
|
||||
// import utils from "../../utils";
|
||||
// import Vue from "vue";
|
||||
// import Vuetify from "./plugins/vuetify";
|
||||
import { vueApp } from "./main";
|
||||
|
||||
const notifyHelpers = {
|
||||
baseCSS: "notify-base",
|
||||
error: "notify-error-color",
|
||||
warning: "notify-warning-color",
|
||||
success: "notify-success-color",
|
||||
info: "notify-info-color",
|
||||
};
|
||||
|
||||
const days = [
|
||||
"Sunday",
|
||||
@@ -81,28 +72,4 @@ export default {
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
},
|
||||
notify: {
|
||||
show: function(text, type = "info", title = null) {
|
||||
vueApp.flashMessage.show({
|
||||
status: type,
|
||||
title: title,
|
||||
message: text,
|
||||
time: 3000,
|
||||
blockClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
|
||||
contentClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
|
||||
});
|
||||
},
|
||||
info: function(text, title = null) {
|
||||
this.show(text, "info", title);
|
||||
},
|
||||
success: function(text, title = null) {
|
||||
this.show(text, "success", title);
|
||||
},
|
||||
error: function(text, title = null) {
|
||||
this.show(text, "error", title);
|
||||
},
|
||||
warning: function(text, title = null) {
|
||||
this.show(text, "warning", title);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
# import utils.startup as startup
|
||||
from app_config import APP_VERSION, PORT, PRODUCTION, WEB_PATH, docs_url, redoc_url
|
||||
from app_config import PORT, PRODUCTION, WEB_PATH, docs_url, redoc_url
|
||||
from routes import (
|
||||
backup_routes,
|
||||
debug_routes,
|
||||
@@ -12,6 +12,7 @@ from routes import (
|
||||
setting_routes,
|
||||
static_routes,
|
||||
theme_routes,
|
||||
user_routes,
|
||||
)
|
||||
from routes.recipe import (
|
||||
all_recipe_routes,
|
||||
@@ -19,13 +20,25 @@ from routes.recipe import (
|
||||
recipe_crud_routes,
|
||||
tag_routes,
|
||||
)
|
||||
from services.settings_services import default_settings_init
|
||||
from utils.api_docs import generate_api_docs
|
||||
from utils.logger import logger
|
||||
|
||||
"""
|
||||
TODO:
|
||||
- [x] Fix Duplicate Category
|
||||
- [x] Fix category overflow
|
||||
- [ ] Enable Database Name Versioning
|
||||
- [ ] Finish Frontend Category Management
|
||||
- [x] Delete Category
|
||||
- [ ] Sort Sidebar A-Z
|
||||
- [ ] Refactor Test Endpoints - Abstract to fixture?
|
||||
|
||||
|
||||
"""
|
||||
app = FastAPI(
|
||||
title="Mealie",
|
||||
description="A place for all your recipes",
|
||||
version=APP_VERSION,
|
||||
version="0.0.1",
|
||||
docs_url=docs_url,
|
||||
redoc_url=redoc_url,
|
||||
)
|
||||
@@ -39,11 +52,6 @@ def start_scheduler():
|
||||
import services.scheduler.scheduled_jobs
|
||||
|
||||
|
||||
def init_settings():
|
||||
default_settings_init()
|
||||
import services.theme_services
|
||||
|
||||
|
||||
def api_routers():
|
||||
# Recipes
|
||||
app.include_router(all_recipe_routes.router)
|
||||
@@ -57,6 +65,8 @@ def api_routers():
|
||||
app.include_router(theme_routes.router)
|
||||
# Backups/Imports Routes
|
||||
app.include_router(backup_routes.router)
|
||||
# User Routes
|
||||
app.include_router(user_routes.router)
|
||||
# Migration Routes
|
||||
app.include_router(migration_routes.router)
|
||||
app.include_router(debug_routes.router)
|
||||
@@ -81,7 +91,6 @@ app.include_router(static_routes.router)
|
||||
# generate_api_docs(app)
|
||||
|
||||
start_scheduler()
|
||||
init_settings()
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("-----SYSTEM STARTUP-----")
|
||||
|
||||
@@ -15,32 +15,14 @@ def ensure_dirs():
|
||||
ENV = CWD.joinpath(".env")
|
||||
dotenv.load_dotenv(ENV)
|
||||
|
||||
# General
|
||||
APP_VERSION = "v0.2.1"
|
||||
DB_VERSION = "v0.2.0"
|
||||
PRODUCTION = os.environ.get("ENV")
|
||||
PORT = int(os.getenv("mealie_port", 9000))
|
||||
API = os.getenv("api_docs", True)
|
||||
|
||||
if API:
|
||||
docs_url = "/docs"
|
||||
redoc_url = "/redoc"
|
||||
else:
|
||||
docs_url = None
|
||||
redoc_url = None
|
||||
|
||||
# Helpful Globals
|
||||
BASE_DIR = CWD
|
||||
DATA_DIR = CWD.parent.joinpath("app_data")
|
||||
if PRODUCTION:
|
||||
DATA_DIR = Path("/app/data")
|
||||
|
||||
WEB_PATH = CWD.joinpath("dist")
|
||||
IMG_DIR = DATA_DIR.joinpath("img")
|
||||
BACKUP_DIR = DATA_DIR.joinpath("backups")
|
||||
DEBUG_DIR = DATA_DIR.joinpath("debug")
|
||||
MIGRATION_DIR = DATA_DIR.joinpath("migration")
|
||||
NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud")
|
||||
CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown")
|
||||
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
|
||||
SQLITE_DIR = DATA_DIR.joinpath("db")
|
||||
TEMP_DIR = DATA_DIR.joinpath(".temp")
|
||||
@@ -53,23 +35,31 @@ REQUIRED_DIRS = [
|
||||
MIGRATION_DIR,
|
||||
TEMPLATE_DIR,
|
||||
SQLITE_DIR,
|
||||
NEXTCLOUD_DIR,
|
||||
CHOWDOWN_DIR,
|
||||
]
|
||||
|
||||
ensure_dirs()
|
||||
APP_VERSION = "v0.2.0"
|
||||
# General
|
||||
PRODUCTION = os.environ.get("ENV")
|
||||
PORT = int(os.getenv("mealie_port", 9000))
|
||||
API = os.getenv("api_docs", True)
|
||||
|
||||
if API:
|
||||
docs_url = "/docs"
|
||||
redoc_url = "/redoc"
|
||||
else:
|
||||
docs_url = None
|
||||
redoc_url = None
|
||||
|
||||
# DATABASE ENV
|
||||
SQLITE_FILE = None
|
||||
DATABASE_TYPE = os.getenv("db_type", "sqlite")
|
||||
# DATABASE ENV
|
||||
DATABASE_TYPE = os.getenv("db_type", "sqlite") # mongo, sqlite
|
||||
if DATABASE_TYPE == "sqlite":
|
||||
USE_SQL = True
|
||||
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{DB_VERSION}.sqlite")
|
||||
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{APP_VERSION}.sqlite")
|
||||
|
||||
else:
|
||||
raise Exception(
|
||||
"Unable to determine database type. Acceptible options are 'sqlite' "
|
||||
"Unable to determine database type. Acceptible options are 'mongo' or 'tinydb' "
|
||||
)
|
||||
|
||||
# Mongo Database
|
||||
@@ -82,3 +72,6 @@ DB_PORT = os.getenv("db_port", 27017)
|
||||
# SFTP Email Stuff - For use Later down the line!
|
||||
SFTP_USERNAME = os.getenv("sftp_username", None)
|
||||
SFTP_PASSWORD = os.getenv("sftp_password", None)
|
||||
|
||||
|
||||
ensure_dirs()
|
||||
|
||||
@@ -9,6 +9,7 @@ from db.sql.theme_models import SiteThemeModel
|
||||
"""
|
||||
# TODO
|
||||
- [ ] Abstract Classes to use save_new, and update from base models
|
||||
- [x] Create Category and Tags Table with Many to Many relationship
|
||||
"""
|
||||
|
||||
|
||||
@@ -17,7 +18,7 @@ class _Recipes(BaseDocument):
|
||||
self.primary_key = "slug"
|
||||
self.sql_model = RecipeModel
|
||||
|
||||
def update_image(self, session: Session, slug: str, extension: str = None) -> str:
|
||||
def update_image(self, session: Session, slug: str, extension: str) -> str:
|
||||
entry: RecipeModel = self._query_one(session, match_value=slug)
|
||||
entry.image = f"{slug}.{extension}"
|
||||
session.commit()
|
||||
@@ -48,14 +49,13 @@ class _Settings(BaseDocument):
|
||||
self.primary_key = "name"
|
||||
self.sql_model = SiteSettingsModel
|
||||
|
||||
def create(self, session: Session, main: dict, webhooks: dict) -> str:
|
||||
def save_new(self, session: Session, main: dict, webhooks: dict) -> str:
|
||||
new_settings = self.sql_model(main.get("name"), webhooks)
|
||||
|
||||
session.add(new_settings)
|
||||
return_data = new_settings.dict()
|
||||
session.commit()
|
||||
|
||||
return return_data
|
||||
return new_settings.dict()
|
||||
|
||||
|
||||
class _Themes(BaseDocument):
|
||||
|
||||
@@ -106,7 +106,7 @@ class BaseDocument:
|
||||
|
||||
return db_entry
|
||||
|
||||
def create(self, session: Session, document: dict) -> dict:
|
||||
def save_new(self, session: Session, document: dict) -> dict:
|
||||
"""Creates a new database entry for the given SQL Alchemy Model.
|
||||
|
||||
Args: \n
|
||||
|
||||
63
mealie/db/db_mealplan.py
Normal file
63
mealie/db/db_mealplan.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from typing import List
|
||||
|
||||
from app_config import USE_MONGO, USE_SQL
|
||||
|
||||
from db.db_base import BaseDocument
|
||||
from db.db_setup import USE_MONGO, USE_SQL
|
||||
from db.mongo.meal_models import MealDocument, MealPlanDocument
|
||||
from db.sql.db_session import create_session
|
||||
from db.sql.meal_models import MealPlanModel
|
||||
|
||||
|
||||
class _Meals(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "uid"
|
||||
if USE_SQL:
|
||||
self.sql_model = MealPlanModel
|
||||
self.create_session = create_session
|
||||
|
||||
self.document = MealPlanDocument
|
||||
|
||||
@staticmethod
|
||||
def _process_meals(meals: List[dict]) -> List[MealDocument]:
|
||||
"""Turns a list of Meals in dictionary form into a list of
|
||||
MealDocuments that can be attached to a MealPlanDocument
|
||||
|
||||
|
||||
Args: \n
|
||||
meals (List[dict]): From a Pydantic Class in meal_services.py \n
|
||||
|
||||
Returns:
|
||||
a List of MealDocuments
|
||||
"""
|
||||
meal_docs = []
|
||||
for meal in meals:
|
||||
meal_doc = MealDocument(**meal)
|
||||
meal_docs.append(meal_doc)
|
||||
|
||||
return meal_docs
|
||||
|
||||
def save_new_mongo(self, plan_data: dict) -> None:
|
||||
"""Saves a new meal plan into the database
|
||||
|
||||
Args: \n
|
||||
plan_data (dict): From a Pydantic Class in meal_services.py \n
|
||||
"""
|
||||
|
||||
if USE_MONGO:
|
||||
plan_data["meals"] = _Meals._process_meals(plan_data["meals"])
|
||||
document = self.document(**plan_data)
|
||||
|
||||
document.save()
|
||||
elif USE_SQL:
|
||||
pass
|
||||
|
||||
def update_mongo(self, uid: str, plan_data: dict) -> dict:
|
||||
if USE_MONGO:
|
||||
document = self.document.objects.get(uid=uid)
|
||||
if document:
|
||||
new_meals = _Meals._process_meals(plan_data["meals"])
|
||||
document.update(set__meals=new_meals)
|
||||
document.save()
|
||||
elif USE_SQL:
|
||||
pass
|
||||
68
mealie/db/db_recipes.py
Normal file
68
mealie/db/db_recipes.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from app_config import USE_MONGO, USE_SQL
|
||||
|
||||
from db.db_base import BaseDocument
|
||||
from db.mongo.recipe_models import RecipeDocument
|
||||
from db.sql.db_session import create_session
|
||||
from db.sql.recipe_models import RecipeModel
|
||||
|
||||
|
||||
class _Recipes(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "slug"
|
||||
if USE_SQL:
|
||||
self.sql_model = RecipeModel
|
||||
self.create_session = create_session
|
||||
else:
|
||||
self.document = RecipeDocument
|
||||
|
||||
def save_new_sql(self, recipe_data: dict):
|
||||
session = self.create_session()
|
||||
new_recipe = self.sql_model(**recipe_data)
|
||||
session.add(new_recipe)
|
||||
session.commit()
|
||||
|
||||
return recipe_data
|
||||
|
||||
def update_mongo(self, slug: str, new_data: dict) -> None:
|
||||
if USE_MONGO:
|
||||
document = self.document.objects.get(slug=slug)
|
||||
|
||||
if document:
|
||||
document.update(set__name=new_data.get("name"))
|
||||
document.update(set__description=new_data.get("description"))
|
||||
document.update(set__image=new_data.get("image"))
|
||||
document.update(set__recipeYield=new_data.get("recipeYield"))
|
||||
document.update(set__recipeIngredient=new_data.get("recipeIngredient"))
|
||||
document.update(
|
||||
set__recipeInstructions=new_data.get("recipeInstructions")
|
||||
)
|
||||
document.update(set__totalTime=new_data.get("totalTime"))
|
||||
|
||||
document.update(set__slug=new_data.get("slug"))
|
||||
document.update(set__categories=new_data.get("categories"))
|
||||
document.update(set__tags=new_data.get("tags"))
|
||||
document.update(set__notes=new_data.get("notes"))
|
||||
document.update(set__orgURL=new_data.get("orgURL"))
|
||||
document.update(set__rating=new_data.get("rating"))
|
||||
document.update(set__extras=new_data.get("extras"))
|
||||
document.save()
|
||||
|
||||
return new_data
|
||||
# elif USE_SQL:
|
||||
# session, recipe = self._query_one(match_value=slug)
|
||||
# recipe.update(session=session, **new_data)
|
||||
# recipe_dict = recipe.dict()
|
||||
# session.commit()
|
||||
|
||||
# session.close()
|
||||
|
||||
# return recipe_dict
|
||||
|
||||
def update_image(self, slug: str, extension: str) -> None:
|
||||
if USE_MONGO:
|
||||
document = self.document.objects.get(slug=slug)
|
||||
|
||||
if document:
|
||||
document.update(set__image=f"{slug}.{extension}")
|
||||
elif USE_SQL:
|
||||
pass
|
||||
44
mealie/db/db_settings.py
Normal file
44
mealie/db/db_settings.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from app_config import USE_MONGO, USE_SQL
|
||||
|
||||
from db.db_base import BaseDocument
|
||||
from db.db_setup import USE_MONGO, USE_SQL
|
||||
from db.mongo.settings_models import SiteSettingsDocument, WebhooksDocument
|
||||
from db.sql.db_session import create_session
|
||||
from db.sql.settings_models import SiteSettingsModel
|
||||
|
||||
|
||||
class _Settings(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
|
||||
self.primary_key = "name"
|
||||
|
||||
if USE_SQL:
|
||||
self.sql_model = SiteSettingsModel
|
||||
self.create_session = create_session
|
||||
|
||||
self.document = SiteSettingsDocument
|
||||
|
||||
def save_new(self, main: dict, webhooks: dict) -> str:
|
||||
|
||||
if USE_MONGO:
|
||||
main["webhooks"] = WebhooksDocument(**webhooks)
|
||||
new_doc = self.document(**main)
|
||||
return new_doc.save()
|
||||
|
||||
elif USE_SQL:
|
||||
session = create_session()
|
||||
new_settings = self.sql_model(main.get("name"), webhooks)
|
||||
|
||||
session.add(new_settings)
|
||||
session.commit()
|
||||
|
||||
return new_settings.dict()
|
||||
|
||||
def update_mongo(self, name: str, new_data: dict) -> dict:
|
||||
if USE_MONGO:
|
||||
document = self.document.objects.get(name=name)
|
||||
if document:
|
||||
document.update(set__webhooks=WebhooksDocument(**new_data["webhooks"]))
|
||||
document.save()
|
||||
elif USE_SQL:
|
||||
return
|
||||
56
mealie/db/db_themes.py
Normal file
56
mealie/db/db_themes.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from app_config import USE_MONGO, USE_SQL
|
||||
|
||||
from db.db_base import BaseDocument
|
||||
from db.db_setup import USE_MONGO, USE_SQL
|
||||
from db.mongo.settings_models import SiteThemeDocument, ThemeColorsDocument
|
||||
from db.sql.db_session import create_session
|
||||
from db.sql.theme_models import SiteThemeModel
|
||||
|
||||
|
||||
class _Themes(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "name"
|
||||
if USE_SQL:
|
||||
self.sql_model = SiteThemeModel
|
||||
self.create_session = create_session
|
||||
else:
|
||||
self.document = SiteThemeDocument
|
||||
|
||||
def save_new(self, theme_data: dict) -> None:
|
||||
if USE_MONGO:
|
||||
theme_data["colors"] = ThemeColorsDocument(**theme_data["colors"])
|
||||
|
||||
document = self.document(**theme_data)
|
||||
|
||||
document.save()
|
||||
elif USE_SQL:
|
||||
session = self.create_session()
|
||||
new_theme = self.sql_model(**theme_data)
|
||||
|
||||
session.add(new_theme)
|
||||
session.commit()
|
||||
|
||||
return_data = new_theme.dict()
|
||||
|
||||
session.close()
|
||||
return return_data
|
||||
|
||||
def update(self, data: dict) -> dict:
|
||||
if USE_MONGO:
|
||||
colors = ThemeColorsDocument(**data["colors"])
|
||||
theme_document = self.document.objects.get(name=data.get("name"))
|
||||
|
||||
if theme_document:
|
||||
theme_document.update(set__colors=colors)
|
||||
theme_document.save()
|
||||
else:
|
||||
raise Exception("No database entry was found to update")
|
||||
|
||||
elif USE_SQL:
|
||||
session, theme_model = self._query_one(
|
||||
match_value=data["name"], match_key="name"
|
||||
)
|
||||
|
||||
theme_model.update(**data)
|
||||
session.commit()
|
||||
session.close()
|
||||
@@ -7,7 +7,6 @@ import sqlalchemy.orm as orm
|
||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
||||
from slugify import slugify
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import validates
|
||||
from utils.logger import logger
|
||||
|
||||
|
||||
@@ -44,26 +43,20 @@ recipes2tags = sa.Table(
|
||||
class Category(SqlAlchemyBase):
|
||||
__tablename__ = "categories"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String, index=True, nullable=False)
|
||||
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
|
||||
name = sa.Column(sa.String, index=True)
|
||||
slug = sa.Column(sa.String, index=True, unique=True)
|
||||
recipes = orm.relationship(
|
||||
"RecipeModel", secondary=recipes2categories, back_populates="categories"
|
||||
)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert not name == ""
|
||||
return name
|
||||
|
||||
def __init__(self, name) -> None:
|
||||
self.name = name.strip()
|
||||
self.slug = slugify(name)
|
||||
|
||||
@staticmethod
|
||||
def create_if_not_exist(session, name: str = None):
|
||||
test_slug = slugify(name)
|
||||
try:
|
||||
result = session.query(Category).filter(Category.slug == test_slug).one()
|
||||
result = session.query(Category).filter(Category.name == name.strip()).one()
|
||||
if result:
|
||||
logger.info("Category exists, associating recipe")
|
||||
return result
|
||||
@@ -89,17 +82,12 @@ class Category(SqlAlchemyBase):
|
||||
class Tag(SqlAlchemyBase):
|
||||
__tablename__ = "tags"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String, index=True, nullable=False)
|
||||
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
|
||||
name = sa.Column(sa.String, index=True)
|
||||
slug = sa.Column(sa.String, index=True, unique=True)
|
||||
recipes = orm.relationship(
|
||||
"RecipeModel", secondary=recipes2tags, back_populates="tags"
|
||||
)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert not name == ""
|
||||
return name
|
||||
|
||||
def to_str(self):
|
||||
return self.name
|
||||
|
||||
@@ -117,9 +105,8 @@ class Tag(SqlAlchemyBase):
|
||||
|
||||
@staticmethod
|
||||
def create_if_not_exist(session, name: str = None):
|
||||
test_slug = slugify(name)
|
||||
try:
|
||||
result = session.query(Tag).filter(Tag.slug == test_slug).first()
|
||||
result = session.query(Tag).filter(Tag.name == name.strip()).first()
|
||||
|
||||
if result:
|
||||
logger.info("Tag exists, associating recipe")
|
||||
@@ -182,7 +169,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
|
||||
# General Recipe Properties
|
||||
name = sa.Column(sa.String, nullable=False)
|
||||
name = sa.Column(sa.String)
|
||||
description = sa.Column(sa.String)
|
||||
image = sa.Column(sa.String)
|
||||
recipeYield = sa.Column(sa.String)
|
||||
@@ -218,11 +205,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
orgURL = sa.Column(sa.String)
|
||||
extras: List[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete")
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert not name == ""
|
||||
return name
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session,
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
from datetime import date
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Meal(BaseModel):
|
||||
slug: Optional[str]
|
||||
name: Optional[str]
|
||||
date: date
|
||||
dateText: str
|
||||
image: Optional[str]
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class MealData(BaseModel):
|
||||
name: Optional[str]
|
||||
slug: str
|
||||
dateText: str
|
||||
|
||||
|
||||
class MealPlan(BaseModel):
|
||||
uid: Optional[str]
|
||||
startDate: date
|
||||
endDate: date
|
||||
meals: List[Meal]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"startDate": date.today(),
|
||||
"endDate": date.today(),
|
||||
"meals": [
|
||||
{"slug": "Packed Mac and Cheese", "date": date.today()},
|
||||
{"slug": "Eggs and Toast", "date": date.today()},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,38 @@
|
||||
import datetime
|
||||
from typing import Any, List, Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, validator
|
||||
from slugify import slugify
|
||||
import pydantic
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
|
||||
class RecipeNote(BaseModel):
|
||||
title: str
|
||||
text: str
|
||||
|
||||
|
||||
class RecipeStep(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
class Recipe(BaseModel):
|
||||
# Standard Schema
|
||||
name: str
|
||||
description: Optional[str]
|
||||
image: Optional[Any]
|
||||
recipeYield: Optional[str]
|
||||
recipeIngredient: Optional[list]
|
||||
recipeInstructions: Optional[list]
|
||||
|
||||
totalTime: Optional[str] = None
|
||||
prepTime: Optional[str] = None
|
||||
performTime: Optional[str] = None
|
||||
|
||||
# Mealie Specific
|
||||
slug: Optional[str] = ""
|
||||
categories: Optional[List[str]] = []
|
||||
tags: Optional[List[str]] = []
|
||||
dateAdded: Optional[datetime.date]
|
||||
notes: Optional[List[RecipeNote]] = []
|
||||
rating: Optional[int]
|
||||
orgURL: Optional[str]
|
||||
extras: Optional[dict] = {}
|
||||
class AllRecipeResponse(BaseModel):
|
||||
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "Chicken and Rice With Leeks and Salsa Verde",
|
||||
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
|
||||
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
|
||||
"recipeYield": "4 Servings",
|
||||
"recipeIngredient": [
|
||||
"1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)",
|
||||
"Kosher salt, freshly ground pepper",
|
||||
"3 Tbsp. unsalted butter, divided",
|
||||
],
|
||||
"recipeInstructions": [
|
||||
{
|
||||
"text": "Season chicken with salt and pepper.",
|
||||
},
|
||||
],
|
||||
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"tags": ["favorite", "yummy!"],
|
||||
"categories": ["Dinner", "Pasta"],
|
||||
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
|
||||
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
|
||||
"rating": 3,
|
||||
"extras": {"message": "Don't forget to defrost the chicken!"},
|
||||
}
|
||||
"example": [
|
||||
{
|
||||
"slug": "crockpot-buffalo-chicken",
|
||||
"image": "crockpot-buffalo-chicken.jpg",
|
||||
"name": "Crockpot Buffalo Chicken",
|
||||
},
|
||||
{
|
||||
"slug": "downtown-marinade",
|
||||
"image": "downtown-marinade.jpg",
|
||||
"name": "Downtown Marinade",
|
||||
},
|
||||
{
|
||||
"slug": "detroit-style-pepperoni-pizza",
|
||||
"image": "detroit-style-pepperoni-pizza.jpg",
|
||||
"name": "Detroit-Style Pepperoni Pizza",
|
||||
},
|
||||
{
|
||||
"slug": "crispy-carrots",
|
||||
"image": "crispy-carrots.jpg",
|
||||
"name": "Crispy Carrots",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@validator("slug", always=True, pre=True)
|
||||
def validate_slug(slug: str, values):
|
||||
name: str = values["name"]
|
||||
calc_slug: str = slugify(name)
|
||||
|
||||
if slug == calc_slug:
|
||||
return slug
|
||||
else:
|
||||
slug = calc_slug
|
||||
return slug
|
||||
|
||||
|
||||
class AllRecipeRequest(BaseModel):
|
||||
properties: List[str]
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Webhooks(BaseModel):
|
||||
webhookTime: str = "00:00"
|
||||
webhookURLs: Optional[List[str]] = []
|
||||
enabled: bool = False
|
||||
|
||||
|
||||
class SiteSettings(BaseModel):
|
||||
name: str = "main"
|
||||
webhooks: Webhooks
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "main",
|
||||
"webhooks": {
|
||||
"webhookTime": "00:00",
|
||||
"webhookURLs": ["https://mywebhookurl.com/webhook"],
|
||||
"enable": False,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Colors(BaseModel):
|
||||
primary: str
|
||||
accent: str
|
||||
secondary: str
|
||||
success: str
|
||||
info: str
|
||||
warning: str
|
||||
error: str
|
||||
|
||||
|
||||
class SiteTheme(BaseModel):
|
||||
name: str
|
||||
colors: Colors
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "default",
|
||||
"colors": {
|
||||
"primary": "#E58325",
|
||||
"accent": "#00457A",
|
||||
"secondary": "#973542",
|
||||
"success": "#5AB1BB",
|
||||
"info": "#4990BA",
|
||||
"warning": "#FF4081",
|
||||
"error": "#EF5350",
|
||||
},
|
||||
}
|
||||
}
|
||||
10
mealie/models/user_models.py
Normal file
10
mealie/models/user_models.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
disabled: Optional[bool] = None
|
||||
@@ -32,10 +32,10 @@ def available_imports():
|
||||
|
||||
|
||||
@router.post("/export/database", status_code=201)
|
||||
def export_database(data: BackupJob, session: Session = Depends(generate_session)):
|
||||
def export_database(data: BackupJob, db: Session = Depends(generate_session)):
|
||||
"""Generates a backup of the recipe database in json format."""
|
||||
export_path = backup_all(
|
||||
session=session,
|
||||
session=db,
|
||||
tag=data.tag,
|
||||
templates=data.templates,
|
||||
export_recipes=data.options.recipes,
|
||||
@@ -66,7 +66,7 @@ def upload_backup_zipfile(archive: UploadFile = File(...)):
|
||||
|
||||
|
||||
@router.get("/{file_name}/download")
|
||||
async def upload_nextcloud_zipfile(file_name: str):
|
||||
def upload_nextcloud_zipfile(file_name: str):
|
||||
""" Upload a .zip File to later be imported into Mealie """
|
||||
file = BACKUP_DIR.joinpath(file_name)
|
||||
|
||||
@@ -80,12 +80,12 @@ async def upload_nextcloud_zipfile(file_name: str):
|
||||
|
||||
@router.post("/{file_name}/import", status_code=200)
|
||||
def import_database(
|
||||
file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)
|
||||
file_name: str, import_data: ImportJob, db: Session = Depends(generate_session)
|
||||
):
|
||||
""" Import a database backup file generated from Mealie. """
|
||||
|
||||
import_db = ImportDatabase(
|
||||
session=session,
|
||||
session=db,
|
||||
zip_archive=import_data.name,
|
||||
import_recipes=import_data.recipes,
|
||||
force_import=import_data.force,
|
||||
@@ -98,7 +98,7 @@ def import_database(
|
||||
return imported
|
||||
|
||||
|
||||
@router.delete("/{file_name}/delete", status_code=200)
|
||||
@router.delete("/{file_name}/delete", tags=["Import / Export"], status_code=200)
|
||||
def delete_backup(file_name: str):
|
||||
""" Removes a database backup from the file system """
|
||||
|
||||
@@ -110,4 +110,4 @@ def delete_backup(file_name: str):
|
||||
detail=SnackResponse.error("Unable to Delete Backup. See Log File"),
|
||||
)
|
||||
|
||||
return SnackResponse.error(f"{file_name} Deleted")
|
||||
return SnackResponse.success(f"{file_name} Deleted")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from app_config import APP_VERSION, DEBUG_DIR
|
||||
from app_config import DEBUG_DIR
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import HTMLResponse
|
||||
from utils.logger import LOGGER_FILE
|
||||
@@ -8,12 +9,6 @@ from utils.logger import LOGGER_FILE
|
||||
router = APIRouter(prefix="/api/debug", tags=["Debug"])
|
||||
|
||||
|
||||
@router.get("/version")
|
||||
async def get_mealie_version():
|
||||
""" Returns the current version of mealie"""
|
||||
return {"version": APP_VERSION}
|
||||
|
||||
|
||||
@router.get("/last-recipe-json")
|
||||
async def get_last_recipe_json():
|
||||
""" Doc Str """
|
||||
@@ -27,7 +22,18 @@ async def get_log(num: int):
|
||||
""" Doc Str """
|
||||
with open(LOGGER_FILE, "rb") as f:
|
||||
log_text = tail(f, num)
|
||||
HTML_RESPONSE = log_text
|
||||
HTML_RESPONSE = f"""
|
||||
<html>
|
||||
<head>
|
||||
<title>Mealie Log</title>
|
||||
</head>
|
||||
<body style="white-space: pre-line">
|
||||
<p>
|
||||
{log_text}
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return HTML_RESPONSE
|
||||
|
||||
|
||||
@@ -10,53 +10,66 @@ router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
|
||||
|
||||
|
||||
@router.get("/all", response_model=List[MealPlan])
|
||||
def get_all_meals(session: Session = Depends(generate_session)):
|
||||
def get_all_meals(db: Session = Depends(generate_session)):
|
||||
""" Returns a list of all available Meal Plan """
|
||||
|
||||
return MealPlan.get_all(session)
|
||||
return MealPlan.get_all(db)
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
def set_meal_plan(data: MealPlan, session: Session = Depends(generate_session)):
|
||||
def set_meal_plan(data: MealPlan, db: Session = Depends(generate_session)):
|
||||
""" Creates a meal plan database entry """
|
||||
data.process_meals(session)
|
||||
data.save_to_db(session)
|
||||
data.process_meals(db)
|
||||
data.save_to_db(db)
|
||||
|
||||
# raise HTTPException(
|
||||
# status_code=404,
|
||||
# detail=SnackResponse.error("Unable to Create Mealplan See Log"),
|
||||
# )
|
||||
|
||||
return SnackResponse.success("Mealplan Created")
|
||||
|
||||
|
||||
@router.get("/this-week", response_model=MealPlan)
|
||||
def get_this_week(session: Session = Depends(generate_session)):
|
||||
def get_this_week(db: Session = Depends(generate_session)):
|
||||
""" Returns the meal plan data for this week """
|
||||
|
||||
return MealPlan.this_week(session)
|
||||
return MealPlan.this_week(db)
|
||||
|
||||
|
||||
@router.put("/{plan_id}")
|
||||
def update_meal_plan(
|
||||
plan_id: str, meal_plan: MealPlan, session: Session = Depends(generate_session)
|
||||
plan_id: str, meal_plan: MealPlan, db: Session = Depends(generate_session)
|
||||
):
|
||||
""" Updates a meal plan based off ID """
|
||||
meal_plan.process_meals(session)
|
||||
meal_plan.update(session, plan_id)
|
||||
meal_plan.process_meals(db)
|
||||
meal_plan.update(db, plan_id)
|
||||
# try:
|
||||
# meal_plan.process_meals()
|
||||
# meal_plan.update(plan_id)
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=404,
|
||||
# detail=SnackResponse.error("Unable to Update Mealplan"),
|
||||
# )
|
||||
|
||||
return SnackResponse.info("Mealplan Updated")
|
||||
return SnackResponse.success("Mealplan Updated")
|
||||
|
||||
|
||||
@router.delete("/{plan_id}")
|
||||
def delete_meal_plan(plan_id, session: Session = Depends(generate_session)):
|
||||
def delete_meal_plan(plan_id, db: Session = Depends(generate_session)):
|
||||
""" Removes a meal plan from the database """
|
||||
|
||||
MealPlan.delete(session, plan_id)
|
||||
MealPlan.delete(db, plan_id)
|
||||
|
||||
return SnackResponse.error("Mealplan Deleted")
|
||||
return SnackResponse.success("Mealplan Deleted")
|
||||
|
||||
|
||||
@router.get("/today", tags=["Meal Plan"])
|
||||
def get_today(session: Session = Depends(generate_session)):
|
||||
def get_today(db: Session = Depends(generate_session)):
|
||||
"""
|
||||
Returns the recipe slug for the meal scheduled for today.
|
||||
If no meal is scheduled nothing is returned
|
||||
"""
|
||||
|
||||
return MealPlan.today(session)
|
||||
return MealPlan.today(db)
|
||||
|
||||
@@ -37,14 +37,14 @@ def get_avaiable_nextcloud_imports():
|
||||
|
||||
@router.post("/{type}/{file_name}/import")
|
||||
def import_nextcloud_directory(
|
||||
type: str, file_name: str, session: Session = Depends(generate_session)
|
||||
type: str, file_name: str, db: Session = Depends(generate_session)
|
||||
):
|
||||
""" Imports all the recipes in a given directory """
|
||||
file_path = MIGRATION_DIR.joinpath(type, file_name)
|
||||
if type == "nextcloud":
|
||||
return nextcloud_migrate(session, file_path)
|
||||
return nextcloud_migrate(db, file_path)
|
||||
elif type == "chowdown":
|
||||
return chowdow_migrate(session, file_path)
|
||||
return chowdow_migrate(db, file_path)
|
||||
else:
|
||||
return SnackResponse.error("Incorrect Migration Type Selected")
|
||||
|
||||
@@ -62,7 +62,7 @@ def delete_migration_data(type: str, file_name: str):
|
||||
else:
|
||||
SnackResponse.error("File/Folder not found.")
|
||||
|
||||
return SnackResponse.error(f"Migration Data Remove: {remove_path.absolute()}")
|
||||
return SnackResponse.info(f"Migration Data Remove: {remove_path.absolute()}")
|
||||
|
||||
|
||||
@router.post("/{type}/upload")
|
||||
|
||||
@@ -4,8 +4,6 @@ from fastapi import APIRouter, Depends
|
||||
from models.category_models import RecipeCategoryResponse
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/categories",
|
||||
tags=["Recipe Categories"],
|
||||
@@ -35,5 +33,3 @@ async def delete_recipe_category(
|
||||
from any recipes that contain it """
|
||||
|
||||
db.categories.delete(session, category)
|
||||
|
||||
return SnackResponse(f"Category Deleted: {category}")
|
||||
|
||||
@@ -62,11 +62,11 @@ def delete_recipe(recipe_slug: str, db: Session = Depends(generate_session)):
|
||||
status_code=404, detail=SnackResponse.error("Unable to Delete Recipe")
|
||||
)
|
||||
|
||||
return SnackResponse.error(f"Recipe {recipe_slug} Deleted")
|
||||
return SnackResponse.success("Recipe Deleted")
|
||||
|
||||
|
||||
@router.get("/{recipe_slug}/image")
|
||||
async def get_recipe_img(recipe_slug: str):
|
||||
def get_recipe_img(recipe_slug: str):
|
||||
""" Takes in a recipe slug, returns the static image """
|
||||
recipe_image = read_image(recipe_slug)
|
||||
|
||||
@@ -75,13 +75,10 @@ async def get_recipe_img(recipe_slug: str):
|
||||
|
||||
@router.put("/{recipe_slug}/image")
|
||||
def update_recipe_image(
|
||||
recipe_slug: str,
|
||||
image: bytes = File(...),
|
||||
extension: str = Form(...),
|
||||
session: Session = Depends(generate_session),
|
||||
recipe_slug: str, image: bytes = File(...), extension: str = Form(...)
|
||||
):
|
||||
""" Removes an existing image and replaces it with the incoming file. """
|
||||
response = write_image(recipe_slug, image, extension)
|
||||
Recipe.update_image(session, recipe_slug, extension)
|
||||
Recipe.update_image(recipe_slug, extension)
|
||||
|
||||
return response
|
||||
|
||||
@@ -3,8 +3,6 @@ from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(tags=["Recipes"])
|
||||
|
||||
router = APIRouter(
|
||||
@@ -32,5 +30,3 @@ async def delete_recipe_tag(tag: str, session: Session = Depends(generate_sessio
|
||||
from any recipes that contain it"""
|
||||
|
||||
db.tags.delete(session, tag)
|
||||
|
||||
return SnackResponse.error(f"Tag Deleted: {tag}")
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from db.database import db
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from models.settings_models import SiteSettings
|
||||
from services.settings_services import default_settings_init
|
||||
from services.settings_services import SiteSettings
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.post_webhooks import post_webhooks
|
||||
from utils.snackbar import SnackResponse
|
||||
@@ -11,24 +9,10 @@ router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_main_settings(session: Session = Depends(generate_session)):
|
||||
def get_main_settings(db: Session = Depends(generate_session)):
|
||||
""" Returns basic site settings """
|
||||
|
||||
try:
|
||||
data = db.settings.get(session, "main")
|
||||
except:
|
||||
default_settings_init(session)
|
||||
data = db.settings.get(session, "main")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@router.put("")
|
||||
def update_settings(data: SiteSettings, session: Session = Depends(generate_session)):
|
||||
""" Returns Site Settings """
|
||||
db.settings.update(session, "main", data.dict())
|
||||
|
||||
return SnackResponse.success("Settings Updated")
|
||||
return SiteSettings.get_site_settings(db)
|
||||
|
||||
|
||||
@router.post("/webhooks/test")
|
||||
@@ -36,3 +20,20 @@ def test_webhooks():
|
||||
""" Run the function to test your webhooks """
|
||||
|
||||
return post_webhooks()
|
||||
|
||||
|
||||
@router.put("")
|
||||
def update_settings(data: SiteSettings, db: Session = Depends(generate_session)):
|
||||
""" Returns Site Settings """
|
||||
data.update(db)
|
||||
# try:
|
||||
# data.update()
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=400, detail=SnackResponse.error("Unable to Save Settings")
|
||||
# )
|
||||
|
||||
return SnackResponse.success("Settings Updated")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ def facivon():
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def root():
|
||||
def root():
|
||||
return FileResponse(BASE_HTML)
|
||||
|
||||
|
||||
@router.get("/{full_path:path}")
|
||||
async def root_plus(full_path):
|
||||
def root_plus(full_path):
|
||||
return FileResponse(BASE_HTML)
|
||||
|
||||
@@ -1,47 +1,64 @@
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from models.theme_models import SiteTheme
|
||||
from services.settings_services import SiteTheme
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.snackbar import SnackResponse
|
||||
from db.database import db
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["Themes"])
|
||||
|
||||
|
||||
@router.get("/themes")
|
||||
def get_all_themes(session: Session = Depends(generate_session)):
|
||||
def get_all_themes(db: Session = Depends(generate_session)):
|
||||
""" Returns all site themes """
|
||||
|
||||
return db.themes.get_all(session)
|
||||
return SiteTheme.get_all(db)
|
||||
|
||||
|
||||
@router.post("/themes/create")
|
||||
def create_theme(data: SiteTheme, session: Session = Depends(generate_session)):
|
||||
def create_theme(data: SiteTheme, db: Session = Depends(generate_session)):
|
||||
""" Creates a site color theme database entry """
|
||||
db.themes.create(session, data.dict())
|
||||
data.save_to_db(db)
|
||||
# try:
|
||||
# data.save_to_db()
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=400, detail=SnackResponse.error("Unable to Save Theme")
|
||||
# )
|
||||
|
||||
return SnackResponse.success("Theme Saved")
|
||||
|
||||
|
||||
@router.get("/themes/{theme_name}")
|
||||
def get_single_theme(theme_name: str, session: Session = Depends(generate_session)):
|
||||
def get_single_theme(theme_name: str, db: Session = Depends(generate_session)):
|
||||
""" Returns a named theme """
|
||||
return db.themes.get(session, theme_name)
|
||||
return SiteTheme.get_by_name(db, theme_name)
|
||||
|
||||
|
||||
@router.put("/themes/{theme_name}")
|
||||
def update_theme(
|
||||
theme_name: str, data: SiteTheme, session: Session = Depends(generate_session)
|
||||
theme_name: str, data: SiteTheme, db: Session = Depends(generate_session)
|
||||
):
|
||||
""" Update a theme database entry """
|
||||
db.themes.update(session, theme_name, data.dict())
|
||||
data.update_document(db)
|
||||
|
||||
return SnackResponse.info(f"Theme Updated: {theme_name}")
|
||||
# try:
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=400, detail=SnackResponse.error("Unable to Update Theme")
|
||||
# )
|
||||
|
||||
return SnackResponse.success("Theme Updated")
|
||||
|
||||
|
||||
@router.delete("/themes/{theme_name}")
|
||||
def delete_theme(theme_name: str, session: Session = Depends(generate_session)):
|
||||
def delete_theme(theme_name: str, db: Session = Depends(generate_session)):
|
||||
""" Deletes theme from the database """
|
||||
db.themes.delete(session, theme_name)
|
||||
SiteTheme.delete_theme(db, theme_name)
|
||||
# try:
|
||||
# SiteTheme.delete_theme(theme_name)
|
||||
# except:
|
||||
# raise HTTPException(
|
||||
# status_code=400, detail=SnackResponse.error("Unable to Delete Theme")
|
||||
# )
|
||||
|
||||
return SnackResponse.error(f"Theme Deleted: {theme_name}")
|
||||
return SnackResponse.success("Theme Deleted")
|
||||
|
||||
33
mealie/routes/user_routes.py
Normal file
33
mealie/routes/user_routes.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
# from fastapi_login import LoginManager
|
||||
# from fastapi_login.exceptions import InvalidCredentialsException
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# SECRET = "876cfb59db03d9c17cefec967b00255d3f7d93a823e5dc2a"
|
||||
# manager = LoginManager(SECRET, tokenUrl="/api/auth/token")
|
||||
|
||||
# fake_db = {"johndoe@e.mail": {"password": "hunter2"}}
|
||||
|
||||
|
||||
# @manager.user_loader
|
||||
# def load_user(email: str): # could also be an asynchronous function
|
||||
# user = fake_db.get(email)
|
||||
# return user
|
||||
|
||||
|
||||
# @router.post("/api/auth/token", tags=["User Gen"])
|
||||
# def login(data: OAuth2PasswordRequestForm = Depends()):
|
||||
# email = data.username
|
||||
# password = data.password
|
||||
|
||||
# user = load_user(email) # we are using the same function to retrieve the user
|
||||
# if not user:
|
||||
# raise InvalidCredentialsException # you can also use your own HTTPException
|
||||
# elif password != user["password"]:
|
||||
# raise InvalidCredentialsException
|
||||
|
||||
# access_token = manager.create_access_token(data=dict(sub=email))
|
||||
# return {"access_token": access_token, "token_type": "bearer"}
|
||||
5
mealie/run.sh
Normal file
5
mealie/run.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
## Run Migration
|
||||
|
||||
|
||||
## Start Application
|
||||
uvicorn app:app --host 0.0.0.0 --port 80
|
||||
@@ -4,11 +4,11 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
|
||||
from db.database import db
|
||||
from db.db_setup import create_session
|
||||
from jinja2 import Template
|
||||
from services.meal_services import MealPlan
|
||||
from services.recipe_services import Recipe
|
||||
from services.settings_services import SiteSettings, SiteTheme
|
||||
from utils.logger import logger
|
||||
|
||||
|
||||
@@ -88,18 +88,20 @@ class ExportDatabase:
|
||||
shutil.copy(file, self.img_dir.joinpath(file.name))
|
||||
|
||||
def export_settings(self):
|
||||
all_settings = db.settings.get(self.session, "main")
|
||||
all_settings = SiteSettings.get_site_settings(self.session)
|
||||
out_file = self.settings_dir.joinpath("settings.json")
|
||||
ExportDatabase._write_json_file(all_settings, out_file)
|
||||
ExportDatabase._write_json_file(all_settings.dict(), out_file)
|
||||
|
||||
def export_themes(self):
|
||||
all_themes = db.themes.get_all(self.session)
|
||||
all_themes = SiteTheme.get_all(self.session)
|
||||
if all_themes:
|
||||
all_themes = [x.dict() for x in all_themes]
|
||||
out_file = self.themes_dir.joinpath("themes.json")
|
||||
ExportDatabase._write_json_file(all_themes, out_file)
|
||||
|
||||
def export_meals(self):
|
||||
#! Problem Parseing Datetime Objects... May come back to this
|
||||
def export_meals(
|
||||
self,
|
||||
): #! Problem Parseing Datetime Objects... May come back to this
|
||||
meal_plans = MealPlan.get_all(self.session)
|
||||
if meal_plans:
|
||||
meal_plans = [x.dict() for x in meal_plans]
|
||||
@@ -108,7 +110,7 @@ class ExportDatabase:
|
||||
ExportDatabase._write_json_file(meal_plans, out_file)
|
||||
|
||||
@staticmethod
|
||||
def _write_json_file(data: dict, out_file: Path):
|
||||
def _write_json_file(data, out_file: Path):
|
||||
json_data = json.dumps(data, indent=4, default=str)
|
||||
|
||||
with open(out_file, "w") as f:
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import json
|
||||
import shutil
|
||||
import zipfile
|
||||
from logging import error
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR
|
||||
from db.database import db
|
||||
from models.theme_models import SiteTheme
|
||||
from services.recipe_services import Recipe
|
||||
from services.settings_services import SiteSettings
|
||||
from services.settings_services import SiteSettings, SiteTheme
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.logger import logger
|
||||
|
||||
@@ -57,7 +54,6 @@ class ImportDatabase:
|
||||
raise Exception("Import file does not exist")
|
||||
|
||||
def run(self):
|
||||
report = {}
|
||||
if self.imp_recipes:
|
||||
report = self.import_recipes()
|
||||
if self.imp_settings:
|
||||
@@ -99,24 +95,9 @@ class ImportDatabase:
|
||||
del recipe_dict["_id"]
|
||||
del recipe_dict["dateAdded"]
|
||||
except:
|
||||
pass
|
||||
logger.info("Detected new backup Schema, skipping migration...")
|
||||
return recipe_dict
|
||||
# Migration from list to Object Type Data
|
||||
try:
|
||||
if "" in recipe_dict["tags"]:
|
||||
recipe_dict["tags"] = [
|
||||
tag for tag in recipe_dict["tags"] if not tag == ""
|
||||
]
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
if "" in recipe_dict["categories"]:
|
||||
recipe_dict["categories"] = [
|
||||
cat for cat in recipe_dict["categories"] if not cat == ""
|
||||
]
|
||||
except:
|
||||
pass
|
||||
|
||||
if type(recipe_dict["extras"]) == list:
|
||||
recipe_dict["extras"] = {}
|
||||
|
||||
@@ -132,13 +113,11 @@ class ImportDatabase:
|
||||
themes_file = self.import_dir.joinpath("themes", "themes.json")
|
||||
|
||||
with open(themes_file, "r") as f:
|
||||
themes: list[dict] = json.loads(f.read())
|
||||
themes: list = json.loads(f.read())
|
||||
for theme in themes:
|
||||
if theme.get("name") == "default":
|
||||
continue
|
||||
new_theme = SiteTheme(**theme)
|
||||
try:
|
||||
db.themes.create(self.session, new_theme.dict())
|
||||
new_theme.save_to_db(self.session)
|
||||
except:
|
||||
logger.info(f"Unable Import Theme {new_theme.name}")
|
||||
|
||||
@@ -148,7 +127,9 @@ class ImportDatabase:
|
||||
with open(settings_file, "r") as f:
|
||||
settings: dict = json.loads(f.read())
|
||||
|
||||
db.settings.update(self.session, settings.get("name"), settings)
|
||||
settings = SiteSettings(**settings)
|
||||
|
||||
settings.update(self.session)
|
||||
|
||||
def clean_up(self):
|
||||
shutil.rmtree(TEMP_DIR)
|
||||
|
||||
@@ -8,6 +8,19 @@ from sqlalchemy.orm.session import Session
|
||||
|
||||
from services.recipe_services import Recipe
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
THIS_WEEK = CWD.parent.joinpath("data", "meal_plan", "this_week.json")
|
||||
NEXT_WEEK = CWD.parent.joinpath("data", "meal_plan", "next_week.json")
|
||||
WEEKDAYS = [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
]
|
||||
|
||||
|
||||
class Meal(BaseModel):
|
||||
slug: Optional[str]
|
||||
@@ -68,7 +81,7 @@ class MealPlan(BaseModel):
|
||||
self.meals = meals
|
||||
|
||||
def save_to_db(self, session: Session):
|
||||
db.meals.create(session, self.dict())
|
||||
db.meals.save_new(session, self.dict())
|
||||
|
||||
@staticmethod
|
||||
def get_all(session: Session) -> List:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Optional
|
||||
|
||||
@@ -97,7 +98,13 @@ class Recipe(BaseModel):
|
||||
except:
|
||||
recipe_dict["image"] = "no image"
|
||||
|
||||
recipe_doc = db.recipes.create(session, recipe_dict)
|
||||
# try:
|
||||
# total_time = recipe_dict.get("totalTime")
|
||||
# recipe_dict["totalTime"] = str(total_time)
|
||||
# except:
|
||||
# pass
|
||||
|
||||
recipe_doc = db.recipes.save_new(session, recipe_dict)
|
||||
recipe = Recipe(**recipe_doc)
|
||||
|
||||
return recipe.slug
|
||||
@@ -115,7 +122,7 @@ class Recipe(BaseModel):
|
||||
return updated_slug.get("slug")
|
||||
|
||||
@staticmethod
|
||||
def update_image(session: Session, slug: str, extension: str = None) -> str:
|
||||
def update_image(slug: str, extension: str) -> str:
|
||||
"""A helper function to pass the new image name and extension
|
||||
into the database.
|
||||
|
||||
@@ -123,8 +130,11 @@ class Recipe(BaseModel):
|
||||
slug (str): The current recipe slug
|
||||
extension (str): the file extension of the new image
|
||||
"""
|
||||
return db.recipes.update_image(session, slug, extension)
|
||||
return db.recipes.update_image(slug, extension)
|
||||
|
||||
@staticmethod
|
||||
def get_all(session: Session):
|
||||
return db.recipes.get_all(session)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,8 @@ from db.db_setup import create_session
|
||||
from services.backups.exports import auto_backup_job
|
||||
from services.scheduler.global_scheduler import scheduler
|
||||
from services.scheduler.scheduler_utils import Cron, cron_parser
|
||||
from services.settings_services import SiteSettings
|
||||
from utils.logger import logger
|
||||
from models.settings_models import SiteSettings
|
||||
from db.database import db
|
||||
from utils.post_webhooks import post_webhooks
|
||||
|
||||
|
||||
@@ -16,8 +15,7 @@ def update_webhook_schedule():
|
||||
poll the database for changes and reschedule the webhook time
|
||||
"""
|
||||
session = create_session()
|
||||
settings = db.settings.get(session, "main")
|
||||
settings = SiteSettings(**settings)
|
||||
settings = SiteSettings.get_site_settings(session=session)
|
||||
time = cron_parser(settings.webhooks.webhookTime)
|
||||
job = JOB_STORE.get("webhooks")
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from w3lib.html import get_base_url
|
||||
from services.image_services import scrape_image
|
||||
from services.recipe_services import Recipe
|
||||
|
||||
LAST_JSON = DEBUG_DIR.joinpath("last_recipe.json")
|
||||
TEMP_FILE = DEBUG_DIR.joinpath("last_recipe.json")
|
||||
|
||||
|
||||
def cleanhtml(raw_html):
|
||||
@@ -121,7 +121,6 @@ def process_recipe_data(new_recipe: dict, url=None) -> dict:
|
||||
|
||||
def extract_recipe_from_html(html: str, url: str) -> dict:
|
||||
scraped_recipes: List[dict] = scrape_schema_recipe.loads(html, python_objects=True)
|
||||
dump_last_json(scraped_recipes)
|
||||
|
||||
if not scraped_recipes:
|
||||
scraped_recipes: List[dict] = scrape_schema_recipe.scrape_url(
|
||||
@@ -165,11 +164,7 @@ def og_fields(properties: List[Tuple[str, str]], field_name: str) -> List[str]:
|
||||
def basic_recipe_from_opengraph(html: str, url: str) -> dict:
|
||||
base_url = get_base_url(html, url)
|
||||
data = extruct.extract(html, base_url=base_url)
|
||||
try:
|
||||
properties = data["opengraph"][0]["properties"]
|
||||
except:
|
||||
return
|
||||
|
||||
properties = data["opengraph"][0]["properties"]
|
||||
return {
|
||||
"name": og_field(properties, "og:title"),
|
||||
"description": og_field(properties, "og:description"),
|
||||
@@ -189,13 +184,6 @@ def basic_recipe_from_opengraph(html: str, url: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def dump_last_json(recipe_data: dict):
|
||||
with open(LAST_JSON, "w") as f:
|
||||
f.write(json.dumps(recipe_data, indent=4, default=str))
|
||||
|
||||
return
|
||||
|
||||
|
||||
def process_recipe_url(url: str) -> dict:
|
||||
r = requests.get(url)
|
||||
new_recipe = extract_recipe_from_html(r.text, url)
|
||||
@@ -206,6 +194,9 @@ def process_recipe_url(url: str) -> dict:
|
||||
def create_from_url(url: str) -> Recipe:
|
||||
recipe_data = process_recipe_url(url)
|
||||
|
||||
with open(TEMP_FILE, "w") as f:
|
||||
f.write(json.dumps(recipe_data, indent=4, default=str))
|
||||
|
||||
recipe = Recipe(**recipe_data)
|
||||
|
||||
return recipe
|
||||
|
||||
@@ -1,16 +1,149 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from db.database import db
|
||||
from db.db_setup import create_session, sql_exists
|
||||
from models.settings_models import SiteSettings, Webhooks
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.logger import logger
|
||||
|
||||
|
||||
def default_settings_init(session: Session = None):
|
||||
if session == None:
|
||||
session = create_session()
|
||||
class Webhooks(BaseModel):
|
||||
webhookTime: str = "00:00"
|
||||
webhookURLs: Optional[List[str]] = []
|
||||
enabled: bool = False
|
||||
|
||||
|
||||
class SiteSettings(BaseModel):
|
||||
name: str = "main"
|
||||
webhooks: Webhooks
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "main",
|
||||
"webhooks": {
|
||||
"webhookTime": "00:00",
|
||||
"webhookURLs": ["https://mywebhookurl.com/webhook"],
|
||||
"enable": False,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_all(session: Session):
|
||||
db.settings.get_all(session)
|
||||
|
||||
@classmethod
|
||||
def get_site_settings(cls, session: Session):
|
||||
try:
|
||||
document = db.settings.get(session=session, match_value="main")
|
||||
except:
|
||||
webhooks = Webhooks()
|
||||
default_entry = SiteSettings(name="main", webhooks=webhooks)
|
||||
document = db.settings.save_new(
|
||||
session, default_entry.dict(), webhooks.dict()
|
||||
)
|
||||
|
||||
return cls(**document)
|
||||
|
||||
def update(self, session: Session):
|
||||
db.settings.update(session, "main", new_data=self.dict())
|
||||
|
||||
|
||||
class Colors(BaseModel):
|
||||
primary: str
|
||||
accent: str
|
||||
secondary: str
|
||||
success: str
|
||||
info: str
|
||||
warning: str
|
||||
error: str
|
||||
|
||||
|
||||
class SiteTheme(BaseModel):
|
||||
name: str
|
||||
colors: Colors
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "default",
|
||||
"colors": {
|
||||
"primary": "#E58325",
|
||||
"accent": "#00457A",
|
||||
"secondary": "#973542",
|
||||
"success": "#5AB1BB",
|
||||
"info": "#4990BA",
|
||||
"warning": "#FF4081",
|
||||
"error": "#EF5350",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_by_name(cls, session: Session, theme_name):
|
||||
db_entry = db.themes.get(session, theme_name)
|
||||
name = db_entry.get("name")
|
||||
colors = Colors(**db_entry.get("colors"))
|
||||
|
||||
return cls(name=name, colors=colors)
|
||||
|
||||
@staticmethod
|
||||
def get_all(session: Session):
|
||||
all_themes = db.themes.get_all(session)
|
||||
for index, theme in enumerate(all_themes):
|
||||
name = theme.get("name")
|
||||
colors = Colors(**theme.get("colors"))
|
||||
|
||||
all_themes[index] = SiteTheme(name=name, colors=colors)
|
||||
|
||||
return all_themes
|
||||
|
||||
def save_to_db(self, session: Session):
|
||||
db.themes.save_new(session, self.dict())
|
||||
|
||||
def update_document(self, session: Session):
|
||||
db.themes.update(session, self.name, self.dict())
|
||||
|
||||
@staticmethod
|
||||
def delete_theme(session: Session, theme_name: str) -> str:
|
||||
""" Removes the theme by name """
|
||||
db.themes.delete(session, theme_name)
|
||||
|
||||
|
||||
def default_theme_init():
|
||||
default_colors = {
|
||||
"primary": "#E58325",
|
||||
"accent": "#00457A",
|
||||
"secondary": "#973542",
|
||||
"success": "#5AB1BB",
|
||||
"info": "#4990BA",
|
||||
"warning": "#FF4081",
|
||||
"error": "#EF5350",
|
||||
}
|
||||
session = create_session()
|
||||
try:
|
||||
SiteTheme.get_by_name(session, "default")
|
||||
logger.info("Default theme exists... skipping generation")
|
||||
except:
|
||||
logger.info("Generating Default Theme")
|
||||
colors = Colors(**default_colors)
|
||||
default_theme = SiteTheme(name="default", colors=colors)
|
||||
default_theme.save_to_db(session)
|
||||
|
||||
|
||||
def default_settings_init():
|
||||
session = create_session()
|
||||
try:
|
||||
document = db.settings.get(session, "main")
|
||||
except:
|
||||
webhooks = Webhooks()
|
||||
default_entry = SiteSettings(name="main", webhooks=webhooks)
|
||||
document = db.settings.create(session, default_entry.dict(), webhooks.dict())
|
||||
except:
|
||||
pass
|
||||
document = db.settings.save_new(session, default_entry.dict(), webhooks.dict())
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
if not sql_exists:
|
||||
default_settings_init()
|
||||
default_theme_init()
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
from db.database import db
|
||||
from db.db_setup import create_session, sql_exists
|
||||
from utils.logger import logger
|
||||
|
||||
|
||||
def default_theme_init():
|
||||
default_theme = {
|
||||
"name": "default",
|
||||
"colors": {
|
||||
"primary": "#E58325",
|
||||
"accent": "#00457A",
|
||||
"secondary": "#973542",
|
||||
"success": "#5AB1BB",
|
||||
"info": "#4990BA",
|
||||
"warning": "#FF4081",
|
||||
"error": "#EF5350",
|
||||
},
|
||||
}
|
||||
session = create_session()
|
||||
try:
|
||||
db.themes.create(session, default_theme)
|
||||
logger.info("Generating default theme...")
|
||||
except:
|
||||
logger.info("Default Theme Exists.. skipping generation")
|
||||
|
||||
|
||||
if not sql_exists:
|
||||
default_theme_init()
|
||||
@@ -5,8 +5,6 @@ from app_config import SQLITE_DIR
|
||||
from db.db_setup import generate_session, sql_global_init
|
||||
from fastapi.testclient import TestClient
|
||||
from pytest import fixture
|
||||
from services.settings_services import default_settings_init
|
||||
from services.theme_services import default_theme_init
|
||||
|
||||
from tests.test_config import TEST_DATA
|
||||
|
||||
@@ -20,13 +18,13 @@ TestSessionLocal = sql_global_init(SQLITE_FILE, check_thread=False)
|
||||
def override_get_db():
|
||||
try:
|
||||
db = TestSessionLocal()
|
||||
default_theme_init()
|
||||
default_settings_init()
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
|
||||
|
||||
@fixture(scope="session")
|
||||
def api_client():
|
||||
|
||||
|
||||
0
mealie/tests/test_recipes/__init__.py
Normal file
0
mealie/tests/test_recipes/__init__.py
Normal file
99
mealie/tests/test_recipes/test_scraper.py
Normal file
99
mealie/tests/test_recipes/test_scraper.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from services.scrape_services import (
|
||||
extract_recipe_from_html,
|
||||
normalize_data,
|
||||
normalize_instructions,
|
||||
)
|
||||
|
||||
CWD = Path(__file__).parent
|
||||
RAW_RECIPE_DIR = CWD.parent.joinpath("data", "recipes-raw")
|
||||
RAW_HTML_DIR = CWD.parent.joinpath("data", "html-raw")
|
||||
|
||||
# https://github.com/django/django/blob/stable/1.3.x/django/core/validators.py#L45
|
||||
url_validation_regex = re.compile(
|
||||
r"^(?:http|ftp)s?://" # http:// or https://
|
||||
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain...
|
||||
r"localhost|" # localhost...
|
||||
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
|
||||
r"(?::\d+)?" # optional port
|
||||
r"(?:/?|[/?]\S+)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"json_file,num_steps",
|
||||
[
|
||||
("best-homemade-salsa-recipe.json", 2),
|
||||
(
|
||||
"blue-cheese-stuffed-turkey-meatballs-with-raspberry-balsamic-glaze-2.json",
|
||||
3,
|
||||
),
|
||||
("bon_appetit.json", 8),
|
||||
("chunky-apple-cake.json", 4),
|
||||
("dairy-free-impossible-pumpkin-pie.json", 7),
|
||||
("how-to-make-instant-pot-spaghetti.json", 8),
|
||||
("instant-pot-chicken-and-potatoes.json", 4),
|
||||
("instant-pot-kerala-vegetable-stew.json", 13),
|
||||
("jalapeno-popper-dip.json", 4),
|
||||
("microwave_sweet_potatoes_04783.json", 4),
|
||||
("moroccan-skirt-steak-with-roasted-pepper-couscous.json", 4),
|
||||
("Pizza-Knoblauch-Champignon-Paprika-vegan.html.json", 3),
|
||||
],
|
||||
)
|
||||
def test_normalize_data(json_file, num_steps):
|
||||
recipe_data = normalize_data(json.load(open(RAW_RECIPE_DIR.joinpath(json_file))))
|
||||
assert len(recipe_data["recipeInstructions"]) == num_steps
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"instructions",
|
||||
[
|
||||
"A\n\nB\n\nC\n\n",
|
||||
"A\nB\nC\n",
|
||||
"A\r\n\r\nB\r\n\r\nC\r\n\r\n",
|
||||
"A\r\nB\r\nC\r\n",
|
||||
["A", "B", "C"],
|
||||
[{"@type": "HowToStep", "text": x} for x in ["A", "B", "C"]],
|
||||
],
|
||||
)
|
||||
def test_normalize_instructions(instructions):
|
||||
assert normalize_instructions(instructions) == [
|
||||
{"text": "A"},
|
||||
{"text": "B"},
|
||||
{"text": "C"},
|
||||
]
|
||||
|
||||
|
||||
def test_html_no_recipe_data():
|
||||
path = RAW_HTML_DIR.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
|
||||
url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
|
||||
recipe_data = extract_recipe_from_html(open(path).read(), url)
|
||||
|
||||
assert len(recipe_data["name"]) > 10
|
||||
assert len(recipe_data["slug"]) > 10
|
||||
assert recipe_data["orgURL"] == url
|
||||
assert len(recipe_data["description"]) > 100
|
||||
assert url_validation_regex.match(recipe_data["image"])
|
||||
assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
|
||||
assert recipe_data["recipeInstructions"] == [
|
||||
{"text": "Could not detect instructions"}
|
||||
]
|
||||
|
||||
|
||||
def test_html_with_recipe_data():
|
||||
path = RAW_HTML_DIR.joinpath("healthy_pasta_bake_60759.html")
|
||||
url = "https://www.bbc.co.uk/food/recipes/healthy_pasta_bake_60759"
|
||||
recipe_data = extract_recipe_from_html(open(path).read(), url)
|
||||
|
||||
assert len(recipe_data["name"]) > 10
|
||||
assert len(recipe_data["slug"]) > 10
|
||||
assert recipe_data["orgURL"] == url
|
||||
assert len(recipe_data["description"]) > 100
|
||||
assert url_validation_regex.match(recipe_data["image"])
|
||||
assert len(recipe_data["recipeIngredient"]) == 13
|
||||
assert len(recipe_data["recipeInstructions"]) == 4
|
||||
@@ -32,7 +32,6 @@ def default_theme(api_client):
|
||||
"error": "#EF5350",
|
||||
},
|
||||
}
|
||||
|
||||
api_client.post(THEMES_CREATE, json=default_theme)
|
||||
|
||||
return default_theme
|
||||
|
||||
@@ -65,20 +65,20 @@ def test_normalize_instructions(instructions):
|
||||
]
|
||||
|
||||
|
||||
# def test_html_no_recipe_data(): #! Unsure why it's failing, code didn't change?
|
||||
# path = TEST_RAW_HTML.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
|
||||
# url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
|
||||
# recipe_data = extract_recipe_from_html(open(path).read(), url)
|
||||
def test_html_no_recipe_data():
|
||||
path = TEST_RAW_HTML.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
|
||||
url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
|
||||
recipe_data = extract_recipe_from_html(open(path).read(), url)
|
||||
|
||||
# assert len(recipe_data["name"]) > 10
|
||||
# assert len(recipe_data["slug"]) > 10
|
||||
# assert recipe_data["orgURL"] == url
|
||||
# assert len(recipe_data["description"]) > 100
|
||||
# assert url_validation_regex.match(recipe_data["image"])
|
||||
# assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
|
||||
# assert recipe_data["recipeInstructions"] == [
|
||||
# {"text": "Could not detect instructions"}
|
||||
# ]
|
||||
assert len(recipe_data["name"]) > 10
|
||||
assert len(recipe_data["slug"]) > 10
|
||||
assert recipe_data["orgURL"] == url
|
||||
assert len(recipe_data["description"]) > 100
|
||||
assert url_validation_regex.match(recipe_data["image"])
|
||||
assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
|
||||
assert recipe_data["recipeInstructions"] == [
|
||||
{"text": "Could not detect instructions"}
|
||||
]
|
||||
|
||||
|
||||
def test_html_with_recipe_data():
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user