feature/new-feature-cleanup (#389)
* add json editor to theme editor * add toolbars tools to recipe sections * fix recipe yield * add updated_date to recipe schema * update time cards * fix mobile buttons * fix asset URL * fix PG errors CRUD * remove -d from docker-pro * fix theme tests * remvoe old typing * abstract count function Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
c1370afb16
commit
1b0de02b71
34 changed files with 263 additions and 132 deletions
|
@ -1,7 +1,7 @@
|
|||
const baseURL = "/api/";
|
||||
import axios from "axios";
|
||||
import { store } from "../store";
|
||||
import utils from "@/utils";
|
||||
import { utils } from "@/utils";
|
||||
|
||||
axios.defaults.headers.common["Authorization"] = `Bearer ${store.getters.getToken}`;
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ const recipeURLs = {
|
|||
recipe: slug => prefix + slug,
|
||||
update: slug => prefix + slug,
|
||||
delete: slug => prefix + slug,
|
||||
createAsset: slug => `${prefix}media/${slug}/assets`,
|
||||
createAsset: slug => `${prefix}${slug}/assets`,
|
||||
recipeImage: slug => `${prefix}${slug}/image`,
|
||||
updateImage: slug => `${prefix}${slug}/image`,
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import utils from "@/utils";
|
||||
import { utils } from "@/utils";
|
||||
import MealPlanCard from "./MealPlanCard";
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
const CREATE_EVENT = "created";
|
||||
import DatePicker from "@/components/FormHelpers/DatePicker";
|
||||
import { api } from "@/api";
|
||||
import utils from "@/utils";
|
||||
import { utils } from "@/utils";
|
||||
import MealPlanCard from "./MealPlanCard";
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<v-list-item-title class=" mb-1">{{ name }} </v-list-item-title>
|
||||
<v-list-item-subtitle> {{ description }} </v-list-item-subtitle>
|
||||
<div class="d-flex justify-center align-center">
|
||||
<RecipeChips :items="tags" :title="false" :limit="1" :small="true" :isCategory="false" />
|
||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="1" :small="true" :isCategory="false" />
|
||||
<v-rating
|
||||
color="secondary"
|
||||
class="ml-auto"
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
import BulkAdd from "@/components/Recipe/Parts/Helpers/BulkAdd";
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import draggable from "vuedraggable";
|
||||
import utils from "@/utils";
|
||||
import { utils } from "@/utils";
|
||||
export default {
|
||||
components: {
|
||||
BulkAdd,
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
|
||||
<script>
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import utils from "@/utils";
|
||||
import { utils } from "@/utils";
|
||||
export default {
|
||||
components: {
|
||||
VueMarkdown,
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
|
||||
<script>
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import utils from "@/utils";
|
||||
import { utils } from "@/utils";
|
||||
export default {
|
||||
components: {
|
||||
VueMarkdown,
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<v-card-actions>
|
||||
<Rating :value="rating" :name="name" :slug="slug" :small="true" />
|
||||
<v-spacer></v-spacer>
|
||||
<RecipeChips :items="tags" :title="false" :limit="2" :small="true" :isCategory="false" />
|
||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" :isCategory="false" />
|
||||
<ContextMenu :slug="slug" />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
|
|
@ -124,7 +124,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "@/utils";
|
||||
import { utils } from "@/utils";
|
||||
import { api } from "@/api";
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,29 +1,13 @@
|
|||
<template>
|
||||
<v-card
|
||||
color="accent"
|
||||
class="custom-transparent d-flex justify-start align-center text-center time-card-flex"
|
||||
tile
|
||||
v-if="showCards"
|
||||
>
|
||||
<v-card flat color="rgb(255, 0, 0, 0.0)">
|
||||
<v-icon large color="white" class="mx-2"> mdi-clock-outline </v-icon>
|
||||
</v-card>
|
||||
|
||||
<v-card
|
||||
v-for="(time, index) in allTimes"
|
||||
:key="index"
|
||||
class="d-flex justify-start align-center text-center time-card-flex"
|
||||
flat
|
||||
color="rgb(255, 0, 0, 0.0)"
|
||||
>
|
||||
<v-card-text class="caption white--text py-2">
|
||||
<div>
|
||||
<strong> {{ time.name }} </strong>
|
||||
</div>
|
||||
<div>{{ time.value }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-card>
|
||||
<div>
|
||||
<v-chip label color="accent custom-transparent" class="ma-1" v-for="(time, index) in allTimes" :key="index">
|
||||
<v-icon left>
|
||||
mdi-clock-outline
|
||||
</v-icon>
|
||||
{{ time.name }} |
|
||||
{{ time.value }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
:to="`/recipes/${urlParam}/${getSlug(category)}`"
|
||||
:key="category"
|
||||
>
|
||||
{{ category }}
|
||||
{{ truncateText(category) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -19,6 +19,9 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
truncate: {
|
||||
default: false,
|
||||
},
|
||||
items: {
|
||||
default: [],
|
||||
},
|
||||
|
@ -34,6 +37,7 @@ export default {
|
|||
small: {
|
||||
default: false,
|
||||
},
|
||||
maxWidth: {},
|
||||
},
|
||||
computed: {
|
||||
allCategories() {
|
||||
|
@ -58,6 +62,14 @@ export default {
|
|||
if (matches.length > 0) return matches[0].slug;
|
||||
}
|
||||
},
|
||||
truncateText(text, length = 20, clamp) {
|
||||
if (!this.truncate) return text;
|
||||
clamp = clamp || "...";
|
||||
var node = document.createElement("div");
|
||||
node.innerHTML = text;
|
||||
var content = node.textContent;
|
||||
return content.length > length ? content.slice(0, length) + clamp : content;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<v-row dense disabled>
|
||||
<v-col>
|
||||
<v-btn
|
||||
v-if="recipe.yields"
|
||||
v-if="recipe.recipeYield"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
|
@ -18,7 +18,7 @@
|
|||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ recipe.yields }}
|
||||
{{ recipe.recipeYield }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<Rating :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" />
|
||||
|
@ -88,7 +88,7 @@
|
|||
<script>
|
||||
import Nutrition from "@/components/Recipe/Parts/Nutrition";
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import utils from "@/utils";
|
||||
import { utils } from "@/utils";
|
||||
import RecipeChips from "./RecipeChips";
|
||||
import Rating from "@/components/Recipe/Parts/Rating";
|
||||
import Notes from "@/components/Recipe/Parts/Notes";
|
||||
|
|
|
@ -6,21 +6,28 @@
|
|||
</v-icon>
|
||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu offset-y v-if="$listeners.sortRecent || $listeners.sort">
|
||||
<v-btn text @click="navigateRandom">
|
||||
Random
|
||||
</v-btn>
|
||||
<v-menu offset-y v-if="$listeners.sort">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn-toggle group>
|
||||
<v-btn text v-bind="attrs" v-on="on">
|
||||
{{ $t("general.sort") }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<v-btn text v-bind="attrs" v-on="on">
|
||||
{{ $t("general.sort") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="$emit('sortRecent')">
|
||||
<v-list-item-title>{{ $t("general.recent") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="$emit('sort')">
|
||||
<v-list-item @click="sortRecipes(EVENTS.az)">
|
||||
<v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="sortRecipes(EVENTS.rating)">
|
||||
<v-list-item-title>{{ $t("general.rating") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="sortRecipes(EVENTS.updated)">
|
||||
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="sortRecipes(EVENTS.created)">
|
||||
<v-list-item-title>{{ $t("general.created") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
|
@ -76,6 +83,9 @@
|
|||
<script>
|
||||
import RecipeCard from "../Recipe/RecipeCard";
|
||||
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
||||
import { utils } from "@/utils";
|
||||
const SORT_EVENT = "sort";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RecipeCard,
|
||||
|
@ -106,6 +116,12 @@ export default {
|
|||
return {
|
||||
cardLimit: 30,
|
||||
loading: false,
|
||||
EVENTS: {
|
||||
az: "az",
|
||||
rating: "rating",
|
||||
created: "created",
|
||||
updated: "updated",
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -144,6 +160,31 @@ export default {
|
|||
await new Promise(r => setTimeout(r, 1000));
|
||||
this.loading = false;
|
||||
},
|
||||
navigateRandom() {
|
||||
const recipe = utils.recipe.randomRecipe(this.recipes);
|
||||
this.$router.push(`/recipe/${recipe.slug}`);
|
||||
},
|
||||
sortRecipes(sortType) {
|
||||
let sortTarget = [...this.recipes];
|
||||
switch (sortType) {
|
||||
case this.EVENTS.az:
|
||||
utils.recipe.sortAToZ(sortTarget);
|
||||
break;
|
||||
case this.EVENTS.rating:
|
||||
utils.recipe.sortByRating(sortTarget);
|
||||
break;
|
||||
case this.EVENTS.created:
|
||||
utils.recipe.sortByCreated(sortTarget);
|
||||
break;
|
||||
case this.EVENTS.updated:
|
||||
utils.recipe.sortByUpdated(sortTarget);
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown Event", sortType);
|
||||
return;
|
||||
}
|
||||
this.$emit(SORT_EVENT, sortTarget);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"create": "Create",
|
||||
"created": "Created",
|
||||
"current-parenthesis": "(Current)",
|
||||
"dashboard": "Dashboard",
|
||||
"delete": "Delete",
|
||||
|
@ -63,6 +64,7 @@
|
|||
"no": "No",
|
||||
"ok": "OK",
|
||||
"options": "Options:",
|
||||
"rating": "Rating",
|
||||
"random": "Random",
|
||||
"recent": "Recent",
|
||||
"recipes": "Recipes",
|
||||
|
@ -83,6 +85,7 @@
|
|||
"token": "Token",
|
||||
"tuesday": "Tuesday",
|
||||
"update": "Update",
|
||||
"updated": "Updated",
|
||||
"upload": "Upload",
|
||||
"url": "URL",
|
||||
"users": "Users",
|
||||
|
|
|
@ -92,26 +92,36 @@
|
|||
:label="$t('settings.theme.theme-name')"
|
||||
v-model="defaultData.name"
|
||||
:rules="[rules.required]"
|
||||
:append-outer-icon="jsonEditor ? 'mdi-form-select' : 'mdi-code-braces'"
|
||||
@click:append-outer="jsonEditor = !jsonEditor"
|
||||
></v-text-field>
|
||||
<v-row dense dflex wrap justify-content-center v-if="defaultData.colors">
|
||||
<v-row dense dflex wrap justify-content-center v-if="defaultData.colors && !jsonEditor">
|
||||
<v-col cols="12" sm="6" v-for="(_, key) in defaultData.colors" :key="key">
|
||||
<ColorPickerDialog :button-text="labels[key]" v-model="defaultData.colors[key]" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<VJsoneditor @error="logError()" v-else v-model="defaultData" height="250px" :options="jsonEditorOptions" />
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VJsoneditor from "v-jsoneditor";
|
||||
import { api } from "@/api";
|
||||
import ColorPickerDialog from "@/components/FormHelpers/ColorPickerDialog";
|
||||
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||
import StatCard from "@/components/UI/StatCard";
|
||||
export default {
|
||||
components: { StatCard, BaseDialog, ColorPickerDialog },
|
||||
components: { StatCard, BaseDialog, ColorPickerDialog, VJsoneditor },
|
||||
data() {
|
||||
return {
|
||||
jsonEditor: true,
|
||||
jsonEditorOptions: {
|
||||
mode: "code",
|
||||
search: false,
|
||||
mainMenuBar: false,
|
||||
},
|
||||
availableThemes: [],
|
||||
color: "accent",
|
||||
newTheme: false,
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import utils from "@/utils";
|
||||
import { utils } from "@/utils";
|
||||
import NewMeal from "@/components/MealPlan/MealPlanNew";
|
||||
import EditPlan from "@/components/MealPlan/MealPlanEditor";
|
||||
import ShoppingListDialog from "@/components/MealPlan/ShoppingListDialog";
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import utils from "@/utils";
|
||||
import { utils } from "@/utils";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<v-card v-else id="myRecipe">
|
||||
<v-img height="400" :src="getImage(recipeDetails.slug)" class="d-print-none" :key="imageKey">
|
||||
<RecipeTimeCard
|
||||
class="force-bottom"
|
||||
:class="isMobile ? undefined : 'force-bottom'"
|
||||
:prepTime="recipeDetails.prepTime"
|
||||
:totalTime="recipeDetails.totalTime"
|
||||
:performTime="recipeDetails.performTime"
|
||||
|
@ -106,6 +106,9 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
},
|
||||
currentRecipe() {
|
||||
return this.$route.params.recipe;
|
||||
},
|
||||
|
|
|
@ -5,9 +5,8 @@
|
|||
title-icon=""
|
||||
:sortable="true"
|
||||
:title="$t('page.all-recipes')"
|
||||
:recipes="allRecipes"
|
||||
@sort="sortAZ"
|
||||
@sort-recent="sortRecent"
|
||||
:recipes="shownRecipes"
|
||||
@sort="assignSorted"
|
||||
/>
|
||||
</v-container>
|
||||
</template>
|
||||
|
@ -22,6 +21,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
sortedResults: [],
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
|
@ -35,13 +35,17 @@ export default {
|
|||
allRecipes() {
|
||||
return this.$store.getters.getAllRecipes;
|
||||
},
|
||||
shownRecipes() {
|
||||
if (this.sortedResults.length > 0) {
|
||||
return this.sortedResults;
|
||||
} else {
|
||||
return this.allRecipes;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sortAZ() {
|
||||
this.allRecipes.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
},
|
||||
sortRecent() {
|
||||
this.allRecipes.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
|
||||
assignSorted(val) {
|
||||
this.sortedResults = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -47,31 +47,20 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="fuzzyRecipes">
|
||||
<v-col :sm="6" :md="6" :lg="4" :xl="3" v-for="item in fuzzyRecipes.slice(0, maxResults)" :key="item.name">
|
||||
<RecipeCard
|
||||
:name="item.item.name"
|
||||
:description="item.item.description"
|
||||
:slug="item.item.slug"
|
||||
:rating="item.item.rating"
|
||||
:image="item.item.image"
|
||||
:tags="item.item.tags"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<CardSection title-icon="mdi-mag" :recipes="showRecipes" :hardLimit="maxResults" @sort="assignFuzzy" />
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Fuse from "fuse.js";
|
||||
import RecipeCard from "@/components/Recipe/RecipeCard";
|
||||
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
|
||||
import CardSection from "@/components/UI/CardSection";
|
||||
import FilterSelector from "./FilterSelector.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RecipeCard,
|
||||
CardSection,
|
||||
CategoryTagSelector,
|
||||
FilterSelector,
|
||||
},
|
||||
|
@ -88,6 +77,7 @@ export default {
|
|||
exclude: false,
|
||||
matchAny: false,
|
||||
},
|
||||
sortedResults: [],
|
||||
includeCategories: [],
|
||||
includeTags: [],
|
||||
options: {
|
||||
|
@ -126,16 +116,31 @@ export default {
|
|||
},
|
||||
fuzzyRecipes() {
|
||||
if (this.searchString.trim() === "") {
|
||||
return this.filteredRecipes.map(x => ({ item: x }));
|
||||
return this.filteredRecipes;
|
||||
}
|
||||
const result = this.fuse.search(this.searchString.trim());
|
||||
return result;
|
||||
return result.map(x => x.item);
|
||||
},
|
||||
isSearching() {
|
||||
return this.searchString && this.searchString.length > 0;
|
||||
},
|
||||
showRecipes() {
|
||||
if (this.sortedResults.length > 0) {
|
||||
return this.sortedResults;
|
||||
} else {
|
||||
return this.fuzzyRecipes;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showRecipes(val) {
|
||||
console.log(val);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
assignFuzzy(val) {
|
||||
this.sortedResults = val;
|
||||
},
|
||||
check(filterBy, recipeList, matchAny, exclude) {
|
||||
let isMatch = true;
|
||||
if (filterBy.length === 0) return isMatch;
|
||||
|
|
|
@ -3,7 +3,7 @@ import ThisWeek from "@/pages/MealPlan/ThisWeek";
|
|||
import { api } from "@/api";
|
||||
|
||||
import i18n from "@/i18n.js";
|
||||
import utils from "@/utils";
|
||||
import { utils } from "@/utils";
|
||||
|
||||
export const mealRoutes = [
|
||||
{
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { vueApp } from "../main";
|
||||
import { recipe } from "@/utils/recipe";
|
||||
|
||||
// TODO: Migrate to Mixins
|
||||
const notifyHelpers = {
|
||||
|
@ -9,7 +10,8 @@ const notifyHelpers = {
|
|||
info: "notify-info-color",
|
||||
};
|
||||
|
||||
export default {
|
||||
export const utils = {
|
||||
recipe: recipe,
|
||||
getImageURL(image) {
|
||||
return `/api/recipes/${image}/image?image_type=small`;
|
||||
},
|
||||
|
|
31
frontend/src/utils/recipe.js
Normal file
31
frontend/src/utils/recipe.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
export const recipe = {
|
||||
/**
|
||||
* Sorts a list of recipes in place
|
||||
* @param {Array<Object>} list of recipes
|
||||
* @param {Boolean} inverse - Z or A First
|
||||
*/
|
||||
sortAToZ(list) {
|
||||
list.sort((a, b) => {
|
||||
var textA = a.name.toUpperCase();
|
||||
var textB = b.name.toUpperCase();
|
||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||
});
|
||||
},
|
||||
sortByCreated(list) {
|
||||
list.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
|
||||
},
|
||||
sortByUpdated(list) {
|
||||
list.sort((a, b) => (a.dateUpdated > b.dateUpdated ? -1 : 1));
|
||||
},
|
||||
sortByRating(list) {
|
||||
list.sort((a, b) => (a.rating > b.rating ? -1 : 1));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {Array<Object>} list
|
||||
* @returns String / Recipe Slug
|
||||
*/
|
||||
randomRecipe(list) {
|
||||
return list[Math.floor(Math.random() * list.length)];
|
||||
},
|
||||
};
|
2
makefile
2
makefile
|
@ -77,7 +77,7 @@ docker-dev: ## Build and Start Docker Development Stack
|
|||
docker-compose -f docker-compose.dev.yml -p dev-mealie up --build
|
||||
|
||||
docker-prod: ## Build and Start Docker Production Stack
|
||||
docker-compose -f docker-compose.yml -p mealie up --build -d
|
||||
docker-compose -f docker-compose.yml -p mealie up --build
|
||||
|
||||
code-gen: ## Run Code-Gen Scripts
|
||||
poetry run python dev/scripts/app_routes_gen.py
|
||||
|
|
|
@ -37,24 +37,14 @@ class _Recipes(BaseDocument):
|
|||
return f"{slug}.{extension}"
|
||||
|
||||
def count_uncategorized(self, session: Session, count=True, override_schema=None) -> int:
|
||||
eff_schema = override_schema or self.schema
|
||||
if count:
|
||||
return session.query(self.sql_model).filter(RecipeModel.recipe_category == None).count() # noqa: 711
|
||||
else:
|
||||
return [
|
||||
eff_schema.from_orm(x)
|
||||
for x in session.query(self.sql_model).filter(RecipeModel.tags == None).all() # noqa: 711
|
||||
]
|
||||
return self._countr_attribute(
|
||||
session, attribute_name=RecipeModel.recipe_category, attr_match=None, count=True, override_schema=None
|
||||
)
|
||||
|
||||
def count_untagged(self, session: Session, count=True, override_schema=None) -> int:
|
||||
eff_schema = override_schema or self.schema
|
||||
if count:
|
||||
return session.query(self.sql_model).filter(RecipeModel.tags == None).count() # noqa: 711
|
||||
else:
|
||||
return [
|
||||
eff_schema.from_orm(x)
|
||||
for x in session.query(self.sql_model).filter(RecipeModel.tags == None).all() # noqa: 711
|
||||
]
|
||||
return self._countr_attribute(
|
||||
session, attribute_name=RecipeModel.tags, attr_match=None, count=True, override_schema=None
|
||||
)
|
||||
|
||||
|
||||
class _Categories(BaseDocument):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import List
|
||||
from typing import Union
|
||||
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.models.model_base import SqlAlchemyBase
|
||||
|
@ -20,7 +20,7 @@ class BaseDocument:
|
|||
# TODO: Improve Get All Query Functionality
|
||||
def get_all(
|
||||
self, session: Session, limit: int = None, order_by: str = None, start=0, end=9999, override_schema=None
|
||||
) -> List[dict]:
|
||||
) -> list[dict]:
|
||||
eff_schema = override_schema or self.schema
|
||||
|
||||
if order_by:
|
||||
|
@ -33,13 +33,13 @@ class BaseDocument:
|
|||
|
||||
return [eff_schema.from_orm(x) for x in session.query(self.sql_model).offset(start).limit(limit).all()]
|
||||
|
||||
def get_all_limit_columns(self, session: Session, fields: List[str], limit: int = None) -> List[SqlAlchemyBase]:
|
||||
def get_all_limit_columns(self, session: Session, fields: list[str], limit: int = None) -> list[SqlAlchemyBase]:
|
||||
"""Queries the database for the selected model. Restricts return responses to the
|
||||
keys specified under "fields"
|
||||
|
||||
Args: \n
|
||||
session (Session): Database Session Object
|
||||
fields (List[str]): List of column names to query
|
||||
fields (list[str]): list of column names to query
|
||||
limit (int): A limit of values to return
|
||||
|
||||
Returns:
|
||||
|
@ -47,7 +47,7 @@ class BaseDocument:
|
|||
"""
|
||||
return session.query(self.sql_model).options(load_only(*fields)).limit(limit).all()
|
||||
|
||||
def get_all_primary_keys(self, session: Session) -> List[str]:
|
||||
def get_all_primary_keys(self, session: Session) -> list[str]:
|
||||
"""Queries the database of the selected model and returns a list
|
||||
of all primary_key values
|
||||
|
||||
|
@ -79,7 +79,7 @@ class BaseDocument:
|
|||
|
||||
def get(
|
||||
self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False
|
||||
) -> BaseModel or List[BaseModel]:
|
||||
) -> Union[BaseModel, list[BaseModel]]:
|
||||
"""Retrieves an entry from the database by matching a key/value pair. If no
|
||||
key is provided the class objects primary key will be used to match against.
|
||||
|
||||
|
@ -120,6 +120,8 @@ class BaseDocument:
|
|||
Returns:
|
||||
dict: A dictionary representation of the database entry
|
||||
"""
|
||||
document = document if isinstance(document, dict) else document.dict()
|
||||
|
||||
new_document = self.sql_model(session=session, **document)
|
||||
session.add(new_document)
|
||||
session.commit()
|
||||
|
@ -136,6 +138,7 @@ class BaseDocument:
|
|||
Returns:
|
||||
dict: Returns a dictionary representation of the database entry
|
||||
"""
|
||||
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
|
||||
|
||||
entry = self._query_one(session=session, match_value=match_value)
|
||||
entry.update(session=session, **new_data)
|
||||
|
@ -144,6 +147,8 @@ class BaseDocument:
|
|||
return self.schema.from_orm(entry)
|
||||
|
||||
def patch(self, session: Session, match_value: str, new_data: dict) -> BaseModel:
|
||||
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
|
||||
|
||||
entry = self._query_one(session=session, match_value=match_value)
|
||||
|
||||
if not entry:
|
||||
|
@ -168,8 +173,21 @@ class BaseDocument:
|
|||
session.commit()
|
||||
|
||||
def count_all(self, session: Session, match_key=None, match_value=None) -> int:
|
||||
|
||||
if None in [match_key, match_value]:
|
||||
return session.query(self.sql_model).count()
|
||||
else:
|
||||
return session.query(self.sql_model).filter_by(**{match_key: match_value}).count()
|
||||
|
||||
def _countr_attribute(
|
||||
self, session: Session, attribute_name: str, attr_match: str = None, count=True, override_schema=None
|
||||
) -> Union[int, BaseModel]:
|
||||
eff_schema = override_schema or self.schema
|
||||
# attr_filter = getattr(self.sql_model, attribute_name)
|
||||
|
||||
if count:
|
||||
return session.query(self.sql_model).filter(attribute_name == attr_match).count() # noqa: 711
|
||||
else:
|
||||
return [
|
||||
eff_schema.from_orm(x)
|
||||
for x in session.query(self.sql_model).filter(attribute_name == attr_match).all() # noqa: 711
|
||||
]
|
||||
|
|
|
@ -24,7 +24,23 @@ def init_db(db: Session = None) -> None:
|
|||
|
||||
|
||||
def default_theme_init(session: Session):
|
||||
db.themes.create(session, SiteTheme().dict())
|
||||
default_themes = [
|
||||
SiteTheme().dict(),
|
||||
{
|
||||
"name": "Dark",
|
||||
"colors": {
|
||||
"primary": "#424242",
|
||||
"accent": "#455A64",
|
||||
"secondary": "#00796B",
|
||||
"success": "#43A047",
|
||||
"info": "#1976D2",
|
||||
"warning": "#FF6F00",
|
||||
"error": "#EF5350",
|
||||
},
|
||||
},
|
||||
]
|
||||
for theme in default_themes:
|
||||
db.themes.create(session, theme)
|
||||
|
||||
|
||||
def default_settings_init(session: Session):
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from mealie.core.config import settings
|
||||
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models.recipe.category import Category, group2categories
|
||||
from sqlalchemy.orm.session import Session
|
||||
from mealie.core.config import settings
|
||||
|
||||
|
||||
class WebhookURLModel(SqlAlchemyBase):
|
||||
|
@ -50,10 +50,6 @@ class Group(SqlAlchemyBase, BaseMixins):
|
|||
self.webhook_time = webhook_time
|
||||
self.webhook_urls = [WebhookURLModel(url=x) for x in webhook_urls]
|
||||
|
||||
def update(self, session: Session, *args, **kwargs):
|
||||
|
||||
self.__init__(session=session, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_ref(session: Session, name: str):
|
||||
item = session.query(Group).filter(Group.name == name).one_or_none()
|
||||
|
|
|
@ -27,12 +27,16 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||
name = sa.Column(sa.String, nullable=False)
|
||||
description = sa.Column(sa.String)
|
||||
image = sa.Column(sa.String)
|
||||
|
||||
# Time Related Properties
|
||||
total_time = sa.Column(sa.String)
|
||||
prep_time = sa.Column(sa.String)
|
||||
perform_time = sa.Column(sa.String)
|
||||
cookTime = sa.Column(sa.String)
|
||||
cook_time = sa.Column(sa.String)
|
||||
|
||||
recipe_yield = sa.Column(sa.String)
|
||||
recipeCuisine = sa.Column(sa.String)
|
||||
|
||||
tools: list[Tool] = orm.relationship("Tool", cascade="all, delete-orphan")
|
||||
assets: list[RecipeAsset] = orm.relationship("RecipeAsset", cascade="all, delete-orphan")
|
||||
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
|
||||
|
@ -55,12 +59,15 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||
slug = sa.Column(sa.String, index=True, unique=True)
|
||||
settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan")
|
||||
tags: list[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes")
|
||||
date_added = sa.Column(sa.Date, default=date.today)
|
||||
notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan")
|
||||
rating = sa.Column(sa.Integer)
|
||||
org_url = sa.Column(sa.String)
|
||||
extras: list[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
|
||||
|
||||
# Time Stamp Properties
|
||||
date_added = sa.Column(sa.Date, default=date.today)
|
||||
date_updated = sa.Column(sa.DateTime)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert name != ""
|
||||
|
@ -78,6 +85,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||
recipeCuisine: str = None,
|
||||
total_time: str = None,
|
||||
prep_time: str = None,
|
||||
cook_time: str = None,
|
||||
nutrition: dict = None,
|
||||
tools: list[str] = None,
|
||||
perform_time: str = None,
|
||||
|
@ -85,6 +93,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||
recipe_category: list[str] = None,
|
||||
tags: list[str] = None,
|
||||
date_added: datetime.date = None,
|
||||
date_updated: datetime.datetime = None,
|
||||
notes: list[dict] = None,
|
||||
rating: int = None,
|
||||
org_url: str = None,
|
||||
|
@ -113,6 +122,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||
self.total_time = total_time
|
||||
self.prep_time = prep_time
|
||||
self.perform_time = perform_time
|
||||
self.cook_time = cook_time
|
||||
|
||||
self.recipe_category = [Category.create_if_not_exist(session=session, name=cat) for cat in recipe_category]
|
||||
|
||||
|
@ -120,12 +130,15 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||
self.settings = RecipeSettings(**settings) if settings else RecipeSettings()
|
||||
self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags]
|
||||
self.slug = slug
|
||||
self.date_added = date_added
|
||||
self.notes = [Note(**note) for note in notes]
|
||||
self.rating = rating
|
||||
self.org_url = org_url
|
||||
self.extras = [ApiExtras(key=key, value=value) for key, value in extras.items()]
|
||||
|
||||
# Time Stampes
|
||||
self.date_added = date_added
|
||||
self.date_updated = datetime.datetime.now()
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
|
||||
self.__init__(*args, **kwargs)
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from sqlalchemy.sql.sqltypes import Integer
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||
|
||||
|
||||
class SiteThemeModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "site_theme"
|
||||
id = sa.Column(Integer, primary_key=True, unique=True)
|
||||
name = sa.Column(sa.String, nullable=False, unique=True)
|
||||
colors = orm.relationship("ThemeColorsModel", uselist=False, cascade="all, delete")
|
||||
id = Column(Integer, primary_key=True, unique=True)
|
||||
name = Column(String, nullable=False, unique=True)
|
||||
colors = orm.relationship("ThemeColorsModel", uselist=False, single_parent=True, cascade="all, delete-orphan")
|
||||
|
||||
def __init__(self, name: str, colors: dict, *arg, **kwargs) -> None:
|
||||
self.name = name
|
||||
|
@ -17,12 +16,12 @@ class SiteThemeModel(SqlAlchemyBase, BaseMixins):
|
|||
|
||||
class ThemeColorsModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "theme_colors"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("site_theme.name"))
|
||||
primary = sa.Column(sa.String)
|
||||
accent = sa.Column(sa.String)
|
||||
secondary = sa.Column(sa.String)
|
||||
success = sa.Column(sa.String)
|
||||
info = sa.Column(sa.String)
|
||||
warning = sa.Column(sa.String)
|
||||
error = sa.Column(sa.String)
|
||||
id = Column(Integer, primary_key=True)
|
||||
parent_id = Column(Integer, ForeignKey("site_theme.id"))
|
||||
primary = Column(String)
|
||||
accent = Column(String)
|
||||
secondary = Column(String)
|
||||
success = Column(String)
|
||||
info = Column(String)
|
||||
warning = Column(String)
|
||||
error = Column(String)
|
||||
|
|
|
@ -69,6 +69,9 @@ class RecipeSummary(CamelModel):
|
|||
tags: Optional[list[str]] = []
|
||||
rating: Optional[int]
|
||||
|
||||
date_added: Optional[datetime.date]
|
||||
date_updated: Optional[datetime.datetime]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
@ -95,7 +98,6 @@ class Recipe(RecipeSummary):
|
|||
# Mealie Specific
|
||||
settings: Optional[RecipeSettings]
|
||||
assets: Optional[list[RecipeAsset]] = []
|
||||
date_added: Optional[datetime.date]
|
||||
notes: Optional[list[RecipeNote]] = []
|
||||
org_url: Optional[str] = Field(None, alias="orgURL")
|
||||
extras: Optional[dict] = {}
|
||||
|
|
|
@ -8,8 +8,8 @@ class Colors(BaseModel):
|
|||
accent: str = "#00457A"
|
||||
secondary: str = "#973542"
|
||||
success: str = "#43A047"
|
||||
info: str = "#4990BA"
|
||||
warning: str = "#FF4081"
|
||||
info: str = "#1976D2"
|
||||
warning: str = "#FF6F00"
|
||||
error: str = "#EF5350"
|
||||
|
||||
class Config:
|
||||
|
|
|
@ -20,7 +20,7 @@ def default_theme():
|
|||
@pytest.fixture(scope="session")
|
||||
def new_theme():
|
||||
return {
|
||||
"id": 2,
|
||||
"id": 3,
|
||||
"name": "myTestTheme",
|
||||
"colors": {
|
||||
"primary": "#E58325",
|
||||
|
@ -73,7 +73,9 @@ def test_create_theme(api_client: TestClient, api_routes: AppRoutes, new_theme,
|
|||
def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
|
||||
response = api_client.get(api_routes.themes)
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.content) == [default_theme, new_theme]
|
||||
response_dict = json.loads(response.content)
|
||||
assert default_theme in response_dict
|
||||
assert new_theme in response_dict
|
||||
|
||||
|
||||
def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
|
||||
|
|
Loading…
Reference in a new issue