mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
v0.2.0 Updates (#130)
* migration redesign init * new color picker * changelog * added UI language selection * fix layout issue on recipe editor * remove git as dependency * added UI editor for original URL * CI/CD Tests * test: fixed migration routes * test todos * bug/added docker volume * chowdow test data * partial image recipe image testing * added card section card * settings form * homepage cetegory ui * frontend category placeholder * fixed broken scheduler * remove old files * removed temp test Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
parent
ce48ae61c7
commit
874bea7fa4
42 changed files with 763 additions and 746 deletions
37
Dockerfile
37
Dockerfile
|
@ -7,7 +7,7 @@ RUN npm run build
|
||||||
|
|
||||||
FROM python:3.8-alpine
|
FROM python:3.8-alpine
|
||||||
|
|
||||||
RUN apk add --no-cache git curl libxml2-dev libxslt-dev libxml2
|
RUN apk add --no-cache libxml2-dev libxslt-dev libxml2
|
||||||
ENV ENV prod
|
ENV ENV prod
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
@ -15,6 +15,7 @@ WORKDIR /app
|
||||||
COPY ./pyproject.toml /app/
|
COPY ./pyproject.toml /app/
|
||||||
|
|
||||||
RUN apk add --update --no-cache --virtual .build-deps \
|
RUN apk add --update --no-cache --virtual .build-deps \
|
||||||
|
curl \
|
||||||
g++ \
|
g++ \
|
||||||
py-lxml \
|
py-lxml \
|
||||||
python3-dev \
|
python3-dev \
|
||||||
|
@ -34,35 +35,5 @@ COPY --from=build-stage /app/dist /app/dist
|
||||||
RUN rm -rf /app/test /app/.temp
|
RUN rm -rf /app/test /app/.temp
|
||||||
|
|
||||||
|
|
||||||
|
VOLUME [ "/app_data/" ]
|
||||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]
|
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]
|
||||||
|
|
||||||
# ---------------------------------- #
|
|
||||||
# Old Docker File
|
|
||||||
# ---------------------------------- #
|
|
||||||
|
|
||||||
# FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8-slim
|
|
||||||
# FROM mrnr91/uvicorn-gunicorn-fastapi:python3.8
|
|
||||||
|
|
||||||
|
|
||||||
# WORKDIR /app
|
|
||||||
|
|
||||||
# RUN apt-get update -y && \
|
|
||||||
# apt-get install -y python-pip python-dev git curl python3-dev libxml2-dev libxslt1-dev zlib1g-dev --no-install-recommends && \
|
|
||||||
# rm -rf /var/lib/apt/lists/* && \
|
|
||||||
# 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/
|
|
||||||
|
|
||||||
# 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" ]
|
|
|
@ -16,6 +16,11 @@ Don't forget to [join the Discord](https://discord.gg/R6QDyJgbD2)!
|
||||||
|
|
||||||
# Todo's
|
# Todo's
|
||||||
|
|
||||||
|
Test
|
||||||
|
- [ ] Image Upload Test
|
||||||
|
- [ ] Rename and Upload Image Test
|
||||||
|
- [x] Chowdown Migration End Point Test
|
||||||
|
|
||||||
Frontend
|
Frontend
|
||||||
- [ ] No Meal Today Page instead of Null
|
- [ ] No Meal Today Page instead of Null
|
||||||
- [ ] Recipe Print Page
|
- [ ] Recipe Print Page
|
||||||
|
|
173
frontend/package-lock.json
generated
173
frontend/package-lock.json
generated
|
@ -1966,6 +1966,16 @@
|
||||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"cacache": {
|
"cacache": {
|
||||||
"version": "13.0.1",
|
"version": "13.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
|
||||||
|
@ -1992,6 +2002,53 @@
|
||||||
"unique-filename": "^1.1.1"
|
"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
|
||||||
|
},
|
||||||
|
"loader-utils": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"big.js": "^5.2.2",
|
||||||
|
"emojis-list": "^3.0.0",
|
||||||
|
"json5": "^2.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"source-map": {
|
"source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
@ -2008,6 +2065,16 @@
|
||||||
"minipass": "^3.1.1"
|
"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": {
|
"terser-webpack-plugin": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
|
||||||
|
@ -2024,6 +2091,18 @@
|
||||||
"terser": "^4.6.12",
|
"terser": "^4.6.12",
|
||||||
"webpack-sources": "^1.4.3"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -10175,6 +10254,11 @@
|
||||||
"is-plain-obj": "^1.0.0"
|
"is-plain-obj": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sortablejs": {
|
||||||
|
"version": "1.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
|
||||||
|
"integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
|
||||||
|
},
|
||||||
"source-list-map": {
|
"source-list-map": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
||||||
|
@ -11521,87 +11605,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"color-convert": "^2.0.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
|
|
||||||
},
|
|
||||||
"loader-utils": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"big.js": "^5.2.2",
|
|
||||||
"emojis-list": "^3.0.0",
|
|
||||||
"json5": "^2.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vue-router": {
|
"vue-router": {
|
||||||
"version": "3.4.9",
|
"version": "3.4.9",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",
|
||||||
|
@ -11641,6 +11644,14 @@
|
||||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"vuedraggable": {
|
||||||
|
"version": "2.24.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",
|
||||||
|
"integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
|
||||||
|
"requires": {
|
||||||
|
"sortablejs": "1.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"vuetify": {
|
"vuetify": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.4.2.tgz",
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-i18n": "^8.22.4",
|
"vue-i18n": "^8.22.4",
|
||||||
"vue-router": "^3.4.9",
|
"vue-router": "^3.4.9",
|
||||||
|
"vuedraggable": "^2.24.3",
|
||||||
"vuetify": "^2.4.2",
|
"vuetify": "^2.4.2",
|
||||||
"vuex": "^3.6.0",
|
"vuex": "^3.6.0",
|
||||||
"vuex-persistedstate": "^4.0.0-beta.3"
|
"vuex-persistedstate": "^4.0.0-beta.3"
|
||||||
|
@ -61,4 +62,4 @@
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": false
|
"singleQuote": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
@selected="navigateFromSearch"
|
@selected="navigateFromSearch"
|
||||||
/>
|
/>
|
||||||
</v-expand-x-transition>
|
</v-expand-x-transition>
|
||||||
<v-btn icon @click="toggleSearch">
|
<v-btn icon @click="search = !search">
|
||||||
<v-icon>mdi-magnify</v-icon>
|
<v-icon>mdi-magnify</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
|
@ -34,11 +34,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Menu from "./components/UI/Menu"
|
import Menu from "./components/UI/Menu";
|
||||||
import SearchBar from "./components/UI/SearchBar"
|
import SearchBar from "./components/UI/SearchBar";
|
||||||
import AddRecipeFab from "./components/UI/AddRecipeFab"
|
import AddRecipeFab from "./components/UI/AddRecipeFab";
|
||||||
import SnackBar from "./components/UI/SnackBar"
|
import SnackBar from "./components/UI/SnackBar";
|
||||||
import Vuetify from "./plugins/vuetify"
|
import Vuetify from "./plugins/vuetify";
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
|
|
||||||
|
@ -51,16 +51,17 @@ export default {
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
$route() {
|
$route() {
|
||||||
this.search = false
|
this.search = false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch("initTheme")
|
this.$store.dispatch("initTheme");
|
||||||
this.$store.dispatch("requestRecentRecipes")
|
this.$store.dispatch("requestRecentRecipes");
|
||||||
this.$store.dispatch("initLang")
|
this.$store.dispatch("requestHomePageSettings");
|
||||||
this.darkModeSystemCheck()
|
this.$store.dispatch("initLang");
|
||||||
this.darkModeAddEventListener()
|
this.darkModeSystemCheck();
|
||||||
|
this.darkModeAddEventListener();
|
||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
@ -74,30 +75,22 @@ export default {
|
||||||
if (this.$store.getters.getDarkMode === "system")
|
if (this.$store.getters.getDarkMode === "system")
|
||||||
Vuetify.framework.theme.dark = window.matchMedia(
|
Vuetify.framework.theme.dark = window.matchMedia(
|
||||||
"(prefers-color-scheme: dark)"
|
"(prefers-color-scheme: dark)"
|
||||||
).matches
|
).matches;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* This will monitor the OS level darkmode and call to update dark mode.
|
* This will monitor the OS level darkmode and call to update dark mode.
|
||||||
*/
|
*/
|
||||||
darkModeAddEventListener() {
|
darkModeAddEventListener() {
|
||||||
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
darkMediaQuery.addEventListener("change", () => {
|
darkMediaQuery.addEventListener("change", () => {
|
||||||
this.darkModeSystemCheck()
|
this.darkModeSystemCheck();
|
||||||
})
|
});
|
||||||
},
|
|
||||||
|
|
||||||
toggleSearch() {
|
|
||||||
if (this.search === true) {
|
|
||||||
this.search = false
|
|
||||||
} else {
|
|
||||||
this.search = true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
navigateFromSearch(slug) {
|
navigateFromSearch(slug) {
|
||||||
this.$router.push(`/recipe/${slug}`)
|
this.$router.push(`/recipe/${slug}`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<ImportDialog
|
|
||||||
:name="selectedName"
|
|
||||||
:date="selectedDate"
|
|
||||||
ref="import_dialog"
|
|
||||||
@import="importBackup"
|
|
||||||
@delete="deleteBackup"
|
|
||||||
/>
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
:sm="6"
|
|
||||||
:md="6"
|
|
||||||
:lg="4"
|
|
||||||
:xl="4"
|
|
||||||
v-for="backup in backups"
|
|
||||||
:key="backup.name"
|
|
||||||
>
|
|
||||||
<v-card @click="openDialog(backup)">
|
|
||||||
<v-card-text>
|
|
||||||
<v-row align="center">
|
|
||||||
<v-col cols="12" sm="2">
|
|
||||||
<v-icon color="primary"> mdi-backup-restore </v-icon>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" sm="10">
|
|
||||||
<div>
|
|
||||||
<strong>{{ backup.name }}</strong>
|
|
||||||
</div>
|
|
||||||
<div>{{ readableTime(backup.date) }}</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import ImportDialog from "./ImportDialog";
|
|
||||||
import api from "../../../api";
|
|
||||||
import utils from "../../../utils";
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
backups: Array,
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
ImportDialog,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selectedName: "",
|
|
||||||
selectedDate: "",
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openDialog(backup) {
|
|
||||||
this.selectedDate = this.readableTime(backup.date);
|
|
||||||
this.selectedName = backup.name;
|
|
||||||
this.$refs.import_dialog.open();
|
|
||||||
},
|
|
||||||
readableTime(timestamp) {
|
|
||||||
let date = new Date(timestamp);
|
|
||||||
return utils.getDateAsText(date);
|
|
||||||
},
|
|
||||||
async importBackup(data) {
|
|
||||||
this.$emit("loading");
|
|
||||||
let response = await api.backups.import(data.name, data);
|
|
||||||
|
|
||||||
let failed = response.data.failed;
|
|
||||||
let succesful = response.data.successful;
|
|
||||||
|
|
||||||
this.$emit("finished", succesful, failed);
|
|
||||||
},
|
|
||||||
deleteBackup(data) {
|
|
||||||
this.$emit("loading");
|
|
||||||
|
|
||||||
api.backups.delete(data.name);
|
|
||||||
this.selectedBackup = null;
|
|
||||||
this.backupLoading = false;
|
|
||||||
|
|
||||||
this.$emit("finished");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
158
frontend/src/components/Settings/General/HomePageSettings.vue
Normal file
158
frontend/src/components/Settings/General/HomePageSettings.vue
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
<template>
|
||||||
|
<v-card flat>
|
||||||
|
<v-card-text>
|
||||||
|
<h2 class="mt-1 mb-1">Home Page</h2>
|
||||||
|
<v-row align="center" justify="center" dense class="mb-n7 pb-n5">
|
||||||
|
<v-col sm="2">
|
||||||
|
<v-switch v-model="showRecent" label="Show Recent"></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<v-slider
|
||||||
|
class="pt-4"
|
||||||
|
label="Card Per Section"
|
||||||
|
v-model="showLimit"
|
||||||
|
max="30"
|
||||||
|
dense
|
||||||
|
color="primary"
|
||||||
|
min="3"
|
||||||
|
thumb-label
|
||||||
|
>
|
||||||
|
</v-slider>
|
||||||
|
</v-col>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-card outlined min-height="250">
|
||||||
|
<v-card-text class="pt-2 pb-1">
|
||||||
|
<h3>Homepage Categories</h3>
|
||||||
|
</v-card-text>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<v-list min-height="200px" dense>
|
||||||
|
<v-list-item-group>
|
||||||
|
<draggable
|
||||||
|
v-model="homeCategories"
|
||||||
|
group="categories"
|
||||||
|
:style="{
|
||||||
|
minHeight: `200px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
v-for="(item, index) in homeCategories"
|
||||||
|
:key="item"
|
||||||
|
>
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-menu</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title v-text="item"></v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-icon @click="deleteActiveCategory(index)">
|
||||||
|
<v-icon>mdi-delete</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
</v-list-item>
|
||||||
|
</draggable>
|
||||||
|
</v-list-item-group>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<v-card outlined min-height="250px">
|
||||||
|
<v-card-text class="pt-2 pb-1">
|
||||||
|
<h3>
|
||||||
|
All Categories
|
||||||
|
<span>
|
||||||
|
<v-btn absolute right x-small color="success" icon>
|
||||||
|
<v-icon>mdi-plus</v-icon></v-btn
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
</v-card-text>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<v-list min-height="200px" dense>
|
||||||
|
<v-list-item-group>
|
||||||
|
<draggable
|
||||||
|
v-model="categories"
|
||||||
|
group="categories"
|
||||||
|
:style="{
|
||||||
|
minHeight: `200px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<v-list-item v-for="item in categories" :key="item">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-menu</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title v-text="item"></v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-icon @click="deleteActiveCategory(index)">
|
||||||
|
<v-icon>mdi-delete</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
</v-list-item>
|
||||||
|
</draggable>
|
||||||
|
</v-list-item-group>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="success" @click="saveSettings" class="mr-2">
|
||||||
|
<v-icon left> mdi-content-save </v-icon>
|
||||||
|
{{ $t("general.save") }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import draggable from "vuedraggable";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
draggable,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
homeCategories: [],
|
||||||
|
showLimit: null,
|
||||||
|
categories: ["breakfast"],
|
||||||
|
showRecent: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getOptions();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getOptions() {
|
||||||
|
let options = this.$store.getters.getHomePageSettings;
|
||||||
|
this.showLimit = options.showLimit;
|
||||||
|
this.categories = options.categories;
|
||||||
|
this.showRecent = options.showRecent;
|
||||||
|
this.homeCategories = options.homeCategories;
|
||||||
|
},
|
||||||
|
deleteActiveCategory(index) {
|
||||||
|
this.homeCategories.splice(index, 1);
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
let payload = {
|
||||||
|
showRecent: this.showRecent,
|
||||||
|
showLimit: this.showLimit,
|
||||||
|
categories: this.categories,
|
||||||
|
homeCategories: this.homeCategories,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$store.commit("setHomePageSettings", payload);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -1,8 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title> General Settings </v-card-title>
|
<v-card-title>
|
||||||
|
General Settings
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<span>
|
||||||
|
<v-btn class="pt-1" text href="/docs">
|
||||||
|
<v-icon left>mdi-link</v-icon>
|
||||||
|
Local API
|
||||||
|
</v-btn>
|
||||||
|
</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<HomePageSettings />
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
|
<h2 class="mt-1 mb-1">Language</h2>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-select
|
<v-select
|
||||||
|
@ -18,21 +30,35 @@
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
<v-divider></v-divider>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import HomePageSettings from "./HomePageSettings";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
HomePageSettings,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
categories: ["cat 1", "cat 2", "cat 3"],
|
||||||
|
usedCategories: ["recent"],
|
||||||
langOptions: [],
|
langOptions: [],
|
||||||
selectedLang: "en",
|
selectedLang: "en",
|
||||||
|
homeOptions: {
|
||||||
|
recipesToShow: 10,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getOptions();
|
this.getOptions();
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
usedCategories() {
|
||||||
|
console.log(this.usedCategories);
|
||||||
|
},
|
||||||
selectedLang() {
|
selectedLang() {
|
||||||
this.$store.commit("setLang", this.selectedLang);
|
this.$store.commit("setLang", this.selectedLang);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-card-text>
|
|
||||||
<p>
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
"migration.currently-chowdown-via-public-repo-url-is-the-only-supported-type-of-migration"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
<v-form ref="form">
|
|
||||||
<v-row dense align="center">
|
|
||||||
<v-col cols="12" md="5" sm="5">
|
|
||||||
<v-text-field
|
|
||||||
v-model="repo"
|
|
||||||
:label="$t('migration.chowdown-repo-url')"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="4" sm="5">
|
|
||||||
<v-btn text color="info" @click="importRepo">
|
|
||||||
<v-icon left> mdi-import </v-icon>
|
|
||||||
{{ $t("migration.migrate") }}
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-form>
|
|
||||||
<v-alert v-if="failedRecipes[1]" outlined dense type="error">
|
|
||||||
<h4>{{ $t("migration.failed-recipes") }}</h4>
|
|
||||||
<v-list dense>
|
|
||||||
<v-list-item v-for="fail in this.failedRecipes" :key="fail">
|
|
||||||
{{ fail }}
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-alert>
|
|
||||||
<v-alert v-if="failedImages[1]" outlined dense type="error">
|
|
||||||
<h4>{{ $t("migration.failed-images") }}</h4>
|
|
||||||
<v-list dense>
|
|
||||||
<v-list-item v-for="fail in this.failedImages" :key="fail">
|
|
||||||
{{ fail }}
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-alert>
|
|
||||||
</v-card-text>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import api from "../../../api";
|
|
||||||
// import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
|
||||||
// import TimePicker from "./Webhooks/TimePicker";
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
processRan: false,
|
|
||||||
failedImages: [],
|
|
||||||
failedRecipes: [],
|
|
||||||
repo: "",
|
|
||||||
rules: {
|
|
||||||
required: v => !!v || "Selection Required",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async importRepo() {
|
|
||||||
if (this.$refs.form.validate()) {
|
|
||||||
this.$emit("loading");
|
|
||||||
let response = await api.migrations.migrateChowdown(this.repo);
|
|
||||||
this.failedImages = response.failedImages;
|
|
||||||
this.failedRecipes = response.failedRecipes;
|
|
||||||
this.$emit("finished");
|
|
||||||
this.processRan = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
|
@ -1,112 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-card-text>
|
|
||||||
<p>
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
"migration.you-can-import-recipes-from-either-a-zip-file-or-a-directory-located-in-the-app-data-migraiton-folder-please-review-the-documentation-to-ensure-your-directory-structure-matches-what-is-expected"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
<v-form ref="form">
|
|
||||||
<v-row align="center">
|
|
||||||
<v-col cols="12" md="5" sm="12">
|
|
||||||
<v-select
|
|
||||||
:items="availableImports"
|
|
||||||
v-model="selectedImport"
|
|
||||||
:label="$t('migration.nextcloud-data')"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
></v-select>
|
|
||||||
</v-col>
|
|
||||||
<v-col md="1" sm="12">
|
|
||||||
<v-btn-toggle group>
|
|
||||||
<v-btn text color="info" @click="importRecipes">
|
|
||||||
<v-icon left> mdi-import </v-icon>
|
|
||||||
{{ $t("migration.migrate") }}
|
|
||||||
</v-btn>
|
|
||||||
<v-btn text color="error" @click="deleteImportValidation">
|
|
||||||
<v-icon left> mdi-delete </v-icon>
|
|
||||||
{{ $t("general.delete") }}
|
|
||||||
</v-btn>
|
|
||||||
<UploadBtn
|
|
||||||
url="/api/migration/upload/"
|
|
||||||
class="mt-1"
|
|
||||||
@uploaded="getAvaiableImports"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Confirmation
|
|
||||||
:title="$t('general.delete-data')"
|
|
||||||
:message="$t('migration.delete-confirmation')"
|
|
||||||
color="error"
|
|
||||||
icon="mdi-alert-circle"
|
|
||||||
ref="deleteThemeConfirm"
|
|
||||||
v-on:confirm="deleteImport()"
|
|
||||||
/>
|
|
||||||
</v-btn-toggle>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-row>
|
|
||||||
</v-form>
|
|
||||||
<SuccessFailureAlert
|
|
||||||
:success-header="$t('migration.successfully-imported-from-nextcloud')"
|
|
||||||
:success="successfulImports"
|
|
||||||
failed-header="$t('migration.failed-imports')"
|
|
||||||
:failed="failedImports"
|
|
||||||
/>
|
|
||||||
</v-card-text>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import api from "../../../api";
|
|
||||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
|
||||||
import Confirmation from "../../UI/Confirmation";
|
|
||||||
import UploadBtn from "../../UI/UploadBtn";
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
SuccessFailureAlert,
|
|
||||||
Confirmation,
|
|
||||||
UploadBtn,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
successfulImports: [],
|
|
||||||
failedImports: [],
|
|
||||||
availableImports: [],
|
|
||||||
selectedImport: null,
|
|
||||||
rules: {
|
|
||||||
required: v => !!v || "Selection Required",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
this.getAvaiableImports();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async getAvaiableImports() {
|
|
||||||
this.availableImports = await api.migrations.getNextcloudImports();
|
|
||||||
},
|
|
||||||
async importRecipes() {
|
|
||||||
if (this.$refs.form.validate()) {
|
|
||||||
this.$emit("loading");
|
|
||||||
let data = await api.migrations.importNextcloud(this.selectedImport);
|
|
||||||
|
|
||||||
this.successfulImports = data.successful;
|
|
||||||
this.failedImports = data.failed;
|
|
||||||
this.$emit("finished");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteImportValidation() {
|
|
||||||
if (this.$refs.form.validate()) {
|
|
||||||
this.$refs.deleteThemeConfirm.open();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async deleteImport() {
|
|
||||||
await api.migrations.delete(this.selectedImport);
|
|
||||||
this.getAvaiableImports();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
|
@ -1,49 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-form ref="file">
|
|
||||||
<v-file-input
|
|
||||||
:loading="loading"
|
|
||||||
:label="$t('migration.upload-an-archive')"
|
|
||||||
v-model="file"
|
|
||||||
accept=".zip"
|
|
||||||
@change="upload"
|
|
||||||
:prepend-icon="icon"
|
|
||||||
class="file-icon"
|
|
||||||
>
|
|
||||||
</v-file-input>
|
|
||||||
</v-form>
|
|
||||||
</template>c
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import api from "../../../api";
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
file: null,
|
|
||||||
loading: false,
|
|
||||||
icon: "mdi-paperclip",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async upload() {
|
|
||||||
if (this.file != null) {
|
|
||||||
this.loading = true;
|
|
||||||
let formData = new FormData();
|
|
||||||
formData.append("archive", this.file);
|
|
||||||
|
|
||||||
await api.migrations.uploadFile(formData);
|
|
||||||
|
|
||||||
this.loading = false;
|
|
||||||
this.$emit("uploaded");
|
|
||||||
this.file = null;
|
|
||||||
this.icon = "mdi-check";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.file-icon {
|
|
||||||
transition-duration: 5s;
|
|
||||||
}
|
|
||||||
</style>
|
|
81
frontend/src/components/UI/CardSection.vue
Normal file
81
frontend/src/components/UI/CardSection.vue
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<div class="mt-n5">
|
||||||
|
<v-card flat class="transparent" height="60px">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-btn-toggle group>
|
||||||
|
<v-btn text :to="`/recipes/category/${title.toLowerCase()}`">
|
||||||
|
{{ title.toUpperCase() }}
|
||||||
|
</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
</v-col>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-col align="end">
|
||||||
|
<v-menu offset-y v-if="sortable">
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<v-btn-toggle group>
|
||||||
|
<v-btn text v-bind="attrs" v-on="on"> Sort </v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item @click="$emit('sort-recent')">
|
||||||
|
<v-list-item-title> Recent </v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="$emit('sort')">
|
||||||
|
<v-list-item-title> A-Z </v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
:sm="6"
|
||||||
|
:md="6"
|
||||||
|
:lg="4"
|
||||||
|
:xl="3"
|
||||||
|
v-for="recipe in recipes.slice(0, cardLimit)"
|
||||||
|
:key="recipe.name"
|
||||||
|
>
|
||||||
|
<RecipeCard
|
||||||
|
:name="recipe.name"
|
||||||
|
:description="recipe.description"
|
||||||
|
:slug="recipe.slug"
|
||||||
|
:rating="recipe.rating"
|
||||||
|
:image="recipe.image"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import RecipeCard from "./RecipeCard";
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
RecipeCard,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
sortable: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
title: String,
|
||||||
|
recipes: Array,
|
||||||
|
cardLimit: {
|
||||||
|
default: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.transparent {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,40 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
:sm="6"
|
|
||||||
:md="6"
|
|
||||||
:lg="4"
|
|
||||||
:xl="3"
|
|
||||||
v-for="recipe in recipes"
|
|
||||||
:key="recipe.name"
|
|
||||||
>
|
|
||||||
<RecipeCard
|
|
||||||
:name="recipe.name"
|
|
||||||
:description="recipe.description"
|
|
||||||
:slug="recipe.slug"
|
|
||||||
:rating="recipe.rating"
|
|
||||||
:image="recipe.image"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import RecipeCard from "./RecipeCard";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
RecipeCard,
|
|
||||||
},
|
|
||||||
data: () => ({}),
|
|
||||||
mounted() {},
|
|
||||||
computed: {
|
|
||||||
recipes() {
|
|
||||||
return this.$store.getters.getRecentRecipes;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
|
@ -1,63 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-autocomplete
|
|
||||||
:items="items"
|
|
||||||
:loading="isLoading"
|
|
||||||
v-model="selected"
|
|
||||||
clearable
|
|
||||||
return
|
|
||||||
dense
|
|
||||||
hide-details
|
|
||||||
hide-selected
|
|
||||||
item-text="slug"
|
|
||||||
:label="$t('search.search-for-a-recipe')"
|
|
||||||
single-line
|
|
||||||
@keyup.enter.native="moreInfo(selected)"
|
|
||||||
>
|
|
||||||
<template v-slot:no-data>
|
|
||||||
<v-list-item>
|
|
||||||
<v-list-item-title :v-html="$t('search.search-for-your-favorite-recipe')">
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
<template v-slot:item="{ item }">
|
|
||||||
<v-list-item-avatar
|
|
||||||
color="primary"
|
|
||||||
class="headline font-weight-light white--text"
|
|
||||||
>
|
|
||||||
<v-img :src="getImage(item.image)"></v-img>
|
|
||||||
</v-list-item-avatar>
|
|
||||||
<v-list-item-content @click="moreInfo(item.slug)">
|
|
||||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
|
||||||
</v-list-item-content>
|
|
||||||
</template>
|
|
||||||
</v-autocomplete>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import utils from "../../utils";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data: () => ({
|
|
||||||
selected: null,
|
|
||||||
isLoading: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
items() {
|
|
||||||
return this.$store.getters.getRecentRecipes;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
moreInfo(recipeSlug) {
|
|
||||||
this.$router.push(`/recipe/${recipeSlug}`);
|
|
||||||
},
|
|
||||||
getImage(image) {
|
|
||||||
return utils.getImageURL(image);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
|
@ -1,15 +1,66 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<RecentRecipes />
|
<CardSection
|
||||||
|
v-if="pageSettings.showRecent"
|
||||||
|
title="Recent"
|
||||||
|
:recipes="recentRecipes"
|
||||||
|
:card-limit="pageSettings.showLimit"
|
||||||
|
/>
|
||||||
|
<CardSection
|
||||||
|
:sortable="true"
|
||||||
|
v-for="(section, index) in recipeByCategory"
|
||||||
|
:key="index"
|
||||||
|
:title="section.title"
|
||||||
|
:recipes="section.recipes"
|
||||||
|
:card-limit="pageSettings.showLimit"
|
||||||
|
@sort="sortAZ(index)"
|
||||||
|
@sort-recent="sortRecent(index)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import RecentRecipes from "../components/UI/RecentRecipes";
|
import CardSection from "../components/UI/CardSection";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
RecentRecipes,
|
CardSection,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
recipeByCategory: [
|
||||||
|
{
|
||||||
|
title: "Title 1",
|
||||||
|
recipes: this.$store.getters.getRecentRecipes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Title 2",
|
||||||
|
recipes: this.$store.getters.getRecentRecipes,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pageSettings() {
|
||||||
|
return this.$store.getters.getHomePageSettings;
|
||||||
|
},
|
||||||
|
recentRecipes() {
|
||||||
|
return this.$store.getters.getRecentRecipes;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getRecentRecipes() {
|
||||||
|
this.$store.dispatch("requestRecentRecipes");
|
||||||
|
},
|
||||||
|
sortAZ(index) {
|
||||||
|
this.recipeByCategory[index].recipes.sort((a, b) =>
|
||||||
|
a.name > b.name ? 1 : -1
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sortRecent(index) {
|
||||||
|
this.recipeByCategory[index].recipes.sort((a, b) =>
|
||||||
|
a.dateAdded > b.dateAdded ? -1 : 1
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
0
frontend/src/store/modules/recipes.js
Normal file
0
frontend/src/store/modules/recipes.js
Normal file
|
@ -18,6 +18,13 @@ const store = new Vuex.Store({
|
||||||
language,
|
language,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
|
// Home Page Settings
|
||||||
|
homePageSettings: {
|
||||||
|
showRecent: true,
|
||||||
|
showLimit: 9,
|
||||||
|
categories: [],
|
||||||
|
homeCategories: [],
|
||||||
|
},
|
||||||
// Snackbar
|
// Snackbar
|
||||||
snackActive: false,
|
snackActive: false,
|
||||||
snackText: "",
|
snackText: "",
|
||||||
|
@ -29,6 +36,9 @@ const store = new Vuex.Store({
|
||||||
},
|
},
|
||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
|
setHomePageSettings(state, payload) {
|
||||||
|
state.homePageSettings = payload;
|
||||||
|
},
|
||||||
setSnackBar(state, payload) {
|
setSnackBar(state, payload) {
|
||||||
state.snackText = payload.text;
|
state.snackText = payload.text;
|
||||||
state.snackType = payload.type;
|
state.snackType = payload.type;
|
||||||
|
@ -57,6 +67,16 @@ const store = new Vuex.Store({
|
||||||
|
|
||||||
this.commit("setRecentRecipes", payload);
|
this.commit("setRecentRecipes", payload);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async requestHomePageSettings() {
|
||||||
|
// TODO: Query Backend for Categories
|
||||||
|
this.commit("setHomePageSettings", {
|
||||||
|
showRecent: true,
|
||||||
|
showLimit: 9,
|
||||||
|
categories: ["breakfast", "lunch", "dinner"],
|
||||||
|
homeCategories: [],
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
|
@ -65,7 +85,8 @@ const store = new Vuex.Store({
|
||||||
getSnackActive: state => state.snackActive,
|
getSnackActive: state => state.snackActive,
|
||||||
getSnackType: state => state.snackType,
|
getSnackType: state => state.snackType,
|
||||||
|
|
||||||
getRecentRecipes: state => state.recentRecipes,
|
getRecentRecipes: (state) => state.recentRecipes,
|
||||||
|
getHomePageSettings: (state) => state.homePageSettings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ from fastapi import FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
# import utils.startup as startup
|
# import utils.startup as startup
|
||||||
from app_config import PORT, PRODUCTION, SQLITE_FILE, WEB_PATH, docs_url, redoc_url
|
from app_config import PORT, PRODUCTION, WEB_PATH, docs_url, redoc_url
|
||||||
from routes import (
|
from routes import (
|
||||||
backup_routes,
|
backup_routes,
|
||||||
meal_routes,
|
meal_routes,
|
||||||
|
@ -14,7 +14,7 @@ from routes import (
|
||||||
user_routes,
|
user_routes,
|
||||||
)
|
)
|
||||||
|
|
||||||
# from utils.api_docs import generate_api_docs
|
from utils.api_docs import generate_api_docs
|
||||||
from utils.logger import logger
|
from utils.logger import logger
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
|
@ -26,13 +26,13 @@ app = FastAPI(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def mount_static_files():
|
def mount_static_files():
|
||||||
app.mount("/static", StaticFiles(directory=WEB_PATH, html=True))
|
app.mount("/static", StaticFiles(directory=WEB_PATH, html=True))
|
||||||
|
|
||||||
|
|
||||||
def api_routers():
|
def api_routers():
|
||||||
# First
|
# First
|
||||||
|
print()
|
||||||
app.include_router(recipe_routes.router)
|
app.include_router(recipe_routes.router)
|
||||||
app.include_router(meal_routes.router)
|
app.include_router(meal_routes.router)
|
||||||
app.include_router(setting_routes.router)
|
app.include_router(setting_routes.router)
|
||||||
|
@ -46,6 +46,11 @@ if PRODUCTION:
|
||||||
|
|
||||||
api_routers()
|
api_routers()
|
||||||
|
|
||||||
|
|
||||||
|
def start_scheduler():
|
||||||
|
import services.scheduler.scheduled_jobs
|
||||||
|
|
||||||
|
|
||||||
# API 404 Catch all CALL AFTER ROUTERS
|
# API 404 Catch all CALL AFTER ROUTERS
|
||||||
@app.get("/api/{full_path:path}", status_code=404, include_in_schema=False)
|
@app.get("/api/{full_path:path}", status_code=404, include_in_schema=False)
|
||||||
def invalid_api():
|
def invalid_api():
|
||||||
|
@ -56,8 +61,10 @@ app.include_router(static_routes.router)
|
||||||
|
|
||||||
|
|
||||||
# Generate API Documentation
|
# Generate API Documentation
|
||||||
# if not PRODUCTION:
|
if not PRODUCTION:
|
||||||
# generate_api_docs(app)
|
generate_api_docs(app)
|
||||||
|
|
||||||
|
start_scheduler()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logger.info("-----SYSTEM STARTUP-----")
|
logger.info("-----SYSTEM STARTUP-----")
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from db.db_base import BaseDocument
|
from db.db_base import BaseDocument
|
||||||
from db.sql.meal_models import MealPlanModel
|
from db.sql.meal_models import MealPlanModel
|
||||||
from db.sql.recipe_models import RecipeModel
|
from db.sql.recipe_models import RecipeModel
|
||||||
|
@ -16,8 +18,12 @@ class _Recipes(BaseDocument):
|
||||||
self.primary_key = "slug"
|
self.primary_key = "slug"
|
||||||
self.sql_model = RecipeModel
|
self.sql_model = RecipeModel
|
||||||
|
|
||||||
def update_image(self, slug: str, extension: str) -> None:
|
def update_image(self, session: Session, slug: str, extension: str) -> str:
|
||||||
pass
|
entry = self._query_one(session, match_value=slug)
|
||||||
|
entry.image = f"{slug}.{extension}"
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return f"{slug}.{extension}"
|
||||||
|
|
||||||
|
|
||||||
class _Meals(BaseDocument):
|
class _Meals(BaseDocument):
|
||||||
|
@ -31,7 +37,7 @@ class _Settings(BaseDocument):
|
||||||
self.primary_key = "name"
|
self.primary_key = "name"
|
||||||
self.sql_model = SiteSettingsModel
|
self.sql_model = SiteSettingsModel
|
||||||
|
|
||||||
def save_new(self, 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)
|
new_settings = self.sql_model(main.get("name"), webhooks)
|
||||||
|
|
||||||
session.add(new_settings)
|
session.add(new_settings)
|
||||||
|
@ -45,14 +51,6 @@ class _Themes(BaseDocument):
|
||||||
self.primary_key = "name"
|
self.primary_key = "name"
|
||||||
self.sql_model = SiteThemeModel
|
self.sql_model = SiteThemeModel
|
||||||
|
|
||||||
def update(self, session, data: dict) -> dict:
|
|
||||||
theme_model = self._query_one(
|
|
||||||
session=session, match_value=data["name"], match_key="name"
|
|
||||||
)
|
|
||||||
|
|
||||||
theme_model.update(**data)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Union
|
from typing import List, Union
|
||||||
|
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
@ -11,7 +11,10 @@ class BaseDocument:
|
||||||
self.store: str
|
self.store: str
|
||||||
self.sql_model: SqlAlchemyBase
|
self.sql_model: SqlAlchemyBase
|
||||||
|
|
||||||
def get_all(self, session: Session, limit: int = None, order_by: str = None):
|
# TODO: Improve Get All Query Functionality
|
||||||
|
def get_all(
|
||||||
|
self, session: Session, limit: int = None, order_by: str = None
|
||||||
|
) -> List[dict]:
|
||||||
list = [x.dict() for x in session.query(self.sql_model).all()]
|
list = [x.dict() for x in session.query(self.sql_model).all()]
|
||||||
|
|
||||||
if limit == 1:
|
if limit == 1:
|
||||||
|
@ -21,7 +24,7 @@ class BaseDocument:
|
||||||
|
|
||||||
def _query_one(
|
def _query_one(
|
||||||
self, session: Session, match_value: str, match_key: str = None
|
self, session: Session, match_value: str, match_key: str = None
|
||||||
) -> Union[Session, SqlAlchemyBase]:
|
) -> SqlAlchemyBase:
|
||||||
"""Query the sql database for one item an return the sql alchemy model
|
"""Query the sql database for one item an return the sql alchemy model
|
||||||
object. If no match key is provided the primary_key attribute will be used.
|
object. If no match key is provided the primary_key attribute will be used.
|
||||||
|
|
||||||
|
@ -43,7 +46,7 @@ class BaseDocument:
|
||||||
|
|
||||||
def get(
|
def get(
|
||||||
self, session: Session, match_value: str, match_key: str = None, limit=1
|
self, session: Session, match_value: str, match_key: str = None, limit=1
|
||||||
) -> dict or list[dict]:
|
) -> dict or List[dict]:
|
||||||
"""Retrieves an entry from the database by matching a key/value pair. If no
|
"""Retrieves an entry from the database by matching a key/value pair. If no
|
||||||
key is provided the class objects primary key will be used to match against.
|
key is provided the class objects primary key will be used to match against.
|
||||||
|
|
||||||
|
@ -67,6 +70,15 @@ class BaseDocument:
|
||||||
return db_entry
|
return db_entry
|
||||||
|
|
||||||
def save_new(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:
|
||||||
|
session (Session): A Database Session
|
||||||
|
document (dict): A python dictionary representing the data structure
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary representation of the database entry
|
||||||
|
"""
|
||||||
new_document = self.sql_model(**document)
|
new_document = self.sql_model(**document)
|
||||||
session.add(new_document)
|
session.add(new_document)
|
||||||
return_data = new_document.dict()
|
return_data = new_document.dict()
|
||||||
|
@ -74,7 +86,18 @@ class BaseDocument:
|
||||||
|
|
||||||
return return_data
|
return return_data
|
||||||
|
|
||||||
def update(self, session: Session, match_value, new_data) -> dict:
|
def update(self, session: Session, match_value: str, new_data: str) -> dict:
|
||||||
|
"""Update a database entry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session (Session): Database Session
|
||||||
|
match_value (str): Match "key"
|
||||||
|
new_data (str): Match "value"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Returns a dictionary representation of the database entry
|
||||||
|
"""
|
||||||
|
|
||||||
entry = self._query_one(session=session, match_value=match_value)
|
entry = self._query_one(session=session, match_value=match_value)
|
||||||
entry.update(session=session, **new_data)
|
entry.update(session=session, **new_data)
|
||||||
return_data = entry.dict()
|
return_data = entry.dict()
|
||||||
|
|
|
@ -12,7 +12,7 @@ class SiteThemeModel(SqlAlchemyBase):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.colors = ThemeColorsModel(**colors)
|
self.colors = ThemeColorsModel(**colors)
|
||||||
|
|
||||||
def update(self, name, colors: dict) -> dict:
|
def update(self, session=None, name: str = None, colors: dict = None) -> dict:
|
||||||
self.colors.update(**colors)
|
self.colors.update(**colors)
|
||||||
return self.dict()
|
return self.dict()
|
||||||
|
|
||||||
|
|
|
@ -22,13 +22,13 @@ class BackupOptions(BaseModel):
|
||||||
class BackupJob(BaseModel):
|
class BackupJob(BaseModel):
|
||||||
tag: Optional[str]
|
tag: Optional[str]
|
||||||
options: BackupOptions
|
options: BackupOptions
|
||||||
templates: Optional[List[str]] = []
|
templates: Optional[List[str]]
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
schema_extra = {
|
schema_extra = {
|
||||||
"example": {
|
"example": {
|
||||||
"tag": "July 23rd 2021",
|
"tag": "July 23rd 2021",
|
||||||
"options": BackupOptions,
|
"options": BackupOptions(),
|
||||||
"template": ["recipes.md"],
|
"template": ["recipes.md"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,6 @@ def get_recipe_img(recipe_slug: str):
|
||||||
return FileResponse(recipe_image)
|
return FileResponse(recipe_image)
|
||||||
|
|
||||||
|
|
||||||
# Recipe Creations
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/api/recipe/create-url/",
|
"/api/recipe/create-url/",
|
||||||
status_code=201,
|
status_code=201,
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
from db.db_setup import generate_session
|
from db.db_setup import generate_session
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from services.scheduler_services import post_webhooks
|
|
||||||
from services.settings_services import SiteSettings, SiteTheme
|
from services.settings_services import SiteSettings, SiteTheme
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from utils.global_scheduler import scheduler
|
from utils.post_webhooks import post_webhooks
|
||||||
from utils.snackbar import SnackResponse
|
from utils.snackbar import SnackResponse
|
||||||
|
|
||||||
router = APIRouter(tags=["Settings"])
|
router = APIRouter(tags=["Settings"])
|
||||||
|
@ -34,7 +33,6 @@ def update_settings(data: SiteSettings, db: Session = Depends(generate_session))
|
||||||
# status_code=400, detail=SnackResponse.error("Unable to Save Settings")
|
# status_code=400, detail=SnackResponse.error("Unable to Save Settings")
|
||||||
# )
|
# )
|
||||||
|
|
||||||
# scheduler.reschedule_webhooks() #! Need to fix Scheduler
|
|
||||||
return SnackResponse.success("Settings Updated")
|
return SnackResponse.success("Settings Updated")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -82,15 +82,6 @@ class Recipe(BaseModel):
|
||||||
slug = calc_slug
|
slug = calc_slug
|
||||||
return slug
|
return slug
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _unpack_doc(cls, document):
|
|
||||||
document = json.loads(document.to_json())
|
|
||||||
del document["_id"]
|
|
||||||
|
|
||||||
document["dateAdded"] = document["dateAdded"]["$date"]
|
|
||||||
|
|
||||||
return cls(**document)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_slug(cls, session, slug: str):
|
def get_by_slug(cls, session, slug: str):
|
||||||
""" Returns a Recipe Object by Slug """
|
""" Returns a Recipe Object by Slug """
|
||||||
|
@ -132,8 +123,15 @@ class Recipe(BaseModel):
|
||||||
return updated_slug.get("slug")
|
return updated_slug.get("slug")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_image(slug: str, extension: str):
|
def update_image(slug: str, extension: str) -> str:
|
||||||
db.recipes.update_image(slug, extension)
|
"""A helper function to pass the new image name and extension
|
||||||
|
into the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slug (str): The current recipe slug
|
||||||
|
extension (str): the file extension of the new image
|
||||||
|
"""
|
||||||
|
return db.recipes.update_image(slug, extension)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all(session: Session):
|
def get_all(session: Session):
|
||||||
|
|
3
mealie/services/scheduler/global_scheduler.py
Normal file
3
mealie/services/scheduler/global_scheduler.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
|
||||||
|
scheduler = BackgroundScheduler()
|
62
mealie/services/scheduler/scheduled_jobs.py
Normal file
62
mealie/services/scheduler/scheduled_jobs.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
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 utils.post_webhooks import post_webhooks
|
||||||
|
|
||||||
|
|
||||||
|
@scheduler.scheduled_job(trigger="interval", minutes=15)
|
||||||
|
def update_webhook_schedule():
|
||||||
|
"""
|
||||||
|
A scheduled background job that runs every 15 minutes to
|
||||||
|
poll the database for changes and reschedule the webhook time
|
||||||
|
"""
|
||||||
|
session = create_session()
|
||||||
|
settings = SiteSettings.get_site_settings(session=session)
|
||||||
|
time = cron_parser(settings.webhooks.webhookTime)
|
||||||
|
job = JOB_STORE.get("webhooks")
|
||||||
|
|
||||||
|
scheduler.reschedule_job(
|
||||||
|
job.scheduled_task.id,
|
||||||
|
trigger="cron",
|
||||||
|
hour=time.hours,
|
||||||
|
minute=time.minutes,
|
||||||
|
)
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
logger.info(scheduler.print_jobs())
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduledFunction:
|
||||||
|
def __init__(
|
||||||
|
self, scheduler: BackgroundScheduler, function, cron: Cron, name: str
|
||||||
|
) -> None:
|
||||||
|
self.scheduled_task = scheduler.add_job(
|
||||||
|
function,
|
||||||
|
trigger="cron",
|
||||||
|
name=name,
|
||||||
|
hour=cron.hours,
|
||||||
|
minute=cron.minutes,
|
||||||
|
max_instances=1,
|
||||||
|
replace_existing=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("New Function Scheduled")
|
||||||
|
logger.info(scheduler.print_jobs())
|
||||||
|
|
||||||
|
|
||||||
|
logger.info("----INIT SCHEDULE OBJECT-----")
|
||||||
|
|
||||||
|
JOB_STORE = {
|
||||||
|
"backup_job": ScheduledFunction(
|
||||||
|
scheduler, auto_backup_job, Cron(hours=00, minutes=00), "backups"
|
||||||
|
),
|
||||||
|
"webhooks": ScheduledFunction(
|
||||||
|
scheduler, post_webhooks, Cron(hours=00, minutes=00), "webhooks"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler.start()
|
10
mealie/services/scheduler/scheduler_utils.py
Normal file
10
mealie/services/scheduler/scheduler_utils.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import collections
|
||||||
|
|
||||||
|
Cron = collections.namedtuple("Cron", "hours minutes")
|
||||||
|
|
||||||
|
|
||||||
|
def cron_parser(time_str: str) -> Cron:
|
||||||
|
time = time_str.split(":")
|
||||||
|
cron = Cron(hours=int(time[0]), minutes=int(time[1]))
|
||||||
|
|
||||||
|
return cron
|
|
@ -1,73 +0,0 @@
|
||||||
import collections
|
|
||||||
import json
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
|
||||||
from db.db_setup import create_session
|
|
||||||
from utils.logger import logger
|
|
||||||
|
|
||||||
from services.backups.exports import auto_backup_job
|
|
||||||
from services.meal_services import MealPlan
|
|
||||||
from services.recipe_services import Recipe
|
|
||||||
from services.settings_services import SiteSettings
|
|
||||||
|
|
||||||
Cron = collections.namedtuple("Cron", "hours minutes")
|
|
||||||
|
|
||||||
|
|
||||||
def cron_parser(time_str: str) -> Cron:
|
|
||||||
time = time_str.split(":")
|
|
||||||
cron = Cron(hours=int(time[0]), minutes=int(time[1]))
|
|
||||||
|
|
||||||
return cron
|
|
||||||
|
|
||||||
|
|
||||||
def post_webhooks():
|
|
||||||
all_settings = SiteSettings.get_site_settings()
|
|
||||||
|
|
||||||
if all_settings.webhooks.enabled:
|
|
||||||
todays_meal = Recipe.get_by_slug(MealPlan.today()).dict()
|
|
||||||
urls = all_settings.webhooks.webhookURLs
|
|
||||||
|
|
||||||
for url in urls:
|
|
||||||
requests.post(url, json.dumps(todays_meal, default=str))
|
|
||||||
|
|
||||||
|
|
||||||
class Scheduler:
|
|
||||||
def startup_scheduler(self):
|
|
||||||
self.scheduler = BackgroundScheduler()
|
|
||||||
logger.info("----INIT SCHEDULE OBJECT-----")
|
|
||||||
self.scheduler.start()
|
|
||||||
|
|
||||||
self.scheduler.add_job(
|
|
||||||
auto_backup_job, trigger="cron", hour="3", max_instances=1
|
|
||||||
)
|
|
||||||
settings = SiteSettings.get_site_settings(create_session())
|
|
||||||
time = cron_parser(settings.webhooks.webhookTime)
|
|
||||||
|
|
||||||
self.webhook = self.scheduler.add_job(
|
|
||||||
post_webhooks,
|
|
||||||
trigger="cron",
|
|
||||||
name="webhooks",
|
|
||||||
hour=time.hours,
|
|
||||||
minute=time.minutes,
|
|
||||||
max_instances=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(self.scheduler.print_jobs())
|
|
||||||
|
|
||||||
def reschedule_webhooks(self):
|
|
||||||
"""
|
|
||||||
Reads the site settings database entry to reschedule the webhooks task
|
|
||||||
Called after each post to the webhooks endpoint.
|
|
||||||
"""
|
|
||||||
settings = SiteSettings.get_site_settings()
|
|
||||||
time = cron_parser(settings.webhooks.webhookTime)
|
|
||||||
|
|
||||||
self.scheduler.reschedule_job(
|
|
||||||
self.webhook.id,
|
|
||||||
trigger="cron",
|
|
||||||
hour=time.hours,
|
|
||||||
minute=time.minutes,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(self.scheduler.print_jobs())
|
|
|
@ -1,7 +1,7 @@
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from db.database import db
|
from db.database import db
|
||||||
from db.db_setup import create_session, generate_session, sql_exists
|
from db.db_setup import create_session, sql_exists
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from utils.logger import logger
|
from utils.logger import logger
|
||||||
|
@ -103,7 +103,7 @@ class SiteTheme(BaseModel):
|
||||||
db.themes.save_new(session, self.dict())
|
db.themes.save_new(session, self.dict())
|
||||||
|
|
||||||
def update_document(self, session: Session):
|
def update_document(self, session: Session):
|
||||||
db.themes.update(session, self.dict())
|
db.themes.update(session, self.name, self.dict())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_theme(session: Session, theme_name: str) -> str:
|
def delete_theme(session: Session, theme_name: str) -> str:
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from app_config import SQLITE_DIR
|
from app_config import SQLITE_DIR
|
||||||
from db.db_setup import generate_session, sql_global_init
|
from db.db_setup import generate_session, sql_global_init
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from pytest import fixture
|
from pytest import fixture
|
||||||
|
|
||||||
|
from tests.test_config import TEST_DATA
|
||||||
|
|
||||||
SQLITE_FILE = SQLITE_DIR.joinpath("test.db")
|
SQLITE_FILE = SQLITE_DIR.joinpath("test.db")
|
||||||
SQLITE_FILE.unlink(missing_ok=True)
|
SQLITE_FILE.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
@ -26,3 +30,8 @@ def api_client():
|
||||||
yield TestClient(app)
|
yield TestClient(app)
|
||||||
|
|
||||||
SQLITE_FILE.unlink()
|
SQLITE_FILE.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
@fixture(scope="session")
|
||||||
|
def test_image():
|
||||||
|
return TEST_DATA.joinpath("test_image.jpg")
|
||||||
|
|
BIN
mealie/tests/data/migrations/chowdown/test_chowdown-gh-pages.zip
Normal file
BIN
mealie/tests/data/migrations/chowdown/test_chowdown-gh-pages.zip
Normal file
Binary file not shown.
Binary file not shown.
BIN
mealie/tests/data/test_image.jpg
Normal file
BIN
mealie/tests/data/test_image.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
|
@ -3,25 +3,57 @@ import shutil
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from app_config import MIGRATION_DIR
|
from app_config import MIGRATION_DIR
|
||||||
from tests.test_config import TEST_NEXTCLOUD_DIR
|
from tests.test_config import TEST_CHOWDOWN_DIR, TEST_NEXTCLOUD_DIR
|
||||||
|
|
||||||
#! Broken
|
|
||||||
# def test_import_chowdown_recipes(api_client):
|
|
||||||
# response = api_client.post(
|
|
||||||
# "/api/migration/chowdown/repo/",
|
|
||||||
# json={"url": "https://github.com/hay-kot/chowdown"},
|
|
||||||
# )
|
|
||||||
|
|
||||||
# assert response.status_code == 200
|
|
||||||
|
|
||||||
# test_slug = "banana-bread"
|
|
||||||
# response = api_client.get(f"/api/recipe/{test_slug}/")
|
|
||||||
# assert response.status_code == 200
|
|
||||||
|
|
||||||
# recipe = json.loads(response.content)
|
|
||||||
# assert recipe["slug"] == test_slug
|
|
||||||
|
|
||||||
|
|
||||||
|
### Chowdown
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def chowdown_zip():
|
||||||
|
zip = TEST_CHOWDOWN_DIR.joinpath("test_chowdown-gh-pages.zip")
|
||||||
|
|
||||||
|
zip_copy = TEST_CHOWDOWN_DIR.joinpath("chowdown-gh-pages.zip")
|
||||||
|
|
||||||
|
shutil.copy(zip, zip_copy)
|
||||||
|
|
||||||
|
yield zip_copy
|
||||||
|
|
||||||
|
zip_copy.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_chowdown_zip(api_client, chowdown_zip):
|
||||||
|
|
||||||
|
response = api_client.post(
|
||||||
|
"/api/migrations/chowdown/upload/", files={"archive": chowdown_zip.open("rb")}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
assert MIGRATION_DIR.joinpath("chowdown", chowdown_zip.name).is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_chowdown_directory(api_client, chowdown_zip):
|
||||||
|
selection = chowdown_zip.name
|
||||||
|
response = api_client.post(f"/api/migrations/chowdown/{selection}/import/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
report = json.loads(response.content)
|
||||||
|
assert report["failed"] == []
|
||||||
|
|
||||||
|
expected_slug = "roasted-okra"
|
||||||
|
response = api_client.get(f"/api/recipe/{expected_slug}/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_chowdown_migration_data(api_client, chowdown_zip):
|
||||||
|
selection = chowdown_zip.name
|
||||||
|
response = api_client.delete(f"/api/migrations/chowdown/{selection}/delete/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert not MIGRATION_DIR.joinpath(chowdown_zip.name).is_file()
|
||||||
|
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def nextcloud_zip():
|
def nextcloud_zip():
|
||||||
zip = TEST_NEXTCLOUD_DIR.joinpath("nextcloud.zip")
|
zip = TEST_NEXTCLOUD_DIR.joinpath("nextcloud.zip")
|
||||||
|
@ -30,7 +62,9 @@ def nextcloud_zip():
|
||||||
|
|
||||||
shutil.copy(zip, zip_copy)
|
shutil.copy(zip, zip_copy)
|
||||||
|
|
||||||
return zip_copy
|
yield zip_copy
|
||||||
|
|
||||||
|
zip_copy.unlink()
|
||||||
|
|
||||||
|
|
||||||
def test_upload_nextcloud_zip(api_client, nextcloud_zip):
|
def test_upload_nextcloud_zip(api_client, nextcloud_zip):
|
||||||
|
@ -58,7 +92,7 @@ def test_import_nextcloud_directory(api_client, nextcloud_zip):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_delete_migration_data(api_client, nextcloud_zip):
|
def test_delete__nextcloud_migration_data(api_client, nextcloud_zip):
|
||||||
selection = nextcloud_zip.name
|
selection = nextcloud_zip.name
|
||||||
response = api_client.delete(f"/api/migrations/nextcloud/{selection}/delete/")
|
response = api_client.delete(f"/api/migrations/nextcloud/{selection}/delete/")
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,12 @@ import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from tests.test_routes.utils.routes_data import (RecipeTestData,
|
from tests.test_routes.utils.routes_data import (
|
||||||
raw_recipe_dict,
|
RecipeTestData,
|
||||||
recipe_test_data)
|
raw_recipe,
|
||||||
|
raw_recipe_no_image,
|
||||||
|
recipe_test_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("recipe_data", recipe_test_data)
|
@pytest.mark.parametrize("recipe_data", recipe_test_data)
|
||||||
|
@ -15,12 +18,31 @@ def test_create_by_url(api_client, recipe_data: RecipeTestData):
|
||||||
|
|
||||||
|
|
||||||
def test_create_by_json(api_client):
|
def test_create_by_json(api_client):
|
||||||
response = api_client.post("/api/recipe/create/", json=raw_recipe_dict)
|
response = api_client.post("/api/recipe/create/", json=raw_recipe)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert json.loads(response.text) == "banana-bread"
|
assert json.loads(response.text) == "banana-bread"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_no_image(api_client):
|
||||||
|
response = api_client.post("/api/recipe/create/", json=raw_recipe_no_image)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert json.loads(response.text) == "banana-bread-no-image"
|
||||||
|
|
||||||
|
|
||||||
|
# def test_upload_image(api_client, test_image):
|
||||||
|
# data = {"image": test_image.open("rb").read(), "extension": "jpg"}
|
||||||
|
|
||||||
|
# response = api_client.post(
|
||||||
|
# "/api/recipe/banana-bread-no-image/update/image/", files=data
|
||||||
|
# )
|
||||||
|
|
||||||
|
# assert response.status_code == 200
|
||||||
|
|
||||||
|
# response = api_client.get("/api/recipe/banana-bread-no-image/update/image/")
|
||||||
|
|
||||||
|
|
||||||
def test_read_all_post(api_client):
|
def test_read_all_post(api_client):
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
"/api/all-recipes/", json={"properties": ["slug", "description", "rating"]}
|
"/api/all-recipes/", json={"properties": ["slug", "description", "rating"]}
|
||||||
|
|
|
@ -16,7 +16,7 @@ recipe_test_data = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
raw_recipe_dict = {
|
raw_recipe = {
|
||||||
"name": "Banana Bread",
|
"name": "Banana Bread",
|
||||||
"description": "From Angie's mom",
|
"description": "From Angie's mom",
|
||||||
"image": "banana-bread.jpg",
|
"image": "banana-bread.jpg",
|
||||||
|
@ -62,3 +62,50 @@ raw_recipe_dict = {
|
||||||
"orgURL": None,
|
"orgURL": None,
|
||||||
"extras": {},
|
"extras": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
raw_recipe_no_image = {
|
||||||
|
"name": "Banana Bread No Image",
|
||||||
|
"description": "From Angie's mom",
|
||||||
|
"image": "",
|
||||||
|
"recipeYield": "",
|
||||||
|
"recipeIngredient": [
|
||||||
|
"4 bananas",
|
||||||
|
"1/2 cup butter",
|
||||||
|
"1/2 cup sugar",
|
||||||
|
"2 eggs",
|
||||||
|
"2 cups flour",
|
||||||
|
"1/2 tsp baking soda",
|
||||||
|
"1 tsp baking powder",
|
||||||
|
"pinch salt",
|
||||||
|
"1/4 cup nuts (we like pecans)",
|
||||||
|
],
|
||||||
|
"recipeInstructions": [
|
||||||
|
{
|
||||||
|
"@type": "Beat the eggs, then cream with the butter and sugar",
|
||||||
|
"text": "Beat the eggs, then cream with the butter and sugar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Mix in bananas, then flour, baking soda/powder, salt, and nuts",
|
||||||
|
"text": "Mix in bananas, then flour, baking soda/powder, salt, and nuts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Add to greased and floured pan",
|
||||||
|
"text": "Add to greased and floured pan",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Bake until brown/cracked, toothpick comes out clean",
|
||||||
|
"text": "Bake until brown/cracked, toothpick comes out clean",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"totalTime": "None",
|
||||||
|
"prepTime": None,
|
||||||
|
"performTime": None,
|
||||||
|
"slug": "",
|
||||||
|
"categories": [],
|
||||||
|
"tags": ["breakfast", " baking"],
|
||||||
|
"dateAdded": "2021-01-12",
|
||||||
|
"notes": [],
|
||||||
|
"rating": 0,
|
||||||
|
"orgURL": None,
|
||||||
|
"extras": {},
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
test_
|
|
@ -1,11 +0,0 @@
|
||||||
from services.scheduler_services import Scheduler
|
|
||||||
|
|
||||||
|
|
||||||
def start_scheduler():
|
|
||||||
global scheduler
|
|
||||||
scheduler = Scheduler()
|
|
||||||
scheduler.startup_scheduler()
|
|
||||||
return scheduler
|
|
||||||
|
|
||||||
|
|
||||||
scheduler = start_scheduler
|
|
21
mealie/utils/post_webhooks.py
Normal file
21
mealie/utils/post_webhooks.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from db.db_setup import create_session
|
||||||
|
from services.meal_services import MealPlan
|
||||||
|
from services.recipe_services import Recipe
|
||||||
|
from services.settings_services import SiteSettings
|
||||||
|
|
||||||
|
|
||||||
|
def post_webhooks():
|
||||||
|
session = create_session()
|
||||||
|
all_settings = SiteSettings.get_site_settings(session)
|
||||||
|
|
||||||
|
if all_settings.webhooks.enabled:
|
||||||
|
todays_meal = Recipe.get_by_slug(MealPlan.today()).dict()
|
||||||
|
urls = all_settings.webhooks.webhookURLs
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
requests.post(url, json.dumps(todays_meal, default=str))
|
||||||
|
|
||||||
|
session.close()
|
|
@ -1,17 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from app_config import REQUIRED_DIRS
|
|
||||||
from services.settings_services import default_theme_init
|
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
|
||||||
|
|
||||||
|
|
||||||
def post_start():
|
|
||||||
default_theme_init()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
pass
|
|
|
@ -6,6 +6,7 @@ from app_config import TEMP_DIR
|
||||||
|
|
||||||
|
|
||||||
def unpack_zip(selection: Path) -> tempfile.TemporaryDirectory:
|
def unpack_zip(selection: Path) -> tempfile.TemporaryDirectory:
|
||||||
|
TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
temp_dir = tempfile.TemporaryDirectory(dir=TEMP_DIR)
|
temp_dir = tempfile.TemporaryDirectory(dir=TEMP_DIR)
|
||||||
temp_dir_path = Path(temp_dir.name)
|
temp_dir_path = Path(temp_dir.name)
|
||||||
if selection.suffix == ".zip":
|
if selection.suffix == ".zip":
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue