Revert "v0.2.1 (#157)" (#158)

This reverts commit a899f46464.
This commit is contained in:
Hayden 2021-02-10 19:39:46 -09:00 committed by GitHub
parent a899f46464
commit 8221c36a89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
82 changed files with 895 additions and 937 deletions

View file

@ -1,20 +1,5 @@
# Release Notes
## v0.3.0 - Draft!
### Features and Improvements
- Open search with `/` hotkey!
- Unified and improved snackbar notifications
- Recipe Viewer
- Categories, Tags, and Notes will not be displayed below the steps on smaller screens
- Recipe Editor
- Text areas now auto grow to fit content
- Description, Steps, and Notes support Markdown! This includes inline html in Markdown.
### Development / Misc
- Added async file response for images, downloading files.
- Breakup recipe view component
## v0.2.0 - Now with Test!
This is, what I think, is a big release! Tons of new features and some great quality of life improvements with some additional features. You may find that I made promises to include some fixes/features in v0.2.0. The short of is I greatly underestimated the work needed to refactor the database to a usable state and integrate categories in a way that is useful for users. This shouldn't be taken as a sign that I'm dropping those feature requests or ignoring them. I felt it was better to push a release in the current state rather than drag on development to try and fulfil all of the promises I made.

View file

@ -1385,11 +1385,6 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
"dev": true
},
"@smartweb/vue-flash-message": {
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/@smartweb/vue-flash-message/-/vue-flash-message-0.6.10.tgz",
"integrity": "sha512-ceDUUzXI6FDscev36kZQvc2BO+MayOt6uJ2HSh9zoOkfa0PVIhmaoB56InlTTsK7MmlSIvPJpRB+Habdx3MtNw=="
},
"@soda/friendly-errors-webpack-plugin": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
@ -2016,6 +2011,16 @@
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"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.npmjs.org/cacache/-/cacache-13.0.1.tgz",
@ -2042,6 +2047,53 @@
"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": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -2058,6 +2110,16 @@
"minipass": "^3.1.1"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"terser-webpack-plugin": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
@ -2074,6 +2136,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"
}
}
}
},
@ -11861,87 +11935,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",

View file

