diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 000000000000..3a7a0116e975 --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,20 @@ +name: Publish docs via GitHub Pages +on: + push: + branches: + - main + +jobs: + build: + name: Deploy docs + runs-on: ubuntu-latest + steps: + - name: Checkout main + uses: actions/checkout@v1 + + - name: Deploy docs + uses: mhausenblas/mkdocs-deploy-gh-pages@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CONFIG_FILE: docs/mkdocs.yml + EXTRA_PACKAGES: build-base \ No newline at end of file diff --git a/.gitignore b/.gitignore index b2aee14eace5..7230489b4556 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ __pycache__/ *$py.class # frontend/.env.development docs/site/ +mealie/temp/* +mealie/temp/api.html + mealie/data/backups/* mealie/data/debug/* diff --git a/README.md b/README.md index 42eb7bf13087..af08ad7a8c2e 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,14 @@ A Place for All Your Recipes
Explore the docs » -
+ +
View Demo · - Report Bug + Report Bug + · + API · Request Feature @@ -32,7 +35,6 @@ · Docker Hub -

diff --git a/docs/docs/api/api-examples.md b/docs/docs/api/api-examples.md new file mode 100644 index 000000000000..3891caadaed9 --- /dev/null +++ b/docs/docs/api/api-examples.md @@ -0,0 +1,5 @@ +# API Examples + +TODO + +Have Ideas? Submit a PR! \ No newline at end of file diff --git a/docs/docs/api/api-intro.md b/docs/docs/api/api-intro.md deleted file mode 100644 index d32b02e08575..000000000000 --- a/docs/docs/api/api-intro.md +++ /dev/null @@ -1,3 +0,0 @@ -# API Introduction - -TODO \ No newline at end of file diff --git a/docs/docs/api/docs/index.html b/docs/docs/api/docs/index.html new file mode 100644 index 000000000000..0f19965fb7aa --- /dev/null +++ b/docs/docs/api/docs/index.html @@ -0,0 +1,26 @@ + + + + + My Project - ReDoc + + + + + + + +
+ + + + + diff --git a/docs/docs/getting-started/install.md b/docs/docs/getting-started/install.md index 7742622c1d9e..b5196d2b3f61 100644 --- a/docs/docs/getting-started/install.md +++ b/docs/docs/getting-started/install.md @@ -16,6 +16,7 @@ To deploy docker on your local network it is highly recommended to use docker to | db_password | example | The Mongodb password you specified in your mongo container | | db_host | mongo | The host address of MongoDB if you're in docker and using the same network you can use mongo as the host name | | db_port | 27017 | the port to access MongoDB 27017 is the default for mongo | +| api_docs | True | Turns on/off access to the API documentation locally. | | TZ | | You should set your time zone accordingly so the date/time features work correctly | diff --git a/docs/docs/html/api.html b/docs/docs/html/api.html new file mode 100644 index 000000000000..7a1046cc7a85 --- /dev/null +++ b/docs/docs/html/api.html @@ -0,0 +1,26 @@ + + + + + My Project - ReDoc + + + + + + + +
+ + + + + diff --git a/docs/docs/index.md b/docs/docs/index.md index 515a03f99e17..c26fac3d449a 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -8,7 +8,9 @@
View Demo · - Report Bug + Report Bug + · + API · Request Feature diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 41646761ebd8..9074892620a6 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -7,7 +7,6 @@ theme: logo: material/silverware-variant features: - navigation.expand - - navigation.instant markdown_extensions: - pymdownx.emoji: @@ -35,7 +34,8 @@ nav: - Backups and Exports: "getting-started/backups-and-exports.md" - Recipe Migration: "getting-started/migration-imports.md" - API Reference: - - Swagger/OpenAPI: "api/api-intro.md" + - API Documentation: "api/docs/index.html" + - Usage Examples: "api/api-examples.md" - Contributors Guide: - Non-Code: "contributors/non-coders.md" - Developers Guide: diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 000000000000..b0b5a72c639d --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.enableFiletypes": [ + "!javascript" + ] +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1e3d3094c05b..7f00924b37aa 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1738,6 +1738,16 @@ "integrity": "sha1-/q7SVZc9LndVW4PbwIhRpsY1IPo=", "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": { "version": "13.0.1", "resolved": "https://registry.npm.taobao.org/cacache/download/cacache-13.0.1.tgz?cache=0&sync_timestamp=1594428402513&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcacache%2Fdownload%2Fcacache-13.0.1.tgz", @@ -1764,6 +1774,34 @@ "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 + }, "find-cache-dir": { "version": "3.3.1", "resolved": "https://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-3.3.1.tgz?cache=0&sync_timestamp=1583735626956&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffind-cache-dir%2Fdownload%2Ffind-cache-dir-3.3.1.tgz", @@ -1785,6 +1823,25 @@ "path-exists": "^4.0.0" } }, + "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" + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npm.taobao.org/locate-path/download/locate-path-5.0.0.tgz?cache=0&sync_timestamp=1597081764621&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flocate-path%2Fdownload%2Flocate-path-5.0.0.tgz", @@ -1849,6 +1906,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.npm.taobao.org/terser-webpack-plugin/download/terser-webpack-plugin-2.3.8.tgz?cache=0&sync_timestamp=1603882075288&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fterser-webpack-plugin%2Fdownload%2Fterser-webpack-plugin-2.3.8.tgz", @@ -1865,6 +1932,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" + } } } }, @@ -9082,7 +9161,7 @@ }, "rechoir": { "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "resolved": "https://registry.npm.taobao.org/rechoir/download/rechoir-0.6.2.tgz", "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", "dev": true, "requires": { @@ -9736,6 +9815,11 @@ "rechoir": "^0.6.2" } }, + "shvl": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/shvl/-/shvl-2.0.1.tgz", + "integrity": "sha512-VU7R5Uxp38LKHooGuZe0TcX2EPK95nn8DvclAvTPyD9/qHmXvt3dR2pJ4JLZ8uLjxQNQ3zNLFJCreteIj3cvpw==" + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.3.tgz?cache=0&sync_timestamp=1585253323149&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsignal-exit%2Fdownload%2Fsignal-exit-3.0.3.tgz", @@ -11065,11 +11149,6 @@ } } }, - "vue-cookies": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/vue-cookies/-/vue-cookies-1.7.4.tgz", - "integrity": "sha512-mOS5Btr8V9zvAtkmQ7/TfqJIropOx7etDAgBywPCmHjvfJl2gFbH2XgoMghleLoyyMTi5eaJss0mPN7arMoslA==" - }, "vue-eslint-parser": { "version": "7.1.1", "resolved": "https://registry.npm.taobao.org/vue-eslint-parser/download/vue-eslint-parser-7.1.1.tgz", @@ -11102,11 +11181,6 @@ "integrity": "sha1-UylVzB6yCKPZkLOp+acFdGV+CPI=", "dev": true }, - "vue-html-to-paper": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vue-html-to-paper/-/vue-html-to-paper-1.3.1.tgz", - "integrity": "sha512-5IdAPUgStfpVHfcG6nXD0FbUB1onWpvwVD+OZ00jJpy3qaRPkaGD7fFIvYgBB9YPkr0VK065LayEvmGmkkfhaQ==" - }, "vue-loader": { "version": "15.9.5", "resolved": "https://registry.npm.taobao.org/vue-loader/download/vue-loader-15.9.5.tgz?cache=0&sync_timestamp=1605670886675&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-loader%2Fdownload%2Fvue-loader-15.9.5.tgz", @@ -11128,87 +11202,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": { "version": "3.4.9", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz", @@ -11268,6 +11261,22 @@ "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.0.tgz", "integrity": "sha512-W74OO2vCJPs9/YjNjW8lLbj+jzT24waTo2KShI8jLvJW8OaIkgb3wuAMA7D+ZiUxDOx3ubwSZTaJBip9G8a3aQ==" }, + "vuex-persistedstate": { + "version": "4.0.0-beta.2", + "resolved": "https://registry.npmjs.org/vuex-persistedstate/-/vuex-persistedstate-4.0.0-beta.2.tgz", + "integrity": "sha512-JeiweafcU+9d4+/nRvQwK2PyHS9xCRcGIlL2cn0ny/afTw2RP+5M6SdsjkcYoGNICTGPi5i+K3J46ioWEyVgvg==", + "requires": { + "deepmerge": "^4.2.2", + "shvl": "^2.0.0" + }, + "dependencies": { + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + } + } + }, "watchpack": { "version": "1.7.5", "resolved": "https://registry.npm.taobao.org/watchpack/download/watchpack-1.7.5.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwatchpack%2Fdownload%2Fwatchpack-1.7.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index ada4b56dc84a..f10d281c730d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,6 @@ "qs": "^6.9.4", "v-jsoneditor": "^1.4.2", "vue": "^2.6.11", - "vue-html-to-paper": "^1.3.1", "vue-router": "^3.4.9", "vuetify": "^2.4.1", "vuex": "^3.6.0", diff --git a/frontend/src/components/Admin/Theme.vue b/frontend/src/components/Admin/Theme.vue index 3699c5ed4c8e..fbdec561c1e2 100644 --- a/frontend/src/components/Admin/Theme.vue +++ b/frontend/src/components/Admin/Theme.vue @@ -16,17 +16,11 @@ mandatory @change="setStoresDarkMode" > - - Default to system - + Default to system - - Light - + Light - - Dark - + Dark @@ -140,19 +134,20 @@ export default { components: { ColorPicker, Confirmation, - NewTheme + NewTheme, }, data() { return { selectedTheme: {}, selectedDarkMode: "system", - availableThemes: [] + availableThemes: [], }; }, async mounted() { this.availableThemes = await api.themes.requestAll(); this.selectedTheme = this.$store.getters.getActiveTheme; this.selectedDarkMode = this.$store.getters.getDarkMode; + console.log(this.selectedDarkMode); }, methods: { @@ -181,7 +176,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"); @@ -203,6 +198,7 @@ export default { }, setStoresDarkMode() { + console.log(this.selectedDarkMode); this.$store.commit("setDarkMode", this.selectedDarkMode); }, /** @@ -216,8 +212,8 @@ export default { this.selectedTheme.colors ); } - } - } + }, + }, }; diff --git a/frontend/src/components/RecipeEditor/PrintRecipe.vue b/frontend/src/components/RecipeEditor/PrintRecipe.vue new file mode 100644 index 000000000000..6d742aa59bca --- /dev/null +++ b/frontend/src/components/RecipeEditor/PrintRecipe.vue @@ -0,0 +1,198 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/store/modules/userSettings.js b/frontend/src/store/modules/userSettings.js index 005e403f369c..51b922616c12 100644 --- a/frontend/src/store/modules/userSettings.js +++ b/frontend/src/store/modules/userSettings.js @@ -1,11 +1,22 @@ - import api from "../../api"; import Vuetify from "../../plugins/vuetify"; +function inDarkMode(payload) { + let isDark; + + if (payload === "system") { + //Get System Preference from browser + const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + isDark = darkMediaQuery.matches; + } else if (payload === "dark") isDark = true; + else if (payload === "light") isDark = false; + + return isDark; +} + const state = { activeTheme: {}, - darkMode: 'system' - + darkMode: "system", }; const mutations = { @@ -15,17 +26,7 @@ const mutations = { state.activeTheme = payload; }, setDarkMode(state, payload) { - let isDark; - - if (payload === 'system') { - //Get System Preference from browser - const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - isDark = darkMediaQuery.matches; - } - else if (payload === 'dark') - isDark = true; - else if (payload === 'light') - isDark = false; + let isDark = inDarkMode(payload); if (isDark !== null) { Vuetify.framework.theme.dark = isDark; @@ -40,31 +41,30 @@ const actions = { if (defaultTheme.colors) { Vuetify.framework.theme.themes.dark = defaultTheme.colors; Vuetify.framework.theme.themes.light = defaultTheme.colors; - commit('setTheme', defaultTheme) + commit("setTheme", defaultTheme); } }, async initTheme({ dispatch, getters }) { //If theme is empty resetTheme if (Object.keys(getters.getActiveTheme).length === 0) { - await dispatch('resetTheme') - } - else { + await dispatch("resetTheme"); + } else { + Vuetify.framework.theme.dark = inDarkMode(getters.getDarkMode); Vuetify.framework.theme.themes.dark = getters.getActiveTheme.colors; Vuetify.framework.theme.themes.light = getters.getActiveTheme.colors; } }, - -} +}; const getters = { getActiveTheme: (state) => state.activeTheme, - getDarkMode: (state) => state.darkMode -} + getDarkMode: (state) => state.darkMode, +}; export default { state, mutations, actions, - getters -} \ No newline at end of file + getters, +}; diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 2be7e03a17fe..a5e634dd14f2 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -7,11 +7,13 @@ import userSettings from "./modules/userSettings"; Vue.use(Vuex); const store = new Vuex.Store({ - plugins: [createPersistedState({ - paths: ['userSettings'] - })], + plugins: [ + createPersistedState({ + paths: ["userSettings"], + }), + ], modules: { - userSettings + userSettings, }, state: { // Snackbar @@ -40,7 +42,6 @@ const store = new Vuex.Store({ }, actions: { - async requestRecentRecipes() { const keys = [ "name", diff --git a/mealie/app.py b/mealie/app.py index 0893c0eac2f1..cbbdc2d03529 100644 --- a/mealie/app.py +++ b/mealie/app.py @@ -1,5 +1,4 @@ from pathlib import Path -import os import uvicorn from fastapi import FastAPI @@ -15,21 +14,27 @@ from routes import ( static_routes, user_routes, ) -from routes.setting_routes import scheduler -from settings import PORT +from routes.setting_routes import scheduler # ! This has to be imported for scheduling +from settings import PORT, PRODUCTION, docs_url, redoc_url from utils.logger import logger CWD = Path(__file__).parent WEB_PATH = CWD.joinpath("dist") -app = FastAPI() +app = FastAPI( + title="Mealie", + description="A place for all your recipes", + version="0.0.1", + docs_url=docs_url, + redoc_url=redoc_url, +) # Mount Vue Frontend only in production -env = os.environ.get("ENV") -if(env == "prod"): +if PRODUCTION: app.mount("/static", StaticFiles(directory=WEB_PATH, html=True)) + # API Routes app.include_router(recipe_routes.router) app.include_router(meal_routes.router) @@ -49,6 +54,9 @@ app.include_router(static_routes.router) startup.ensure_dirs() startup.generate_default_theme() +# Generate API Documentation +if not PRODUCTION: + startup.generate_api_docs(app) if __name__ == "__main__": logger.info("-----SYSTEM STARTUP-----") diff --git a/mealie/data/debug/last_recipe.json b/mealie/data/debug/last_recipe.json index 42fe45445e77..7f93d3997ecb 100644 --- a/mealie/data/debug/last_recipe.json +++ b/mealie/data/debug/last_recipe.json @@ -1,33 +1,29 @@ { "@context": "http://schema.org", "@type": "Recipe", - "articleBody": "Leftover rice is ideal for this dish (and a great way to use up any takeout that\u2019s hanging around), since fully chilled rice tends to be drier and will become crispier and browner in the skillet. To get the best texture, evenly distribute the rice in your pan and gently press down to flatten it out like a pancake. Don\u2019t touch until you hear it crackle! Finish with a sunny-side-up egg\u2014or poach it if you don't mind the stovetop fuss. This recipe is part of the 2021\u00a0Feel Good Food Plan, our eight-day dinner plan for starting the year off right.", - "alternativeHeadline": "To get the best texture, evenly distribute the rice in your pan and gently press down to flatten it. Don\u2019t touch until you hear it crackle!", - "dateModified": "2021-01-03 03:40:32.190000", - "datePublished": "2021-01-01 06:00:00", + "articleBody": "\u201cAfter a draining day juggling work, homeschooling, and urging children to stop using their masks as slingshots, the ideal food for me isn\u2019t perfectly prepared food that\u2019s been tweezered into position, but a meal that\u2019s simply comforting,\u201d writes the Smitten Kitchen\u2019s Deb Perelman. Right now, it\u2019s this deeply cozy pot of tender chicken thighs, jammy leeks, and broth-soaked rice.", + "alternativeHeadline": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.", + "dateModified": "2021-01-06 17:07:07.791000", + "datePublished": "2020-08-18 04:00:00", "keywords": [ "recipes", - "healthyish", - "salad", - "ginger", - "garlic", - "orange", - "oil", - "soy sauce", - "lemon juice", - "sesame oil", + "chicken recipes", "kosher salt", - "broccoli", - "brown rice", - "egg", - "celery", - "cilantro", - "mint", - "feel good food plan 2021", - "feel good food plan", + "black pepper", + "butter", + "leek", + "lemon zest", + "rice", + "chicken broth", + "anchovy", + "garlic", + "capers", + "herb", + "olive oil", + "healthyish", "web" ], - "thumbnailUrl": "https://assets.bonappetit.com/photos/5fdbe70a84d333dd1dcc7900/1:1/w_1698,h_1698,c_limit/BA1220feelgoodalt.jpg", + "thumbnailUrl": "https://assets.bonappetit.com/photos/5f29796456f43685a49327fb/1:1/w_1125,h_1125,c_limit/Chicken-and-Rice-With-Leeks-Salsa-Verde-01.jpg", "publisher": { "@context": "https://schema.org", "@type": "Organization", @@ -51,75 +47,52 @@ "author": [ { "@type": "Person", - "name": "Devonn Francis", - "sameAs": "https://bon-appetit.com/contributor/devonn-francis/" + "name": "Deb Perelman", + "sameAs": "https://bon-appetit.com/contributor/deb-perelman/" } ], "aggregateRating": { "@type": "AggregateRating", "ratingValue": 4, - "ratingCount": 2 + "ratingCount": 47 }, - "description": "To get the best texture, evenly distribute the rice in your pan and gently press down to flatten it. Don\u2019t touch until you hear it crackle! ", - "image": "crispy-rice-with-ginger-citrus-celery-salad.jpg", - "name": "Crispy Rice With Ginger-Citrus Celery Salad", + "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", + "headline": "Chicken and Rice With Leeks and Salsa Verde", + "name": "Chicken and Rice With Leeks and Salsa Verde", "recipeIngredient": [ - "1 2\" piece ginger, peeled, finely grated", - "1 small garlic clove, finely grated", - "Juice of 1 orange", - "2 tbsp. vegetable oil", - "1Tbsp. coconut aminos or low-sodium soy sauce", - "1 Tbsp. fresh lemon juice", - "\u00bc tsp. toasted sesame oil", - "Kosher salt", - "1 medium head of broccoli", - "6 Tbsp. (or more) vegetable oil, divided", - "Kosher salt", - "2 cups chilled cooked brown rice", - "4 large eggs", - "3 celery stalks, thinly sliced on a steep diagonal", - "\u00bd cup cilantro leaves with tender stems", - "\u00bd cup mint leaves", - "Crushed red pepper flakes (for serving)" + "1\u00bd lb. skinless, boneless chicken thighs (4\u20138 depending on size)", + "Kosher salt, freshly ground pepper", + "3 Tbsp. unsalted butter, divided", + "2 large or 3 medium leeks, white and pale green parts only, halved lengthwise, thinly sliced", + "Zest and juice of 1 lemon, divided", + "1\u00bd cups long-grain white rice, rinsed until water runs clear", + "2\u00be cups low-sodium chicken broth", + "1 oil-packed anchovy fillet", + "2 garlic cloves", + "1 Tbsp. drained capers", + "Crushed red pepper flakes", + "1 cup tender herb leaves (such as parsley, cilantro, and/or mint)", + "4\u20135 Tbsp. extra-virgin olive oil" ], "recipeInstructions": [ { "@type": "HowToStep", - "text": "Whisk ginger, garlic, orange juice, vegetable oil, coconut aminos, lemon juice, and sesame oil in a small bowl; season with salt and set aside." + "text": "Season chicken with salt and pepper. Melt 2 Tbsp. butter in a large high-sided skillet over medium-high heat. Add leeks and half of lemon zest, season with salt and pepper, and mix to coat leeks in butter. Reduce heat to medium-low, cover, and cook, stirring occasionally, until leeks are somewhat tender, about 5 minutes. Remove lid, increase heat to medium-high, and cook, stirring occasionally, until tender and just starting to take on color, about 3 minutes. Add rice and cook, stirring often, 3 minutes, then add broth, scraping up any browned bits. Tuck short sides of each chicken thigh underneath so they are touching and nestle seam side down into rice mixture. Bring to a simmer. Cover, reduce heat to medium-low, and cook until rice is tender and chicken is cooked through, about 20 minutes. Remove from heat. Cut remaining 1 Tbsp. butter into small pieces and scatter over mixture. Re-cover and let sit 10 minutes." }, { "@type": "HowToStep", - "text": "Trim about \u00bd\" from woody end of broccoli stem. Peel tough outer layer from stem. Cut florets from stems and thinly slice stems about \u00bd\" thick. Break florets apart with your hands into 1\"\u20131\u00bd\" pieces." + "text": "Meanwhile, pulse anchovy, garlic, capers, a few pinches of red pepper flakes, and remaining lemon zest in a food processor until finely chopped. Add herbs; process until a paste forms. With motor running, gradually stream in oil until loosened to a thick sauce. Add half of lemon juice; season salsa verde with salt." }, { "@type": "HowToStep", - "text": "Heat 2 Tbsp. oil in a large nonstick skillet over medium. Working in 2 batches if needed, arrange broccoli in a single layer and cook, tossing occasionally, until broccoli is bright green and lightly charred around the edges, about\u00a03 minutes. Transfer to a large plate." - }, - { - "@type": "HowToStep", - "text": "Pour 2 Tbsp. oil into same pan and heat over medium-high. Once you see the first wisp of smoke, add rice and season lightly with salt. Using a spatula or spoon, press rice evenly into pan like a pancake. Rice will begin to crackle, but don\u2019t fuss with it. When the crackling has died down almost completely, about\u00a03 minutes, break rice into large pieces and turn over." - }, - { - "@type": "HowToStep", - "text": "Add broccoli back to pan and give everything a toss to combine. Cook, tossing occasionally and adding another\u00a01 Tbsp. oil if pan looks dry, until broccoli is tender and rice is warmed through and very crisp, about 5 minutes. Transfer mixture to a platter or divide among plates and set aside." - }, - { - "@type": "HowToStep", - "text": "Wipe out skillet; heat remaining\u00a02 Tbsp. oil over medium-high. Crack eggs into skillet; season with salt. Oil should bubble around eggs right away. Cook, rotating skillet occasionally, until whites are golden brown and crisp at the edges and set around the yolk (which should be runny), about 2 minutes." - }, - { - "@type": "HowToStep", - "text": "Toss celery, cilantro, and mint with\u00a03 Tbsp. reserved dressing and a pinch of salt in a medium bowl to combine." - }, - { - "@type": "HowToStep", - "text": "Scatter celery salad over fried rice; top with fried eggs and sprinkle with red pepper flakes. Serve extra dressing alongside." + "text": "Drizzle remaining lemon juice over chicken and rice. Serve with salsa verde." } ], - "recipeYield": "4 servings", - "url": "https://www.bonappetit.com/recipe/crispy-rice-with-ginger-citrus-celery-salad", - "slug": "crispy-rice-with-ginger-citrus-celery-salad", - "orgURL": "https://www.bonappetit.com/recipe/crispy-rice-with-ginger-citrus-celery-salad", + "recipeYield": "4 Servings", + "url": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde", + "slug": "chicken-and-rice-with-leeks-and-salsa-verde", + "orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde", "categories": [], "tags": [], "dateAdded": null, diff --git a/mealie/data/img/bon-appetit-s-perfect-pizza.jpg b/mealie/data/img/bon-appetit-s-perfect-pizza.jpg deleted file mode 100644 index fa3093eb56f8..000000000000 Binary files a/mealie/data/img/bon-appetit-s-perfect-pizza.jpg and /dev/null differ diff --git a/mealie/data/img/mississippi-pot-roast.jpg b/mealie/data/img/mississippi-pot-roast.jpg deleted file mode 100644 index 41496b092c96..000000000000 Binary files a/mealie/data/img/mississippi-pot-roast.jpg and /dev/null differ diff --git a/mealie/models/backup_models.py b/mealie/models/backup_models.py index bfc590155023..2bd34b87e447 100644 --- a/mealie/models/backup_models.py +++ b/mealie/models/backup_models.py @@ -1,5 +1,4 @@ -# from datetime import datetime -from typing import Optional +from typing import List, Optional from pydantic import BaseModel @@ -7,3 +6,24 @@ from pydantic import BaseModel class BackupJob(BaseModel): tag: Optional[str] template: Optional[str] + + class Config: + schema_extra = { + "example": { + "tag": "July 23rd 2021", + "template": "recipes.md", + } + } + + +class Imports(BaseModel): + imports: List[str] + templates: List[str] + + class Config: + schema_extra = { + "example": { + "imports": ["sample_data.zip", "sampe_data2.zip"], + "templates": ["recipes.md", "custom_template.md"], + } + } diff --git a/mealie/models/migration_models.py b/mealie/models/migration_models.py new file mode 100644 index 000000000000..52c8fa036f80 --- /dev/null +++ b/mealie/models/migration_models.py @@ -0,0 +1,12 @@ +from pydantic.main import BaseModel + + +class ChowdownURL(BaseModel): + url: str + + class Config: + schema_extra = { + "example": { + "url": "https://chowdownrepo.com/repo", + } + } diff --git a/mealie/models/recipe_models.py b/mealie/models/recipe_models.py new file mode 100644 index 000000000000..572402bfdb4a --- /dev/null +++ b/mealie/models/recipe_models.py @@ -0,0 +1,59 @@ +from typing import List, Optional + +import pydantic +from pydantic.main import BaseModel + + +class RecipeResponse(BaseModel): + List + + class Config: + schema_extra = { + "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", + }, + ] + } + + +class AllRecipeRequest(BaseModel): + properties: List[str] + limit: Optional[int] + + class Config: + schema_extra = { + "example": { + "properties": ["name", "slug", "image"], + "limit": 100, + } + } + + +class RecipeURLIn(BaseModel): + url: str + + class Config: + schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}} + + +class SlugResponse(BaseModel): + class Config: + schema_extra = {"example": "adult-mac-and-cheese"} diff --git a/mealie/routes/backup_routes.py b/mealie/routes/backup_routes.py index f27526846455..8d93014b069b 100644 --- a/mealie/routes/backup_routes.py +++ b/mealie/routes/backup_routes.py @@ -1,14 +1,20 @@ from fastapi import APIRouter, HTTPException -from models.backup_models import BackupJob -from services.backup_services import (BACKUP_DIR, TEMPLATE_DIR, export_db, - import_from_archive) +from models.backup_models import BackupJob, Imports +from pydantic.main import BaseModel +from services.backup_services import ( + BACKUP_DIR, + TEMPLATE_DIR, + export_db, + import_from_archive, +) from utils.snackbar import SnackResponse router = APIRouter() -@router.get("/api/backups/available/", tags=["Import / Export"]) +@router.get("/api/backups/available/", tags=["Import / Export"], response_model=Imports) async def available_imports(): + """Returns a list of avaiable .zip files for import into Mealie.""" imports = [] templates = [] for archive in BACKUP_DIR.glob("*.zip"): @@ -17,12 +23,12 @@ async def available_imports(): for template in TEMPLATE_DIR.glob("*.md"): templates.append(template.name) - return {"imports": imports, "templates": templates} + return Imports(imports=imports, templates=templates) @router.post("/api/backups/export/database/", tags=["Import / Export"], status_code=201) async def export_database(data: BackupJob): - + """Generates a backup of the recipe database in json format.""" try: export_path = export_db(data.tag, data.template) except: @@ -38,6 +44,7 @@ async def export_database(data: BackupJob): "/api/backups/{file_name}/import/", tags=["Import / Export"], status_code=200 ) async def import_database(file_name: str): + """ Import a database backup file generated from Mealie. """ imported = import_from_archive(file_name) return imported @@ -48,6 +55,7 @@ async def import_database(file_name: str): status_code=200, ) async def delete_backup(backup_name: str): + """ Removes a database backup from the file system """ try: BACKUP_DIR.joinpath(backup_name).unlink() diff --git a/mealie/routes/meal_routes.py b/mealie/routes/meal_routes.py index f9ace57d1ec2..4c9057ad9bdd 100644 --- a/mealie/routes/meal_routes.py +++ b/mealie/routes/meal_routes.py @@ -1,28 +1,26 @@ -from pprint import pprint +from typing import List from fastapi import APIRouter, HTTPException +from models.recipe_models import SlugResponse from services.meal_services import MealPlan from utils.snackbar import SnackResponse router = APIRouter() -@router.get("/api/meal-plan/all/", tags=["Meal Plan"]) +@router.get("/api/meal-plan/all/", tags=["Meal Plan"], response_model=List[MealPlan]) async def get_all_meals(): - """ Returns a list of all available meal plans """ + """ Returns a list of all available Meal Plan """ return MealPlan.get_all() @router.post("/api/meal-plan/create/", tags=["Meal Plan"]) async def set_meal_plan(data: MealPlan): - """ Creates Mealplan from Frontend Data""" + """ Creates a meal plan database entry """ data.process_meals() data.save_to_db() - - # try: - # except: # raise HTTPException( # status_code=404, # detail=SnackResponse.error("Unable to Create Mealplan See Log"), @@ -33,7 +31,7 @@ async def set_meal_plan(data: MealPlan): @router.post("/api/meal-plan/{plan_id}/update/", tags=["Meal Plan"]) async def update_meal_plan(plan_id: str, meal_plan: MealPlan): - """ Updates a Meal Plan Based off ID """ + """ Updates a meal plan based off ID """ try: meal_plan.process_meals() @@ -49,21 +47,27 @@ async def update_meal_plan(plan_id: str, meal_plan: MealPlan): @router.delete("/api/meal-plan/{plan_id}/delete/", tags=["Meal Plan"]) async def delete_meal_plan(plan_id): - """ Doc Str """ + """ Removes a meal plan from the database """ MealPlan.delete(plan_id) return SnackResponse.success("Mealplan Deleted") -@router.get("/api/meal-plan/today/", tags=["Meal Plan"]) +@router.get( + "/api/meal-plan/today/", + tags=["Meal Plan"], +) async def get_today(): - """ Returns the meal plan data for today """ + """ + Returns the recipe slug for the meal scheduled for today. + If no meal is scheduled nothing is returned + """ return MealPlan.today() -@router.get("/api/meal-plan/this-week/", tags=["Meal Plan"]) +@router.get("/api/meal-plan/this-week/", tags=["Meal Plan"], response_model=MealPlan) async def get_this_week(): """ Returns the meal plan data for this week """ diff --git a/mealie/routes/migration_routes.py b/mealie/routes/migration_routes.py index 9d30af09d843..df214303379b 100644 --- a/mealie/routes/migration_routes.py +++ b/mealie/routes/migration_routes.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, HTTPException from models.backup_models import BackupJob +from models.migration_models import ChowdownURL from services.migrations.chowdown import chowdown_migrate as chowdow_migrate from utils.snackbar import SnackResponse @@ -7,10 +8,10 @@ router = APIRouter() @router.post("/api/migration/chowdown/repo/", tags=["Migration"]) -async def import_chowdown_recipes(repo: dict): +async def import_chowdown_recipes(repo: ChowdownURL): """ Import Chowsdown Recipes from Repo URL """ try: - report = chowdow_migrate(repo.get("url")) + report = chowdow_migrate(repo.url) return SnackResponse.success( "Recipes Imported from Git Repo, see report for failures.", additional_data=report, diff --git a/mealie/routes/recipe_routes.py b/mealie/routes/recipe_routes.py index 35ccb406a1dd..7a9949961903 100644 --- a/mealie/routes/recipe_routes.py +++ b/mealie/routes/recipe_routes.py @@ -2,6 +2,7 @@ from typing import List, Optional from fastapi import APIRouter, File, Form, HTTPException, Query from fastapi.responses import FileResponse +from models.recipe_models import AllRecipeRequest, RecipeURLIn, SlugResponse from services.image_services import read_image, write_image from services.recipe_services import Recipe, read_requested_values from services.scrape_services import create_from_url @@ -10,17 +11,42 @@ from utils.snackbar import SnackResponse router = APIRouter() -@router.get("/api/all-recipes/", tags=["Recipes"]) +@router.get("/api/all-recipes/", tags=["Recipes"], response_model=List[dict]) async def get_all_recipes( keys: Optional[List[str]] = Query(...), num: Optional[int] = 100 -) -> Optional[List[str]]: - """ Returns key data for all recipes """ +): + """ + Returns key data for all recipes based off the query paramters provided. + For example, if slug, image, and name are provided you will recieve a list of + recipes containing the slug, image, and name property. By default, responses + are limited to 100. + + **Note:** You may experience problems with with query parameters. As an alternative + you may also use the post method and provide a body. + See the *Post* method for more details. + """ all_recipes = read_requested_values(keys, num) return all_recipes -@router.get("/api/recipe/{recipe_slug}/", tags=["Recipes"]) +@router.post("/api/all-recipes/", tags=["Recipes"], response_model=List[dict]) +async def get_all_recipes_post(body: AllRecipeRequest): + """ + Returns key data for all recipes based off the body data provided. + For example, if slug, image, and name are provided you will recieve a list of + recipes containing the slug, image, and name property. + + Refer to the body example for data formats. + + """ + + all_recipes = read_requested_values(body.properties, body.limit) + + return all_recipes + + +@router.get("/api/recipe/{recipe_slug}/", tags=["Recipes"], response_model=Recipe) async def get_recipe(recipe_slug: str): """ Takes in a recipe slug, returns all data for a recipe """ recipe = Recipe.get_by_slug(recipe_slug) @@ -37,24 +63,21 @@ async def get_recipe_img(recipe_slug: str): # Recipe Creations -@router.post("/api/recipe/create-url/", tags=["Recipes"], status_code=201) -async def get_recipe_url(url: dict): - """ Takes in a URL and Attempts to scrape data and load it into the database """ +@router.post( + "/api/recipe/create-url/", + tags=["Recipes"], + status_code=201, + response_model=str, +) +async def parse_recipe_url(url: RecipeURLIn): + """ Takes in a URL and attempts to scrape data and load it into the database """ - url = url.get("url") - slug = create_from_url(url) - - # try: - # slug = create_from_url(url) - # except: - # raise HTTPException( - # status_code=400, detail=SnackResponse.error("Unable to Parse URL") - # ) + slug = create_from_url(url.url) return slug -@router.post("/api/recipe/create/", tags=["Recipes"]) +@router.post("/api/recipe/create/", tags=["Recipes"], response_model=SlugResponse) async def create_from_json(data: Recipe) -> str: """ Takes in a JSON string and loads data into the database as a new entry""" created_recipe = data.save_to_db() @@ -63,7 +86,7 @@ async def create_from_json(data: Recipe) -> str: @router.post("/api/recipe/{recipe_slug}/update/image/", tags=["Recipes"]) -def update_image( +def update_recipe_image( recipe_slug: str, image: bytes = File(...), extension: str = Form(...) ): """ Removes an existing image and replaces it with the incoming file. """ @@ -73,7 +96,7 @@ def update_image( @router.post("/api/recipe/{recipe_slug}/update/", tags=["Recipes"]) -async def update(recipe_slug: str, data: Recipe): +async def update_recipe(recipe_slug: str, data: Recipe): """ Updates a recipe by existing slug and data. Data should containt """ data.update(recipe_slug) @@ -82,7 +105,7 @@ async def update(recipe_slug: str, data: Recipe): @router.delete("/api/recipe/{recipe_slug}/delete/", tags=["Recipes"]) -async def delete(recipe_slug: str): +async def delete_recipe(recipe_slug: str): """ Deletes a recipe by slug """ try: diff --git a/mealie/routes/setting_routes.py b/mealie/routes/setting_routes.py index 6a254c4e2b10..5d22531cf39f 100644 --- a/mealie/routes/setting_routes.py +++ b/mealie/routes/setting_routes.py @@ -1,3 +1,5 @@ +from typing import List + from db.mongo_setup import global_init from fastapi import APIRouter, HTTPException from services.scheduler_services import Scheduler, post_webhooks @@ -13,14 +15,14 @@ scheduler.startup_scheduler() @router.get("/api/site-settings/", tags=["Settings"]) async def get_main_settings(): - """ Returns basic site Settings """ + """ Returns basic site settings """ return SiteSettings.get_site_settings() @router.post("/api/site-settings/webhooks/test/", tags=["Settings"]) async def test_webhooks(): - """ Test Webhooks """ + """ Run the function to test your webhooks """ return post_webhooks() @@ -40,22 +42,26 @@ async def update_settings(data: SiteSettings): return SnackResponse.success("Settings Updated") -@router.get("/api/site-settings/themes/", tags=["Themes"]) +@router.get( + "/api/site-settings/themes/", tags=["Themes"] +) async def get_all_themes(): """ Returns all site themes """ return SiteTheme.get_all() -@router.get("/api/site-settings/themes/{theme_name}/", tags=["Themes"]) +@router.get( + "/api/site-settings/themes/{theme_name}/", tags=["Themes"] +) async def get_single_theme(theme_name: str): - """ Returns basic site Settings """ + """ Returns a named theme """ return SiteTheme.get_by_name(theme_name) @router.post("/api/site-settings/themes/create/", tags=["Themes"]) async def create_theme(data: SiteTheme): - """ Creates a Site Color Theme """ + """ Creates a site color theme database entry """ try: data.save_to_db() @@ -69,7 +75,7 @@ async def create_theme(data: SiteTheme): @router.post("/api/site-settings/themes/{theme_name}/update/", tags=["Themes"]) async def update_theme(theme_name: str, data: SiteTheme): - """ Returns basic site Settings """ + """ Update a theme database entry """ try: data.update_document() except: @@ -82,7 +88,7 @@ async def update_theme(theme_name: str, data: SiteTheme): @router.delete("/api/site-settings/themes/{theme_name}/delete/", tags=["Themes"]) async def delete_theme(theme_name: str): - """ Returns basic site Settings """ + """ Deletes theme from the database """ try: SiteTheme.delete_theme(theme_name) except: diff --git a/mealie/services/settings_services.py b/mealie/services/settings_services.py index b2b55109b28f..46e0992cba41 100644 --- a/mealie/services/settings_services.py +++ b/mealie/services/settings_services.py @@ -24,6 +24,18 @@ 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 _unpack_doc(document: SiteSettingsDocument): document = json.loads(document.to_json()) @@ -65,6 +77,22 @@ 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", + }, + } + } + @staticmethod def get_by_name(theme_name): document = SiteThemeDocument.objects.get(name=theme_name) diff --git a/mealie/settings.py b/mealie/settings.py index 5b3fb460d9c8..9814edf739f2 100644 --- a/mealie/settings.py +++ b/mealie/settings.py @@ -8,7 +8,16 @@ ENV = CWD.joinpath(".env") dotenv.load_dotenv(ENV) # 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 # Mongo Database MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie") diff --git a/mealie/startup.py b/mealie/startup.py index 70c2b3545ab3..70e388f3154a 100644 --- a/mealie/startup.py +++ b/mealie/startup.py @@ -1,3 +1,4 @@ +import json from pathlib import Path from services.settings_services import Colors, SiteTheme @@ -37,5 +38,43 @@ def generate_default_theme(): default_theme.save_to_db() +"""Script to export the ReDoc documentation page into a standalone HTML file.""" + +HTML_TEMPLATE = """ + + + + My Project - ReDoc + + + + + + + +
+ + + + +""" + +CWD = Path(__file__).parent +out_path = CWD.joinpath("temp", "index.html") + + +def generate_api_docs(app): + with open(out_path, "w") as fd: + print(HTML_TEMPLATE % json.dumps(app.openapi()), file=fd) + + if __name__ == "__main__": pass diff --git a/mealie/utils/document_utils.py b/mealie/utils/document_utils.py deleted file mode 100644 index 576825f9264d..000000000000 --- a/mealie/utils/document_utils.py +++ /dev/null @@ -1 +0,0 @@ -import datetime diff --git a/scratch.json b/scratch.json deleted file mode 100644 index a80b55f1aab5..000000000000 --- a/scratch.json +++ /dev/null @@ -1 +0,0 @@ -// Test Notify \ No newline at end of file