@ -10,7 +10,6 @@
},
"dependencies": {
"@adapttive/vue-markdown": "^3.0.3",
"@smartweb/vue-flash-message": "^0.6.10",
"axios": "^0.21.1",
"core-js": "^3.8.2",
"fuse.js": "^6.4.6",

View file

@ -1,22 +1,16 @@
<template>
<v-app>
<v-app-bar clipped-left dense app color="primary" dark class="d-print-none">
<router-link to="/">
<v-btn icon>
<v-btn @click="$router.push('/')" icon>
<v-icon size="40"> mdi-silverware-variant </v-icon>
</v-btn>
</router-link>
<div btn class="pl-2">
<v-toolbar-title style="cursor: pointer" @click="$router.push('/')"
>Mealie
</v-toolbar-title>
<v-toolbar-title @click="$router.push('/')">Mealie</v-toolbar-title>
</div>
<v-spacer></v-spacer>
<v-expand-x-transition>
<SearchBar
ref="mainSearchBar"
class="mt-7"
v-if="search"
:show-results="true"
@ -35,7 +29,6 @@
<SnackBar />
<router-view></router-view>
</v-container>
<FlashMessage :position="'right bottom'"></FlashMessage>
</v-main>
</v-app>
</template>
@ -61,13 +54,6 @@ export default {
this.search = false;
},
},
created() {
window.addEventListener("keyup", e => {
if (e.key == "/") {
this.search = !this.search;
}
});
},
mounted() {
this.$store.dispatch("initTheme");
@ -108,34 +94,5 @@ export default {
</script>
<style>
.notify-info-color {
border: 1px, solid, var(--v-info-base) !important;
border-left: 3px, solid, var(--v-info-base) !important;
background-color: var(--v-info-base) !important;
}
.notify-warning-color {
border: 1px, solid, var(--v-warning-base) !important;
border-left: 3px, solid, var(--v-warning-base) !important;
background-color: var(--v-warning-base) !important;
}
.notify-error-color {
border: 1px, solid, var(--v-error-base) !important;
border-left: 3px, solid, var(--v-error-base) !important;
background-color: var(--v-error-base) !important;
}
.notify-success-color {
border: 1px, solid, var(--v-success-base) !important;
border-left: 3px, solid, var(--v-success-base) !important;
background-color: var(--v-success-base) !important;
}
.notify-base {
color: white !important;
margin-right: 60px;
margin-bottom: -5px;
opacity: 0.9 !important;
}
</style>

View file

@ -8,7 +8,7 @@ import myUtils from "./api/upload";
import category from "./api/category";
import meta from "./api/meta";
// import api from "@/api";
// import api from "../api";
export default {
recipes: recipe,

View file

@ -1,20 +1,23 @@
const baseURL = "/api/";
import axios from "axios";
import utils from "@/utils";
import store from "../store/store";
// look for data.snackbar in response
function processResponse(response) {
try {
utils.notify.show(response.data.snackbar.text, response.data.snackbar.type);
store.commit("setSnackBar", {
text: response.data.snackbar.text,
type: response.data.snackbar.type,
});
} catch (err) {
return;
}
return;
}
const apiReq = {
post: async function(url, data) {
let response = await axios.post(url, data).catch(function(error) {
post: async function (url, data) {
let response = await axios.post(url, data).catch(function (error) {
if (error.response) {
processResponse(error.response);
return error.response;
@ -24,19 +27,8 @@ const apiReq = {
return response;
},
put: async function(url, data) {
let response = await axios.put(url, data).catch(function(error) {
if (error.response) {
processResponse(error.response);
return response;
} else return;
});
processResponse(response);
return response;
},
get: async function(url, data) {
let response = await axios.get(url, data).catch(function(error) {
put: async function (url, data) {
let response = await axios.put(url, data).catch(function (error) {
if (error.response) {
processResponse(error.response);
return response;
@ -46,8 +38,19 @@ const apiReq = {
return response;
},
delete: async function(url, data) {
let response = await axios.delete(url, data).catch(function(error) {
get: async function (url, data) {
let response = await axios.get(url, data).catch(function (error) {
if (error.response) {
processResponse(error.response);
return response;
} else return;
});
// processResponse(response);
return response;
},
delete: async function (url, data) {
let response = await axios.delete(url, data).catch(function (error) {
if (error.response) {
processResponse(error.response);
return response;

View file

@ -5,10 +5,10 @@ const prefix = baseURL + "themes";
const settingsURLs = {
allThemes: `${baseURL}themes`,
specificTheme: themeName => `${prefix}/${themeName}`,
specificTheme: (themeName) => `${prefix}/${themeName}`,
createTheme: `${prefix}/create`,
updateTheme: themeName => `${prefix}/${themeName}`,
deleteTheme: themeName => `${prefix}/${themeName}`,
updateTheme: (themeName) => `${prefix}/${themeName}`,
deleteTheme: (themeName) => `${prefix}/${themeName}`,
};
export default {
@ -33,7 +33,6 @@ export default {
colors: colors,
};
let response = await apiReq.put(settingsURLs.updateTheme(themeName), body);
console.log(response.data);
return response.data;
},

View file

@ -1,7 +1,7 @@
import { apiReq } from "./api-utils";
export default {
// import api from "@/api";
// import api from "../api";
async uploadFile(url, fileObject) {
let response = await apiReq.post(url, fileObject, {
headers: {

View file

@ -26,7 +26,7 @@
</template>
<script>
import utils from "@/utils";
import utils from "../../utils";
import SearchDialog from "../UI/SearchDialog";
export default {
components: {

View file

@ -20,8 +20,8 @@
</template>
<script>
import api from "@/api";
import utils from "@/utils";
import api from "../../api";
import utils from "../../utils";
import MealPlanCard from "./MealPlanCard";
export default {
components: {

View file

@ -85,8 +85,8 @@
</template>
<script>
import api from "@/api";
import utils from "@/utils";
import api from "../../api";
import utils from "../../utils";
import MealPlanCard from "./MealPlanCard";
export default {
components: {

View file

@ -41,23 +41,21 @@
>
</v-text-field>
<v-textarea
auto-grow
min-height="100"
height="100"
:label="$t('recipe.description')"
v-model="value.description"
>
</v-textarea>
<div class="my-2"></div>
<v-row dense disabled>
<v-col sm="4">
<v-col sm="5">
<v-text-field
:label="$t('recipe.servings')"
v-model="value.recipeYield"
class="rounded-sm"
>
</v-text-field>
</v-col>
<v-spacer></v-spacer>
<v-col></v-col>
<v-rating
class="mr-2 align-end"
color="secondary darken-1"
@ -188,7 +186,6 @@
</v-row>
<v-textarea
auto-grow
:label="$t('recipe.note')"
v-model="value.notes[index]['text']"
>
@ -221,18 +218,17 @@
elevation="0"
@click="removeStep(index)"
>
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
{{ $t("recipe.step-index", { step: index + 1 }) }}
</v-card-title>
<v-icon color="error">mdi-delete</v-icon> </v-btn
>{{
$t("recipe.step-index", { step: index + 1 })
}}</v-card-title
>
<v-card-text>
<v-textarea
auto-grow
dense
v-model="value.recipeInstructions[index]['text']"
:key="generateKey('instructions', index)"
>
</v-textarea>
></v-textarea>
</v-card-text>
</v-card>
</v-hover>
@ -254,8 +250,8 @@
<script>
import draggable from "vuedraggable";
import api from "@/api";
import utils from "@/utils";
import api from "../../../api";
import utils from "../../../utils";
import BulkAdd from "./BulkAdd";
import ExtrasEditor from "./ExtrasEditor";
export default {

View file

@ -161,7 +161,7 @@
</template>
<script>
import utils from "@/utils";
import utils from "../../utils";
export default {
props: {

View file

@ -0,0 +1,193 @@
<template>
<div>
<v-card-title class="headline">
{{ name }}
</v-card-title>
<v-card-text>
<vue-markdown :source="description"> </vue-markdown>
<div class="my-2"></div>
<v-row dense disabled>
<v-col>
<v-btn
v-if="yields"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
color="secondary darken-1"
class="rounded-sm static"
>
{{ yields }}
</v-btn>
</v-col>
<v-rating
class="mr-2 align-end static"
color="secondary darken-1"
background-color="secondary lighten-3"
length="5"
:value="rating"
></v-rating>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<div
v-for="(ingredient, index) in ingredients"
:key="generateKey('ingredient', index)"
>
<v-checkbox
hide-details
class="ingredients"
:label="ingredient"
color="secondary"
>
</v-checkbox>
</div>
<div v-if="categories[0]">
<h2 class="mt-4">{{ $t("recipe.categories") }}</h2>
<v-chip
class="ma-1"
color="accent"
dark
v-for="category in categories"
:key="category"
>
{{ category }}
</v-chip>
</div>
<div v-if="tags[0]">
<h2 class="mt-4">{{ $t("recipe.tags") }}</h2>
<v-chip
class="ma-1"
color="accent"
dark
v-for="tag in tags"
:key="tag"
>
{{ tag }}
</v-chip>
</div>
<h2 v-if="notes[0]" class="my-4">{{ $t("recipe.notes") }}</h2>
<v-card
class="mt-1"
v-for="(note, index) in notes"
:key="generateKey('note', index)"
>
<v-card-title> {{ note.title }}</v-card-title>
<v-card-text>
{{ note.text }}
</v-card-text>
</v-card>
</v-col>
<v-divider class="my-divider" :vertical="true"></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
<v-hover
v-for="(step, index) in instructions"
:key="generateKey('step', index)"
v-slot="{ hover }"
>
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }, isDisabled(index)]"
:elevation="hover ? 12 : 2"
@click="toggleDisabled(index)"
>
<v-card-title>{{
$t("recipe.step-index", { step: index + 1 })
}}</v-card-title>
<v-card-text>
<vue-markdown>
{{ step.text }}
</vue-markdown>
</v-card-text>
</v-card>
</v-hover>
</v-col>
</v-row>
<v-row>
<v-col></v-col>
<v-btn
v-if="orgURL"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
:href="orgURL"
color="secondary darken-1"
target="_blank"
class="rounded-sm mr-4"
>
{{ $t("recipe.original-url") }}
</v-btn>
</v-row>
</v-card-text>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "../../utils";
export default {
components: {
VueMarkdown,
},
props: {
name: String,
description: String,
ingredients: Array,
instructions: Array,
categories: Array,
tags: Array,
notes: Array,
rating: Number,
yields: String,
orgURL: String,
},
data() {
return {
disabledSteps: [],
};
},
methods: {
toggleDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
let index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) {
this.disabledSteps.splice(index, 1);
}
} else {
this.disabledSteps.push(stepIndex);
}
},
isDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
return "disabled-card";
} else {
return;
}
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
.static {
pointer-events: none;
}
.my-divider {
margin: 0 -1px;
}
</style>

View file

@ -1,34 +0,0 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<div
v-for="(ingredient, index) in ingredients"
:key="generateKey('ingredient', index)"
>
<v-checkbox
hide-details
class="ingredients"
:label="ingredient"
color="secondary"
>
</v-checkbox>
</div>
</div>
</template>
<script>
import utils from "@/utils";
export default {
props: {
ingredients: Array,
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
</style>

View file

@ -1,36 +0,0 @@
<template>
<div>
<h2 v-if="notes[0]" class="my-4">{{ $t("recipe.notes") }}</h2>
<v-card
class="mt-1"
v-for="(note, index) in notes"
:key="generateKey('note', index)"
>
<v-card-title> {{ note.title }}</v-card-title>
<v-card-text>
<vue-markdown :source="note.text"> </vue-markdown>
</v-card-text>
</v-card>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
export default {
props: {
notes: Array,
},
components: {
VueMarkdown,
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
</style>

View file

@ -1,26 +0,0 @@
<template>
<div v-if="items[0]">
<h2 class="mt-4">{{ title }}</h2>
<v-chip
class="ma-1"
color="accent"
dark
v-for="category in items"
:key="category"
>
{{ category }}
</v-chip>
</div>
</template>
<script>
export default {
props: {
items: Array,
title: String,
},
};
</script>
<style>
</style>

View file

@ -1,67 +0,0 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
<v-hover
v-for="(step, index) in steps"
:key="generateKey('step', index)"
v-slot="{ hover }"
>
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }, isDisabled(index)]"
:elevation="hover ? 12 : 2"
@click="toggleDisabled(index)"
>
<v-card-title>{{
$t("recipe.step-index", { step: index + 1 })
}}</v-card-title>
<v-card-text>
<vue-markdown :source="step.text"> </vue-markdown>
</v-card-text>
</v-card>
</v-hover>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
export default {
props: {
steps: Array,
},
components: {
VueMarkdown,
},
data() {
return {
disabledSteps: [],
};
},
methods: {
toggleDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
let index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) {
this.disabledSteps.splice(index, 1);
}
} else {
this.disabledSteps.push(stepIndex);
}
},
isDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
return "disabled-card";
} else {
return;
}
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
</style>

View file

@ -1,130 +0,0 @@
<template>
<div>
<v-card-title class="headline">
{{ name }}
</v-card-title>
<v-card-text>
<vue-markdown :source="description"> </vue-markdown>
<v-row dense disabled>
<v-col>
<v-btn
v-if="yields"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
color="secondary darken-1"
class="rounded-sm static"
>
{{ yields }}
</v-btn>
</v-col>
<v-rating
class="mr-2 align-end static"
color="secondary darken-1"
background-color="secondary lighten-3"
length="5"
:value="rating"
></v-rating>
</v-row>
<v-row>
<v-col cols="12" sm="12" md="4" lg="4">
<Ingredients :ingredients="ingredients" />
<div v-if="medium">
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
<Notes :notes="notes" />
</div>
</v-col>
<v-divider
v-if="medium"
class="my-divider"
:vertical="true"
></v-divider>
<v-col cols="12" sm="12" md="8" lg="8">
<Steps :steps="instructions" />
</v-col>
</v-row>
<div v-if="!medium">
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
<Notes :notes="notes" />
</div>
<v-row class="mt-2 mb-1">
<v-col></v-col>
<v-btn
v-if="orgURL"
dense
small
:hover="false"
type="label"
:ripple="false"
elevation="0"
:href="orgURL"
color="secondary darken-1"
target="_blank"
class="rounded-sm mr-4"
>
{{ $t("recipe.original-url") }}
</v-btn>
</v-row>
</v-card-text>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
import RecipeChips from "./RecipeChips";
import Steps from "./Steps";
import Notes from "./Notes";
import Ingredients from "./Ingredients";
export default {
components: {
VueMarkdown,
RecipeChips,
Steps,
Notes,
Ingredients,
},
props: {
name: String,
description: String,
ingredients: Array,
instructions: Array,
categories: Array,
tags: Array,
notes: Array,
rating: Number,
yields: String,
orgURL: String,
},
data() {
return {
disabledSteps: [],
};
},
computed: {
medium() {
return this.$vuetify.breakpoint.mdAndUp;
},
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
},
};
</script>
<style>
.static {
pointer-events: none;
}
.my-divider {
margin: 0 -1px;
}
</style>

View file

@ -38,8 +38,8 @@
<script>
import ImportDialog from "./ImportDialog";
import api from "@/api";
import utils from "@/utils";
import api from "../../../api";
import utils from "../../../utils";
export default {
props: {
backups: Array,

View file

@ -38,8 +38,8 @@
<script>
import ImportDialog from "./ImportDialog";
import api from "@/api";
import utils from "@/utils";
import api from "../../../api";
import utils from "../../../utils";
export default {
props: {
backups: Array,

View file

@ -46,7 +46,7 @@
</template>
<script>
import api from "@/api";
import api from "../../../api";
export default {
data() {
return {
@ -84,7 +84,7 @@ export default {
methods: {
async getAvailableBackups() {
let response = await api.backups.requestAvailable();
response.templates.forEach(element => {
response.templates.forEach((element) => {
this.availableTemplates.push(element);
});
},
@ -101,6 +101,7 @@ export default {
templates: this.selectedTemplates,
};
await api.backups.create(data);
this.loading = false;

View file

@ -46,7 +46,7 @@
</template>
<script>
import api from "@/api";
import api from "../../../api";
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
import UploadBtn from "../../UI/UploadBtn";
import AvailableBackupCard from "./AvailableBackupCard";

View file

@ -126,7 +126,7 @@
</template>
<script>
import api from "@/api";
import api from "../../../api";
import draggable from "vuedraggable";
export default {

View file

@ -56,8 +56,8 @@
<script>
import UploadBtn from "../../UI/UploadBtn";
import utils from "@/utils";
import api from "@/api";
import utils from "../../../utils";
import api from "../../../api";
export default {
props: {
folder: String,

View file

@ -41,7 +41,7 @@
<script>
import MigrationCard from "./MigrationCard";
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
import api from "@/api";
import api from "../../../api";
export default {
components: {
MigrationCard,
@ -78,7 +78,7 @@ export default {
},
async getAvailableMigrations() {
let response = await api.migrations.getMigrations();
response.forEach(element => {
response.forEach((element) => {
if (element.type === "nextcloud") {
this.migrations.nextcloud.availableImports = element.files;
} else if (element.type === "chowdown") {

View file

@ -53,7 +53,7 @@
return-object
v-model="selectedTheme"
@change="themeSelected"
:rules="[v => !!v || $t('settings.theme.theme-is-required')]"
:rules="[(v) => !!v || $t('settings.theme.theme-is-required')]"
required
>
</v-select>
@ -136,7 +136,7 @@
</template>
<script>
import api from "@/api";
import api from "../../../api";
import ColorPickerDialog from "./ColorPickerDialog";
import NewThemeDialog from "./NewThemeDialog";
import Confirmation from "../../UI/Confirmation";
@ -186,7 +186,7 @@ export default {
//Change to default if deleting current theme.
if (
!this.availableThemes.some(
theme => theme.name === this.selectedTheme.name
(theme) => theme.name === this.selectedTheme.name
)
) {
await this.$store.dispatch("resetTheme");

View file

@ -56,7 +56,7 @@
</template>
<script>
import api from "@/api";
import api from "../../../api";
import TimePickerDialog from "./TimePickerDialog";
export default {
components: {

View file

@ -49,7 +49,7 @@
</template>
<script>
import api from "@/api";
import api from "../../api";
export default {
data() {

View file

@ -44,14 +44,14 @@
color="primary"
block="block"
type="submit"
>{{ $t("login.sign-in") }}</v-btn
>{{$t('login.sign-in')}}</v-btn
>
<v-btn
v-else
block="block"
type="submit"
@click.prevent="options.isLoggingIn = true"
>{{ $t("login.sign-up") }}</v-btn
>{{$t('login.sign-up')}}</v-btn
>
</v-form>
</v-card-text>
@ -72,7 +72,7 @@
</template>
<script>
import api from "@/api";
import api from "../../api";
export default {
props: {},
data() {

View file

@ -40,7 +40,7 @@
</template>
<script>
import utils from "@/utils";
import utils from "../../utils";
export default {
props: {
name: String,

View file

@ -12,8 +12,6 @@
hide-no-data
cache-items
solo
autofocus
auto-select-first
>
<template
v-if="showResults"
@ -45,7 +43,7 @@
<script>
import Fuse from "fuse.js";
import utils from "@/utils";
import utils from "../../utils";
export default {
props: {

View file

@ -9,7 +9,7 @@
</template>
<script>
import api from "@/api";
import api from "../../api";
export default {
props: {
url: String,

View file

@ -5,9 +5,7 @@ import store from "./store/store";
import VueRouter from "vue-router";
import { routes } from "./routes";
import i18n from "./i18n";
import FlashMessage from "@smartweb/vue-flash-message";
Vue.use(FlashMessage);
Vue.config.productionTip = false;
Vue.use(VueRouter);
@ -16,16 +14,16 @@ const router = new VueRouter({
mode: process.env.NODE_ENV === "production" ? "history" : "hash",
});
const vueApp = new Vue({
new Vue({
vuetify,
store,
router,
i18n,
render: h => h(App),
render: (h) => h(App),
}).$mount("#app");
// Truncate
let truncate = function(text, length, clamp) {
let truncate = function (text, length, clamp) {
clamp = clamp || "...";
let node = document.createElement("div");
node.innerHTML = text;
@ -33,12 +31,11 @@ let truncate = function(text, length, clamp) {
return content.length > length ? content.slice(0, length) + clamp : content;
};
let titleCase = function(value) {
return value.replace(/(?:^|\s|-)\S/g, x => x.toUpperCase());
let titleCase = function (value) {
return value.replace(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
};
Vue.filter("truncate", truncate);
Vue.filter("titleCase", titleCase);
export { vueApp };
export { router };

View file

@ -13,7 +13,7 @@
</template>
<script>
import api from "@/api";
import api from "../api";
import CardSection from "../components/UI/CardSection";
import CategorySidebar from "../components/UI/CategorySidebar";
export default {

View file

@ -21,7 +21,7 @@
</template>
<script>
import api from "@/api";
import api from "../api";
import CardSection from "../components/UI/CardSection";
import CategorySidebar from "../components/UI/CategorySidebar";
export default {
@ -55,7 +55,7 @@ export default {
},
methods: {
async buildPage() {
this.homeCategories.forEach(async element => {
this.homeCategories.forEach(async (element) => {
let recipes = await this.getRecipeByCategory(element.slug);
recipes.position = element.position;
this.recipeByCategory.push(recipes);

View file

@ -74,8 +74,8 @@
</template>
<script>
import api from "@/api";
import utils from "@/utils";
import api from "../api";
import utils from "../utils";
import NewMeal from "../components/MealPlan/MealPlanNew";
import EditPlan from "../components/MealPlan/MealPlanEditor";

View file

@ -49,8 +49,8 @@
</template>
<script>
import api from "@/api";
import utils from "@/utils";
import api from "../api";
import utils from "../utils";
export default {
data() {
return {

View file

@ -39,7 +39,7 @@
</template>
<script>
import api from "@/api";
import api from "../api";
import RecipeEditor from "../components/Recipe/RecipeEditor";
import VJsoneditor from "v-jsoneditor";

View file

@ -56,8 +56,8 @@
</template>
<script>
import api from "@/api";
import utils from "@/utils";
import api from "../api";
import utils from "../utils";
import VJsoneditor from "v-jsoneditor";
import RecipeViewer from "../components/Recipe/RecipeViewer";
import RecipeEditor from "../components/Recipe/RecipeEditor";
@ -107,7 +107,7 @@ export default {
},
watch: {
$route: function() {
$route: function () {
this.getRecipeDetails();
},
},

View file

@ -44,7 +44,7 @@ import General from "../components/Settings/General";
import Webhooks from "../components/Settings/Webhook";
import Theme from "../components/Settings/Theme";
import Migration from "../components/Settings/Migration";
import api from "@/api";
import api from "../api";
import axios from "axios";
export default {

View file

@ -6,8 +6,6 @@ Vue.use(Vuetify);
const vuetify = new Vuetify({
theme: {
dark: false,
options: { customProperties: true },
themes: {
light: {
primary: "#E58325",

View file

@ -8,7 +8,7 @@ import AllRecipesPage from "./pages/AllRecipesPage";
import CategoryPage from "./pages/CategoryPage";
import MeaplPlanPage from "./pages/MealPlanPage";
import MealPlanThisWeekPage from "./pages/MealPlanThisWeekPage";
import api from "@/api";
import api from "./api";
export const routes = [
{ path: "/", component: HomePage },
@ -24,7 +24,7 @@ export const routes = [
{
path: "/meal-plan/today",
beforeEnter: async (_to, _from, next) => {
await todaysMealRoute().then(redirect => {
await todaysMealRoute().then((redirect) => {
next(redirect);
});
},

View file

@ -1,4 +1,4 @@
import api from "@/api";
import api from "../../api";
const state = {
showRecent: true,
@ -30,10 +30,10 @@ const actions = {
};
const getters = {
getShowRecent: state => state.showRecent,
getShowLimit: state => state.showLimit,
getCategories: state => state.categories,
getHomeCategories: state => state.homeCategories,
getShowRecent: (state) => state.showRecent,
getShowLimit: (state) => state.showLimit,
getCategories: (state) => state.categories,
getHomeCategories: (state) => state.homeCategories,
};
export default {

View file

@ -1,4 +1,4 @@
import api from "@/api";
import api from "../../api";
import Vuetify from "../../plugins/vuetify";
function inDarkMode(payload) {
@ -60,9 +60,9 @@ const actions = {
};
const getters = {
getActiveTheme: state => state.activeTheme,
getDarkMode: state => state.darkMode,
getIsDark: state => state.isDark,
getActiveTheme: (state) => state.activeTheme,
getDarkMode: (state) => state.darkMode,
getIsDark: (state) => state.isDark,
};
export default {

View file

@ -1,6 +1,6 @@
import Vue from "vue";
import Vuex from "vuex";
import api from "@/api";
import api from "../api";
import createPersistedState from "vuex-persistedstate";
import userSettings from "./modules/userSettings";
import language from "./modules/language";
@ -64,11 +64,11 @@ const store = new Vuex.Store({
getters: {
//
getSnackText: state => state.snackText,
getSnackActive: state => state.snackActive,
getSnackType: state => state.snackType,
getSnackText: (state) => state.snackText,
getSnackActive: (state) => state.snackActive,
getSnackType: (state) => state.snackType,
getRecentRecipes: state => state.recentRecipes,
getRecentRecipes: (state) => state.recentRecipes,
},
});

View file

@ -1,15 +1,6 @@
// import utils from "@/utils";
// import utils from "../../utils";
// import Vue from "vue";
// import Vuetify from "./plugins/vuetify";
import { vueApp } from "./main";
const notifyHelpers = {
baseCSS: "notify-base",
error: "notify-error-color",
warning: "notify-warning-color",
success: "notify-success-color",
info: "notify-info-color",
};
const days = [
"Sunday",
@ -81,28 +72,4 @@ export default {
return `${year}-${month}-${day}`;
},
notify: {
show: function(text, type = "info", title = null) {
vueApp.flashMessage.show({
status: type,
title: title,
message: text,
time: 3000,
blockClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
contentClass: `${notifyHelpers.baseCSS} ${notifyHelpers[type]}`,
});
},
info: function(text, title = null) {
this.show(text, "info", title);
},
success: function(text, title = null) {
this.show(text, "success", title);
},
error: function(text, title = null) {
this.show(text, "error", title);
},
warning: function(text, title = null) {
this.show(text, "warning", title);
},
},
};

View file

@ -12,6 +12,7 @@ from routes import (
setting_routes,
static_routes,
theme_routes,
user_routes,
)
from routes.recipe import (
all_recipe_routes,
@ -19,9 +20,20 @@ from routes.recipe import (
recipe_crud_routes,
tag_routes,
)
from services.settings_services import default_settings_init
from utils.logger import logger
"""
TODO:
- [x] Fix Duplicate Category
- [x] Fix category overflow
- [ ] Enable Database Name Versioning
- [ ] Finish Frontend Category Management
- [x] Delete Category
- [ ] Sort Sidebar A-Z
- [ ] Refactor Test Endpoints - Abstract to fixture?
"""
app = FastAPI(
title="Mealie",
description="A place for all your recipes",
@ -39,11 +51,6 @@ def start_scheduler():
import services.scheduler.scheduled_jobs
def init_settings():
default_settings_init()
import services.theme_services
def api_routers():
# Recipes
app.include_router(all_recipe_routes.router)
@ -57,6 +64,8 @@ def api_routers():
app.include_router(theme_routes.router)
# Backups/Imports Routes
app.include_router(backup_routes.router)
# User Routes
app.include_router(user_routes.router)
# Migration Routes
app.include_router(migration_routes.router)
app.include_router(debug_routes.router)
@ -81,7 +90,6 @@ app.include_router(static_routes.router)
# generate_api_docs(app)
start_scheduler()
init_settings()
if __name__ == "__main__":
logger.info("-----SYSTEM STARTUP-----")

View file

@ -17,7 +17,6 @@ dotenv.load_dotenv(ENV)
# General
APP_VERSION = "v0.2.0"
DB_VERSION = "v0.2.0"
PRODUCTION = os.environ.get("ENV")
PORT = int(os.getenv("mealie_port", 9000))
API = os.getenv("api_docs", True)
@ -65,7 +64,7 @@ SQLITE_FILE = None
DATABASE_TYPE = os.getenv("db_type", "sqlite")
if DATABASE_TYPE == "sqlite":
USE_SQL = True
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{DB_VERSION}.sqlite")
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{APP_VERSION}.sqlite")
else:
raise Exception(

View file

@ -9,6 +9,7 @@ from db.sql.theme_models import SiteThemeModel
"""
# TODO
- [ ] Abstract Classes to use save_new, and update from base models
- [x] Create Category and Tags Table with Many to Many relationship
"""
@ -17,7 +18,7 @@ class _Recipes(BaseDocument):
self.primary_key = "slug"
self.sql_model = RecipeModel
def update_image(self, session: Session, slug: str, extension: str = None) -> str:
def update_image(self, session: Session, slug: str, extension: str) -> str:
entry: RecipeModel = self._query_one(session, match_value=slug)
entry.image = f"{slug}.{extension}"
session.commit()
@ -48,14 +49,13 @@ class _Settings(BaseDocument):
self.primary_key = "name"
self.sql_model = SiteSettingsModel
def create(self, session: Session, main: dict, webhooks: dict) -> str:
def save_new(self, session: Session, main: dict, webhooks: dict) -> str:
new_settings = self.sql_model(main.get("name"), webhooks)
session.add(new_settings)
return_data = new_settings.dict()
session.commit()
return return_data
return new_settings.dict()
class _Themes(BaseDocument):

View file

@ -106,7 +106,7 @@ class BaseDocument:
return db_entry
def create(self, session: Session, document: dict) -> dict:
def save_new(self, session: Session, document: dict) -> dict:
"""Creates a new database entry for the given SQL Alchemy Model.
Args: \n

View file

@ -1,38 +0,0 @@
from datetime import date
from typing import List, Optional
from pydantic import BaseModel
class Meal(BaseModel):
slug: Optional[str]
name: Optional[str]
date: date
dateText: str
image: Optional[str]
description: Optional[str]
class MealData(BaseModel):
name: Optional[str]
slug: str
dateText: str
class MealPlan(BaseModel):
uid: Optional[str]
startDate: date
endDate: date
meals: List[Meal]
class Config:
schema_extra = {
"example": {
"startDate": date.today(),
"endDate": date.today(),
"meals": [
{"slug": "Packed Mac and Cheese", "date": date.today()},
{"slug": "Eggs and Toast", "date": date.today()},
],
}
}

View file

@ -1,79 +1,37 @@
import datetime
from typing import Any, List, Optional
from typing import List, Optional
from pydantic import BaseModel, validator
from slugify import slugify
import pydantic
from pydantic.main import BaseModel
class RecipeNote(BaseModel):
title: str
text: str
class AllRecipeResponse(BaseModel):
class RecipeStep(BaseModel):
text: str
class Recipe(BaseModel):
# Standard Schema
name: str
description: Optional[str]
image: Optional[Any]
recipeYield: Optional[str]
recipeIngredient: Optional[list]
recipeInstructions: Optional[list]
totalTime: Optional[str] = None
prepTime: Optional[str] = None
performTime: Optional[str] = None
# Mealie Specific
slug: Optional[str] = ""
categories: Optional[List[str]] = []
tags: Optional[List[str]] = []
dateAdded: Optional[datetime.date]
notes: Optional[List[RecipeNote]] = []
rating: Optional[int]
orgURL: Optional[str]
extras: Optional[dict] = {}
class Config:
schema_extra = {
"example": {
"name": "Chicken and Rice With Leeks and Salsa Verde",
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
"recipeYield": "4 Servings",
"recipeIngredient": [
"1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)",
"Kosher salt, freshly ground pepper",
"3 Tbsp. unsalted butter, divided",
],
"recipeInstructions": [
"example": [
{
"text": "Season chicken with salt and pepper.",
"slug": "crockpot-buffalo-chicken",
"image": "crockpot-buffalo-chicken.jpg",
"name": "Crockpot Buffalo Chicken",
},
],
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
"tags": ["favorite", "yummy!"],
"categories": ["Dinner", "Pasta"],
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
"rating": 3,
"extras": {"message": "Don't forget to defrost the chicken!"},
{
"slug": "downtown-marinade",
"image": "downtown-marinade.jpg",
"name": "Downtown Marinade",
},
{
"slug": "detroit-style-pepperoni-pizza",
"image": "detroit-style-pepperoni-pizza.jpg",
"name": "Detroit-Style Pepperoni Pizza",
},
{
"slug": "crispy-carrots",
"image": "crispy-carrots.jpg",
"name": "Crispy Carrots",
},
]
}
}
@validator("slug", always=True, pre=True)
def validate_slug(slug: str, values):
name: str = values["name"]
calc_slug: str = slugify(name)
if slug == calc_slug:
return slug
else:
slug = calc_slug
return slug
class AllRecipeRequest(BaseModel):

View file

@ -1,26 +0,0 @@
from typing import List, Optional
from pydantic import BaseModel
class Webhooks(BaseModel):
webhookTime: str = "00:00"
webhookURLs: Optional[List[str]] = []
enabled: bool = False
class SiteSettings(BaseModel):
name: str = "main"
webhooks: Webhooks
class Config:
schema_extra = {
"example": {
"name": "main",
"webhooks": {
"webhookTime": "00:00",
"webhookURLs": ["https://mywebhookurl.com/webhook"],
"enable": False,
},
}
}

View file

@ -1,31 +0,0 @@
from pydantic import BaseModel
class Colors(BaseModel):
primary: str
accent: str
secondary: str
success: str
info: str
warning: str
error: str
class SiteTheme(BaseModel):
name: str
colors: Colors
class Config:
schema_extra = {
"example": {
"name": "default",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
},
}
}

View file

@ -0,0 +1,10 @@
from typing import Optional
from pydantic import BaseModel
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None

View file

@ -32,10 +32,10 @@ def available_imports():
@router.post("/export/database", status_code=201)
def export_database(data: BackupJob, session: Session = Depends(generate_session)):
def export_database(data: BackupJob, db: Session = Depends(generate_session)):
"""Generates a backup of the recipe database in json format."""
export_path = backup_all(
session=session,
session=db,
tag=data.tag,
templates=data.templates,
export_recipes=data.options.recipes,
@ -66,7 +66,7 @@ def upload_backup_zipfile(archive: UploadFile = File(...)):
@router.get("/{file_name}/download")
async def upload_nextcloud_zipfile(file_name: str):
def upload_nextcloud_zipfile(file_name: str):
""" Upload a .zip File to later be imported into Mealie """
file = BACKUP_DIR.joinpath(file_name)
@ -80,12 +80,12 @@ async def upload_nextcloud_zipfile(file_name: str):
@router.post("/{file_name}/import", status_code=200)
def import_database(
file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)
file_name: str, import_data: ImportJob, db: Session = Depends(generate_session)
):
""" Import a database backup file generated from Mealie. """
import_db = ImportDatabase(
session=session,
session=db,
zip_archive=import_data.name,
import_recipes=import_data.recipes,
force_import=import_data.force,
@ -110,4 +110,4 @@ def delete_backup(file_name: str):
detail=SnackResponse.error("Unable to Delete Backup. See Log File"),
)
return SnackResponse.error(f"{file_name} Deleted")
return SnackResponse.success(f"{file_name} Deleted")

View file

@ -27,7 +27,18 @@ async def get_log(num: int):
""" Doc Str """
with open(LOGGER_FILE, "rb") as f:
log_text = tail(f, num)
HTML_RESPONSE = log_text
HTML_RESPONSE = f"""
<html>
<head>
<title>Mealie Log</title>
</head>
<body style="white-space: pre-line">
<p>
{log_text}
</p>
</body>
</html>
"""
return HTML_RESPONSE

View file

@ -10,53 +10,66 @@ router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@router.get("/all", response_model=List[MealPlan])
def get_all_meals(session: Session = Depends(generate_session)):
def get_all_meals(db: Session = Depends(generate_session)):
""" Returns a list of all available Meal Plan """
return MealPlan.get_all(session)
return MealPlan.get_all(db)
@router.post("/create")
def set_meal_plan(data: MealPlan, session: Session = Depends(generate_session)):
def set_meal_plan(data: MealPlan, db: Session = Depends(generate_session)):
""" Creates a meal plan database entry """
data.process_meals(session)
data.save_to_db(session)
data.process_meals(db)
data.save_to_db(db)
# raise HTTPException(
# status_code=404,
# detail=SnackResponse.error("Unable to Create Mealplan See Log"),
# )
return SnackResponse.success("Mealplan Created")
@router.get("/this-week", response_model=MealPlan)
def get_this_week(session: Session = Depends(generate_session)):
def get_this_week(db: Session = Depends(generate_session)):
""" Returns the meal plan data for this week """
return MealPlan.this_week(session)
return MealPlan.this_week(db)
@router.put("/{plan_id}")
def update_meal_plan(
plan_id: str, meal_plan: MealPlan, session: Session = Depends(generate_session)
plan_id: str, meal_plan: MealPlan, db: Session = Depends(generate_session)
):
""" Updates a meal plan based off ID """
meal_plan.process_meals(session)
meal_plan.update(session, plan_id)
meal_plan.process_meals(db)
meal_plan.update(db, plan_id)
# try:
# meal_plan.process_meals()
# meal_plan.update(plan_id)
# except:
# raise HTTPException(
# status_code=404,
# detail=SnackResponse.error("Unable to Update Mealplan"),
# )
return SnackResponse.info("Mealplan Updated")
return SnackResponse.success("Mealplan Updated")
@router.delete("/{plan_id}")
def delete_meal_plan(plan_id, session: Session = Depends(generate_session)):
def delete_meal_plan(plan_id, db: Session = Depends(generate_session)):
""" Removes a meal plan from the database """
MealPlan.delete(session, plan_id)
MealPlan.delete(db, plan_id)
return SnackResponse.error("Mealplan Deleted")
return SnackResponse.success("Mealplan Deleted")
@router.get("/today", tags=["Meal Plan"])
def get_today(session: Session = Depends(generate_session)):
def get_today(db: Session = Depends(generate_session)):
"""
Returns the recipe slug for the meal scheduled for today.
If no meal is scheduled nothing is returned
"""
return MealPlan.today(session)
return MealPlan.today(db)

View file

@ -37,14 +37,14 @@ def get_avaiable_nextcloud_imports():
@router.post("/{type}/{file_name}/import")
def import_nextcloud_directory(
type: str, file_name: str, session: Session = Depends(generate_session)
type: str, file_name: str, db: Session = Depends(generate_session)
):
""" Imports all the recipes in a given directory """
file_path = MIGRATION_DIR.joinpath(type, file_name)
if type == "nextcloud":
return nextcloud_migrate(session, file_path)
return nextcloud_migrate(db, file_path)
elif type == "chowdown":
return chowdow_migrate(session, file_path)
return chowdow_migrate(db, file_path)
else:
return SnackResponse.error("Incorrect Migration Type Selected")
@ -62,7 +62,7 @@ def delete_migration_data(type: str, file_name: str):
else:
SnackResponse.error("File/Folder not found.")
return SnackResponse.error(f"Migration Data Remove: {remove_path.absolute()}")
return SnackResponse.info(f"Migration Data Remove: {remove_path.absolute()}")
@router.post("/{type}/upload")

View file

@ -4,8 +4,6 @@ from fastapi import APIRouter, Depends
from models.category_models import RecipeCategoryResponse
from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse
router = APIRouter(
prefix="/api/categories",
tags=["Recipe Categories"],
@ -35,5 +33,3 @@ async def delete_recipe_category(
from any recipes that contain it """
db.categories.delete(session, category)
return SnackResponse(f"Category Deleted: {category}")

View file

@ -62,11 +62,11 @@ def delete_recipe(recipe_slug: str, db: Session = Depends(generate_session)):
status_code=404, detail=SnackResponse.error("Unable to Delete Recipe")
)
return SnackResponse.error(f"Recipe {recipe_slug} Deleted")
return SnackResponse.success("Recipe Deleted")
@router.get("/{recipe_slug}/image")
async def get_recipe_img(recipe_slug: str):
def get_recipe_img(recipe_slug: str):
""" Takes in a recipe slug, returns the static image """
recipe_image = read_image(recipe_slug)
@ -75,13 +75,10 @@ async def get_recipe_img(recipe_slug: str):
@router.put("/{recipe_slug}/image")
def update_recipe_image(
recipe_slug: str,
image: bytes = File(...),
extension: str = Form(...),
session: Session = Depends(generate_session),
recipe_slug: str, image: bytes = File(...), extension: str = Form(...)
):
""" Removes an existing image and replaces it with the incoming file. """
response = write_image(recipe_slug, image, extension)
Recipe.update_image(session, recipe_slug, extension)
Recipe.update_image(recipe_slug, extension)
return response

View file

@ -3,8 +3,6 @@ from db.db_setup import generate_session
from fastapi import APIRouter, Depends
from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse
router = APIRouter(tags=["Recipes"])
router = APIRouter(
@ -32,5 +30,3 @@ async def delete_recipe_tag(tag: str, session: Session = Depends(generate_sessio
from any recipes that contain it"""
db.tags.delete(session, tag)
return SnackResponse.error(f"Tag Deleted: {tag}")

View file

@ -1,8 +1,6 @@
from db.database import db
from db.db_setup import generate_session
from fastapi import APIRouter, Depends
from models.settings_models import SiteSettings
from services.settings_services import default_settings_init
from services.settings_services import SiteSettings
from sqlalchemy.orm.session import Session
from utils.post_webhooks import post_webhooks
from utils.snackbar import SnackResponse
@ -11,24 +9,10 @@ router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
@router.get("")
def get_main_settings(session: Session = Depends(generate_session)):
def get_main_settings(db: Session = Depends(generate_session)):
""" Returns basic site settings """
try:
data = db.settings.get(session, "main")
except:
default_settings_init(session)
data = db.settings.get(session, "main")
return data
@router.put("")
def update_settings(data: SiteSettings, session: Session = Depends(generate_session)):
""" Returns Site Settings """
db.settings.update(session, "main", data.dict())
return SnackResponse.success("Settings Updated")
return SiteSettings.get_site_settings(db)
@router.post("/webhooks/test")
@ -36,3 +20,20 @@ def test_webhooks():
""" Run the function to test your webhooks """
return post_webhooks()
@router.put("")
def update_settings(data: SiteSettings, db: Session = Depends(generate_session)):
""" Returns Site Settings """
data.update(db)
# try:
# data.update()
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Save Settings")
# )
return SnackResponse.success("Settings Updated")

View file

@ -15,10 +15,10 @@ def facivon():
@router.get("/")
async def root():
def root():
return FileResponse(BASE_HTML)
@router.get("/{full_path:path}")
async def root_plus(full_path):
def root_plus(full_path):
return FileResponse(BASE_HTML)

View file

@ -1,47 +1,64 @@
from db.db_setup import generate_session
from fastapi import APIRouter, Depends
from models.theme_models import SiteTheme
from services.settings_services import SiteTheme
from sqlalchemy.orm.session import Session
from utils.snackbar import SnackResponse
from db.database import db
router = APIRouter(prefix="/api", tags=["Themes"])
@router.get("/themes")
def get_all_themes(session: Session = Depends(generate_session)):
def get_all_themes(db: Session = Depends(generate_session)):
""" Returns all site themes """
return db.themes.get_all(session)
return SiteTheme.get_all(db)
@router.post("/themes/create")
def create_theme(data: SiteTheme, session: Session = Depends(generate_session)):
def create_theme(data: SiteTheme, db: Session = Depends(generate_session)):
""" Creates a site color theme database entry """
db.themes.create(session, data.dict())
data.save_to_db(db)
# try:
# data.save_to_db()
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Save Theme")
# )
return SnackResponse.success("Theme Saved")
@router.get("/themes/{theme_name}")
def get_single_theme(theme_name: str, session: Session = Depends(generate_session)):
def get_single_theme(theme_name: str, db: Session = Depends(generate_session)):
""" Returns a named theme """
return db.themes.get(session, theme_name)
return SiteTheme.get_by_name(db, theme_name)
@router.put("/themes/{theme_name}")
def update_theme(
theme_name: str, data: SiteTheme, session: Session = Depends(generate_session)
theme_name: str, data: SiteTheme, db: Session = Depends(generate_session)
):
""" Update a theme database entry """
db.themes.update(session, theme_name, data.dict())
data.update_document(db)
return SnackResponse.info(f"Theme Updated: {theme_name}")
# try:
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Update Theme")
# )
return SnackResponse.success("Theme Updated")
@router.delete("/themes/{theme_name}")
def delete_theme(theme_name: str, session: Session = Depends(generate_session)):
def delete_theme(theme_name: str, db: Session = Depends(generate_session)):
""" Deletes theme from the database """
db.themes.delete(session, theme_name)
SiteTheme.delete_theme(db, theme_name)
# try:
# SiteTheme.delete_theme(theme_name)
# except:
# raise HTTPException(
# status_code=400, detail=SnackResponse.error("Unable to Delete Theme")
# )
return SnackResponse.error(f"Theme Deleted: {theme_name}")
return SnackResponse.success("Theme Deleted")

View file

@ -0,0 +1,33 @@
from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordRequestForm
# from fastapi_login import LoginManager
# from fastapi_login.exceptions import InvalidCredentialsException
router = APIRouter()
# SECRET = "876cfb59db03d9c17cefec967b00255d3f7d93a823e5dc2a"
# manager = LoginManager(SECRET, tokenUrl="/api/auth/token")
# fake_db = {"johndoe@e.mail": {"password": "hunter2"}}
# @manager.user_loader
# def load_user(email: str): # could also be an asynchronous function
# user = fake_db.get(email)
# return user
# @router.post("/api/auth/token", tags=["User Gen"])
# def login(data: OAuth2PasswordRequestForm = Depends()):
# email = data.username
# password = data.password
# user = load_user(email) # we are using the same function to retrieve the user
# if not user:
# raise InvalidCredentialsException # you can also use your own HTTPException
# elif password != user["password"]:
# raise InvalidCredentialsException
# access_token = manager.create_access_token(data=dict(sub=email))
# return {"access_token": access_token, "token_type": "bearer"}

View file

@ -4,11 +4,11 @@ from datetime import datetime
from pathlib import Path
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
from db.database import db
from db.db_setup import create_session
from jinja2 import Template
from services.meal_services import MealPlan
from services.recipe_services import Recipe
from services.settings_services import SiteSettings, SiteTheme
from utils.logger import logger
@ -88,18 +88,20 @@ class ExportDatabase:
shutil.copy(file, self.img_dir.joinpath(file.name))
def export_settings(self):
all_settings = db.settings.get(self.session, "main")
all_settings = SiteSettings.get_site_settings(self.session)
out_file = self.settings_dir.joinpath("settings.json")
ExportDatabase._write_json_file(all_settings, out_file)
ExportDatabase._write_json_file(all_settings.dict(), out_file)
def export_themes(self):
all_themes = db.themes.get_all(self.session)
all_themes = SiteTheme.get_all(self.session)
if all_themes:
all_themes = [x.dict() for x in all_themes]
out_file = self.themes_dir.joinpath("themes.json")
ExportDatabase._write_json_file(all_themes, out_file)
def export_meals(self):
#! Problem Parseing Datetime Objects... May come back to this
def export_meals(
self,
): #! Problem Parseing Datetime Objects... May come back to this
meal_plans = MealPlan.get_all(self.session)
if meal_plans:
meal_plans = [x.dict() for x in meal_plans]
@ -108,7 +110,7 @@ class ExportDatabase:
ExportDatabase._write_json_file(meal_plans, out_file)
@staticmethod
def _write_json_file(data: dict, out_file: Path):
def _write_json_file(data, out_file: Path):
json_data = json.dumps(data, indent=4, default=str)
with open(out_file, "w") as f:

View file

@ -1,15 +1,12 @@
import json
import shutil
import zipfile
from logging import error
from pathlib import Path
from typing import List
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR
from db.database import db
from models.theme_models import SiteTheme
from services.recipe_services import Recipe
from services.settings_services import SiteSettings
from services.settings_services import SiteSettings, SiteTheme
from sqlalchemy.orm.session import Session
from utils.logger import logger
@ -57,7 +54,6 @@ class ImportDatabase:
raise Exception("Import file does not exist")
def run(self):
report = {}
if self.imp_recipes:
report = self.import_recipes()
if self.imp_settings:
@ -132,13 +128,11 @@ class ImportDatabase:
themes_file = self.import_dir.joinpath("themes", "themes.json")
with open(themes_file, "r") as f:
themes: list[dict] = json.loads(f.read())
themes: list = json.loads(f.read())
for theme in themes:
if theme.get("name") == "default":
continue
new_theme = SiteTheme(**theme)
try:
db.themes.create(self.session, new_theme.dict())
new_theme.save_to_db(self.session)
except:
logger.info(f"Unable Import Theme {new_theme.name}")
@ -148,7 +142,9 @@ class ImportDatabase:
with open(settings_file, "r") as f:
settings: dict = json.loads(f.read())
db.settings.update(self.session, settings.get("name"), settings)
settings = SiteSettings(**settings)
settings.update(self.session)
def clean_up(self):
shutil.rmtree(TEMP_DIR)

View file

@ -8,6 +8,19 @@ from sqlalchemy.orm.session import Session
from services.recipe_services import Recipe
CWD = Path(__file__).parent
THIS_WEEK = CWD.parent.joinpath("data", "meal_plan", "this_week.json")
NEXT_WEEK = CWD.parent.joinpath("data", "meal_plan", "next_week.json")
WEEKDAYS = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
]
class Meal(BaseModel):
slug: Optional[str]
@ -68,7 +81,7 @@ class MealPlan(BaseModel):
self.meals = meals
def save_to_db(self, session: Session):
db.meals.create(session, self.dict())
db.meals.save_new(session, self.dict())
@staticmethod
def get_all(session: Session) -> List:

View file

@ -1,4 +1,5 @@
import datetime
import json
from pathlib import Path
from typing import Any, List, Optional
@ -97,7 +98,13 @@ class Recipe(BaseModel):
except:
recipe_dict["image"] = "no image"
recipe_doc = db.recipes.create(session, recipe_dict)
# try:
# total_time = recipe_dict.get("totalTime")
# recipe_dict["totalTime"] = str(total_time)
# except:
# pass
recipe_doc = db.recipes.save_new(session, recipe_dict)
recipe = Recipe(**recipe_doc)
return recipe.slug
@ -115,7 +122,7 @@ class Recipe(BaseModel):
return updated_slug.get("slug")
@staticmethod
def update_image(session: Session, slug: str, extension: str = None) -> str:
def update_image(slug: str, extension: str) -> str:
"""A helper function to pass the new image name and extension
into the database.
@ -123,8 +130,11 @@ class Recipe(BaseModel):
slug (str): The current recipe slug
extension (str): the file extension of the new image
"""
return db.recipes.update_image(session, slug, extension)
return db.recipes.update_image(slug, extension)
@staticmethod
def get_all(session: Session):
return db.recipes.get_all(session)

View file

@ -3,9 +3,8 @@ from db.db_setup import create_session
from services.backups.exports import auto_backup_job
from services.scheduler.global_scheduler import scheduler
from services.scheduler.scheduler_utils import Cron, cron_parser
from services.settings_services import SiteSettings
from utils.logger import logger
from models.settings_models import SiteSettings
from db.database import db
from utils.post_webhooks import post_webhooks
@ -16,8 +15,7 @@ def update_webhook_schedule():
poll the database for changes and reschedule the webhook time
"""
session = create_session()
settings = db.settings.get(session, "main")
settings = SiteSettings(**settings)
settings = SiteSettings.get_site_settings(session=session)
time = cron_parser(settings.webhooks.webhookTime)
job = JOB_STORE.get("webhooks")

View file

@ -14,7 +14,7 @@ from w3lib.html import get_base_url
from services.image_services import scrape_image
from services.recipe_services import Recipe
LAST_JSON = DEBUG_DIR.joinpath("last_recipe.json")
TEMP_FILE = DEBUG_DIR.joinpath("last_recipe.json")
def cleanhtml(raw_html):
@ -121,7 +121,6 @@ def process_recipe_data(new_recipe: dict, url=None) -> dict:
def extract_recipe_from_html(html: str, url: str) -> dict:
scraped_recipes: List[dict] = scrape_schema_recipe.loads(html, python_objects=True)
dump_last_json(scraped_recipes)
if not scraped_recipes:
scraped_recipes: List[dict] = scrape_schema_recipe.scrape_url(
@ -165,11 +164,7 @@ def og_fields(properties: List[Tuple[str, str]], field_name: str) -> List[str]:
def basic_recipe_from_opengraph(html: str, url: str) -> dict:
base_url = get_base_url(html, url)
data = extruct.extract(html, base_url=base_url)
try:
properties = data["opengraph"][0]["properties"]
except:
return
return {
"name": og_field(properties, "og:title"),
"description": og_field(properties, "og:description"),
@ -189,13 +184,6 @@ def basic_recipe_from_opengraph(html: str, url: str) -> dict:
}
def dump_last_json(recipe_data: dict):
with open(LAST_JSON, "w") as f:
f.write(json.dumps(recipe_data, indent=4, default=str))
return
def process_recipe_url(url: str) -> dict:
r = requests.get(url)
new_recipe = extract_recipe_from_html(r.text, url)
@ -206,6 +194,9 @@ def process_recipe_url(url: str) -> dict:
def create_from_url(url: str) -> Recipe:
recipe_data = process_recipe_url(url)
with open(TEMP_FILE, "w") as f:
f.write(json.dumps(recipe_data, indent=4, default=str))
recipe = Recipe(**recipe_data)
return recipe

View file

@ -1,16 +1,149 @@
from typing import List, Optional
from db.database import db
from db.db_setup import create_session, sql_exists
from models.settings_models import SiteSettings, Webhooks
from pydantic import BaseModel
from sqlalchemy.orm.session import Session
from utils.logger import logger
def default_settings_init(session: Session = None):
if session == None:
session = create_session()
class Webhooks(BaseModel):
webhookTime: str = "00:00"
webhookURLs: Optional[List[str]] = []
enabled: bool = False
class SiteSettings(BaseModel):
name: str = "main"
webhooks: Webhooks
class Config:
schema_extra = {
"example": {
"name": "main",
"webhooks": {
"webhookTime": "00:00",
"webhookURLs": ["https://mywebhookurl.com/webhook"],
"enable": False,
},
}
}
@staticmethod
def get_all(session: Session):
db.settings.get_all(session)
@classmethod
def get_site_settings(cls, session: Session):
try:
document = db.settings.get(session=session, match_value="main")
except:
webhooks = Webhooks()
default_entry = SiteSettings(name="main", webhooks=webhooks)
document = db.settings.create(session, default_entry.dict(), webhooks.dict())
except:
pass
document = db.settings.save_new(
session, default_entry.dict(), webhooks.dict()
)
return cls(**document)
def update(self, session: Session):
db.settings.update(session, "main", new_data=self.dict())
class Colors(BaseModel):
primary: str
accent: str
secondary: str
success: str
info: str
warning: str
error: str
class SiteTheme(BaseModel):
name: str
colors: Colors
class Config:
schema_extra = {
"example": {
"name": "default",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
},
}
}
@classmethod
def get_by_name(cls, session: Session, theme_name):
db_entry = db.themes.get(session, theme_name)
name = db_entry.get("name")
colors = Colors(**db_entry.get("colors"))
return cls(name=name, colors=colors)
@staticmethod
def get_all(session: Session):
all_themes = db.themes.get_all(session)
for index, theme in enumerate(all_themes):
name = theme.get("name")
colors = Colors(**theme.get("colors"))
all_themes[index] = SiteTheme(name=name, colors=colors)
return all_themes
def save_to_db(self, session: Session):
db.themes.save_new(session, self.dict())
def update_document(self, session: Session):
db.themes.update(session, self.name, self.dict())
@staticmethod
def delete_theme(session: Session, theme_name: str) -> str:
""" Removes the theme by name """
db.themes.delete(session, theme_name)
def default_theme_init():
default_colors = {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
}
session = create_session()
try:
SiteTheme.get_by_name(session, "default")
logger.info("Default theme exists... skipping generation")
except:
logger.info("Generating Default Theme")
colors = Colors(**default_colors)
default_theme = SiteTheme(name="default", colors=colors)
default_theme.save_to_db(session)
def default_settings_init():
session = create_session()
try:
document = db.settings.get(session, "main")
except:
webhooks = Webhooks()
default_entry = SiteSettings(name="main", webhooks=webhooks)
document = db.settings.save_new(session, default_entry.dict(), webhooks.dict())
session.close()
if not sql_exists:
default_settings_init()
default_theme_init()

View file

@ -1,28 +0,0 @@
from db.database import db
from db.db_setup import create_session, sql_exists
from utils.logger import logger
def default_theme_init():
default_theme = {
"name": "default",
"colors": {
"primary": "#E58325",
"accent": "#00457A",
"secondary": "#973542",
"success": "#5AB1BB",
"info": "#4990BA",
"warning": "#FF4081",
"error": "#EF5350",
},
}
session = create_session()
try:
db.themes.create(session, default_theme)
logger.info("Generating default theme...")
except:
logger.info("Default Theme Exists.. skipping generation")
if not sql_exists:
default_theme_init()

View file

@ -5,8 +5,6 @@ from app_config import SQLITE_DIR
from db.db_setup import generate_session, sql_global_init
from fastapi.testclient import TestClient
from pytest import fixture
from services.settings_services import default_settings_init
from services.theme_services import default_theme_init
from tests.test_config import TEST_DATA
@ -20,13 +18,13 @@ TestSessionLocal = sql_global_init(SQLITE_FILE, check_thread=False)
def override_get_db():
try:
db = TestSessionLocal()
default_theme_init()
default_settings_init()
yield db
finally:
db.close()
@fixture(scope="session")
def api_client():

View file

View file

@ -0,0 +1,99 @@
import json
import re
from pathlib import Path
import pytest
from services.scrape_services import (
extract_recipe_from_html,
normalize_data,
normalize_instructions,
)
CWD = Path(__file__).parent
RAW_RECIPE_DIR = CWD.parent.joinpath("data", "recipes-raw")
RAW_HTML_DIR = CWD.parent.joinpath("data", "html-raw")
# https://github.com/django/django/blob/stable/1.3.x/django/core/validators.py#L45
url_validation_regex = re.compile(
r"^(?:http|ftp)s?://" # http:// or https://
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain...
r"localhost|" # localhost...
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
r"(?::\d+)?" # optional port
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
@pytest.mark.parametrize(
"json_file,num_steps",
[
("best-homemade-salsa-recipe.json", 2),
(
"blue-cheese-stuffed-turkey-meatballs-with-raspberry-balsamic-glaze-2.json",
3,
),
("bon_appetit.json", 8),
("chunky-apple-cake.json", 4),
("dairy-free-impossible-pumpkin-pie.json", 7),
("how-to-make-instant-pot-spaghetti.json", 8),
("instant-pot-chicken-and-potatoes.json", 4),
("instant-pot-kerala-vegetable-stew.json", 13),
("jalapeno-popper-dip.json", 4),
("microwave_sweet_potatoes_04783.json", 4),
("moroccan-skirt-steak-with-roasted-pepper-couscous.json", 4),
("Pizza-Knoblauch-Champignon-Paprika-vegan.html.json", 3),
],
)
def test_normalize_data(json_file, num_steps):
recipe_data = normalize_data(json.load(open(RAW_RECIPE_DIR.joinpath(json_file))))
assert len(recipe_data["recipeInstructions"]) == num_steps
@pytest.mark.parametrize(
"instructions",
[
"A\n\nB\n\nC\n\n",
"A\nB\nC\n",
"A\r\n\r\nB\r\n\r\nC\r\n\r\n",
"A\r\nB\r\nC\r\n",
["A", "B", "C"],
[{"@type": "HowToStep", "text": x} for x in ["A", "B", "C"]],
],
)
def test_normalize_instructions(instructions):
assert normalize_instructions(instructions) == [
{"text": "A"},
{"text": "B"},
{"text": "C"},
]
def test_html_no_recipe_data():
path = RAW_HTML_DIR.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
recipe_data = extract_recipe_from_html(open(path).read(), url)
assert len(recipe_data["name"]) > 10
assert len(recipe_data["slug"]) > 10
assert recipe_data["orgURL"] == url
assert len(recipe_data["description"]) > 100
assert url_validation_regex.match(recipe_data["image"])
assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
assert recipe_data["recipeInstructions"] == [
{"text": "Could not detect instructions"}
]
def test_html_with_recipe_data():
path = RAW_HTML_DIR.joinpath("healthy_pasta_bake_60759.html")
url = "https://www.bbc.co.uk/food/recipes/healthy_pasta_bake_60759"
recipe_data = extract_recipe_from_html(open(path).read(), url)
assert len(recipe_data["name"]) > 10
assert len(recipe_data["slug"]) > 10
assert recipe_data["orgURL"] == url
assert len(recipe_data["description"]) > 100
assert url_validation_regex.match(recipe_data["image"])
assert len(recipe_data["recipeIngredient"]) == 13
assert len(recipe_data["recipeInstructions"]) == 4

View file

@ -32,7 +32,6 @@ def default_theme(api_client):
"error": "#EF5350",
},
}
api_client.post(THEMES_CREATE, json=default_theme)
return default_theme

View file

@ -65,20 +65,20 @@ def test_normalize_instructions(instructions):
]
# def test_html_no_recipe_data(): #! Unsure why it's failing, code didn't change?
# path = TEST_RAW_HTML.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
# url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
# recipe_data = extract_recipe_from_html(open(path).read(), url)
def test_html_no_recipe_data():
path = TEST_RAW_HTML.joinpath("carottes-rapps-with-rice-and-sunflower-seeds.html")
url = "https://www.feedtheswimmers.com/blog/2019/6/5/carottes-rapps-with-rice-and-sunflower-seeds"
recipe_data = extract_recipe_from_html(open(path).read(), url)
# assert len(recipe_data["name"]) > 10
# assert len(recipe_data["slug"]) > 10
# assert recipe_data["orgURL"] == url
# assert len(recipe_data["description"]) > 100
# assert url_validation_regex.match(recipe_data["image"])
# assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
# assert recipe_data["recipeInstructions"] == [
# {"text": "Could not detect instructions"}
# ]
assert len(recipe_data["name"]) > 10
assert len(recipe_data["slug"]) > 10
assert recipe_data["orgURL"] == url
assert len(recipe_data["description"]) > 100
assert url_validation_regex.match(recipe_data["image"])
assert recipe_data["recipeIngredient"] == ["Could not detect ingredients"]
assert recipe_data["recipeInstructions"] == [
{"text": "Could not detect instructions"}
]
def test_html_with_recipe_data():

View file

@ -1,17 +1,15 @@
import json
import requests
from db.database import db
from db.db_setup import create_session
from models.settings_models import SiteSettings
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 = db.get(session, "main")
all_settings = SiteSettings(**all_settings)
all_settings = SiteSettings.get_site_settings(session)
if all_settings.webhooks.enabled:
todays_meal = Recipe.get_by_slug(MealPlan.today()).dict()

View file

@ -9,6 +9,18 @@ class SnackResponse:
return snackbar
@staticmethod
def primary(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "primary", additional_data)
@staticmethod
def accent(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "accent", additional_data)
@staticmethod
def secondary(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "secondary", additional_data)
@staticmethod
def success(message: str, additional_data: dict = None) -> dict:
return SnackResponse._create_response(message, "success", additional_data)