mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	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:
		| @@ -1,7 +1,7 @@ | |||||||
| const baseURL = "/api/"; | const baseURL = "/api/"; | ||||||
| import axios from "axios"; | import axios from "axios"; | ||||||
| import { store } from "../store"; | import { store } from "../store"; | ||||||
| import utils from "@/utils"; | import { utils } from "@/utils"; | ||||||
|  |  | ||||||
| axios.defaults.headers.common["Authorization"] = `Bearer ${store.getters.getToken}`; | axios.defaults.headers.common["Authorization"] = `Bearer ${store.getters.getToken}`; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ const recipeURLs = { | |||||||
|   recipe: slug => prefix + slug, |   recipe: slug => prefix + slug, | ||||||
|   update: slug => prefix + slug, |   update: slug => prefix + slug, | ||||||
|   delete: slug => prefix + slug, |   delete: slug => prefix + slug, | ||||||
|   createAsset: slug => `${prefix}media/${slug}/assets`, |   createAsset: slug => `${prefix}${slug}/assets`, | ||||||
|   recipeImage: slug => `${prefix}${slug}/image`, |   recipeImage: slug => `${prefix}${slug}/image`, | ||||||
|   updateImage: slug => `${prefix}${slug}/image`, |   updateImage: slug => `${prefix}${slug}/image`, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| import utils from "@/utils"; | import { utils } from "@/utils"; | ||||||
| import MealPlanCard from "./MealPlanCard"; | import MealPlanCard from "./MealPlanCard"; | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|   | |||||||
| @@ -82,7 +82,7 @@ | |||||||
| const CREATE_EVENT = "created"; | const CREATE_EVENT = "created"; | ||||||
| import DatePicker from "@/components/FormHelpers/DatePicker"; | import DatePicker from "@/components/FormHelpers/DatePicker"; | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| import utils from "@/utils"; | import { utils } from "@/utils"; | ||||||
| import MealPlanCard from "./MealPlanCard"; | import MealPlanCard from "./MealPlanCard"; | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
|         <v-list-item-title class=" mb-1">{{ name }} </v-list-item-title> |         <v-list-item-title class=" mb-1">{{ name }} </v-list-item-title> | ||||||
|         <v-list-item-subtitle> {{ description }} </v-list-item-subtitle> |         <v-list-item-subtitle> {{ description }} </v-list-item-subtitle> | ||||||
|         <div class="d-flex justify-center align-center"> |         <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 |           <v-rating | ||||||
|             color="secondary" |             color="secondary" | ||||||
|             class="ml-auto" |             class="ml-auto" | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ | |||||||
| import BulkAdd from "@/components/Recipe/Parts/Helpers/BulkAdd"; | import BulkAdd from "@/components/Recipe/Parts/Helpers/BulkAdd"; | ||||||
| import VueMarkdown from "@adapttive/vue-markdown"; | import VueMarkdown from "@adapttive/vue-markdown"; | ||||||
| import draggable from "vuedraggable"; | import draggable from "vuedraggable"; | ||||||
| import utils from "@/utils"; | import { utils } from "@/utils"; | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     BulkAdd, |     BulkAdd, | ||||||
|   | |||||||
| @@ -66,7 +66,7 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import VueMarkdown from "@adapttive/vue-markdown"; | import VueMarkdown from "@adapttive/vue-markdown"; | ||||||
| import utils from "@/utils"; | import { utils } from "@/utils"; | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     VueMarkdown, |     VueMarkdown, | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import VueMarkdown from "@adapttive/vue-markdown"; | import VueMarkdown from "@adapttive/vue-markdown"; | ||||||
| import utils from "@/utils"; | import { utils } from "@/utils"; | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     VueMarkdown, |     VueMarkdown, | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ | |||||||
|       <v-card-actions> |       <v-card-actions> | ||||||
|         <Rating :value="rating" :name="name" :slug="slug" :small="true" /> |         <Rating :value="rating" :name="name" :slug="slug" :small="true" /> | ||||||
|         <v-spacer></v-spacer> |         <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" /> |         <ContextMenu :slug="slug" /> | ||||||
|       </v-card-actions> |       </v-card-actions> | ||||||
|     </v-card> |     </v-card> | ||||||
|   | |||||||
| @@ -124,7 +124,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import utils from "@/utils"; | import { utils } from "@/utils"; | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   | |||||||
| @@ -1,29 +1,13 @@ | |||||||
| <template> | <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> |   <div> | ||||||
|           <strong> {{ time.name }} </strong> |     <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> |   </div> | ||||||
|         <div>{{ time.value }}</div> |  | ||||||
|       </v-card-text> |  | ||||||
|     </v-card> |  | ||||||
|   </v-card> |  | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
|       :to="`/recipes/${urlParam}/${getSlug(category)}`" |       :to="`/recipes/${urlParam}/${getSlug(category)}`" | ||||||
|       :key="category" |       :key="category" | ||||||
|     > |     > | ||||||
|       {{ category }} |       {{ truncateText(category) }} | ||||||
|     </v-chip> |     </v-chip> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| @@ -19,6 +19,9 @@ | |||||||
| <script> | <script> | ||||||
| export default { | export default { | ||||||
|   props: { |   props: { | ||||||
|  |     truncate: { | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|     items: { |     items: { | ||||||
|       default: [], |       default: [], | ||||||
|     }, |     }, | ||||||
| @@ -34,6 +37,7 @@ export default { | |||||||
|     small: { |     small: { | ||||||
|       default: false, |       default: false, | ||||||
|     }, |     }, | ||||||
|  |     maxWidth: {}, | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     allCategories() { |     allCategories() { | ||||||
| @@ -58,6 +62,14 @@ export default { | |||||||
|         if (matches.length > 0) return matches[0].slug; |         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> | </script> | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
|       <v-row dense disabled> |       <v-row dense disabled> | ||||||
|         <v-col> |         <v-col> | ||||||
|           <v-btn |           <v-btn | ||||||
|             v-if="recipe.yields" |             v-if="recipe.recipeYield" | ||||||
|             dense |             dense | ||||||
|             small |             small | ||||||
|             :hover="false" |             :hover="false" | ||||||
| @@ -18,7 +18,7 @@ | |||||||
|             color="secondary darken-1" |             color="secondary darken-1" | ||||||
|             class="rounded-sm static" |             class="rounded-sm static" | ||||||
|           > |           > | ||||||
|             {{ recipe.yields }} |             {{ recipe.recipeYield }} | ||||||
|           </v-btn> |           </v-btn> | ||||||
|         </v-col> |         </v-col> | ||||||
|         <Rating :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" /> |         <Rating :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" /> | ||||||
| @@ -88,7 +88,7 @@ | |||||||
| <script> | <script> | ||||||
| import Nutrition from "@/components/Recipe/Parts/Nutrition"; | import Nutrition from "@/components/Recipe/Parts/Nutrition"; | ||||||
| import VueMarkdown from "@adapttive/vue-markdown"; | import VueMarkdown from "@adapttive/vue-markdown"; | ||||||
| import utils from "@/utils"; | import { utils } from "@/utils"; | ||||||
| import RecipeChips from "./RecipeChips"; | import RecipeChips from "./RecipeChips"; | ||||||
| import Rating from "@/components/Recipe/Parts/Rating"; | import Rating from "@/components/Recipe/Parts/Rating"; | ||||||
| import Notes from "@/components/Recipe/Parts/Notes"; | import Notes from "@/components/Recipe/Parts/Notes"; | ||||||
|   | |||||||
| @@ -6,21 +6,28 @@ | |||||||
|       </v-icon> |       </v-icon> | ||||||
|       <v-toolbar-title class="headline"> {{ title }} </v-toolbar-title> |       <v-toolbar-title class="headline"> {{ title }} </v-toolbar-title> | ||||||
|       <v-spacer></v-spacer> |       <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 }"> |         <template v-slot:activator="{ on, attrs }"> | ||||||
|           <v-btn-toggle group> |  | ||||||
|           <v-btn text v-bind="attrs" v-on="on"> |           <v-btn text v-bind="attrs" v-on="on"> | ||||||
|             {{ $t("general.sort") }} |             {{ $t("general.sort") }} | ||||||
|           </v-btn> |           </v-btn> | ||||||
|           </v-btn-toggle> |  | ||||||
|         </template> |         </template> | ||||||
|         <v-list> |         <v-list> | ||||||
|           <v-list-item @click="$emit('sortRecent')"> |           <v-list-item @click="sortRecipes(EVENTS.az)"> | ||||||
|             <v-list-item-title>{{ $t("general.recent") }}</v-list-item-title> |  | ||||||
|           </v-list-item> |  | ||||||
|           <v-list-item @click="$emit('sort')"> |  | ||||||
|             <v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title> |             <v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title> | ||||||
|           </v-list-item> |           </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-list> | ||||||
|       </v-menu> |       </v-menu> | ||||||
|     </v-app-bar> |     </v-app-bar> | ||||||
| @@ -76,6 +83,9 @@ | |||||||
| <script> | <script> | ||||||
| import RecipeCard from "../Recipe/RecipeCard"; | import RecipeCard from "../Recipe/RecipeCard"; | ||||||
| import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard"; | import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard"; | ||||||
|  | import { utils } from "@/utils"; | ||||||
|  | const SORT_EVENT = "sort"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     RecipeCard, |     RecipeCard, | ||||||
| @@ -106,6 +116,12 @@ export default { | |||||||
|     return { |     return { | ||||||
|       cardLimit: 30, |       cardLimit: 30, | ||||||
|       loading: false, |       loading: false, | ||||||
|  |       EVENTS: { | ||||||
|  |         az: "az", | ||||||
|  |         rating: "rating", | ||||||
|  |         created: "created", | ||||||
|  |         updated: "updated", | ||||||
|  |       }, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
| @@ -144,6 +160,31 @@ export default { | |||||||
|       await new Promise(r => setTimeout(r, 1000)); |       await new Promise(r => setTimeout(r, 1000)); | ||||||
|       this.loading = false; |       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> | </script> | ||||||
|   | |||||||
| @@ -36,6 +36,7 @@ | |||||||
|     "close": "Close", |     "close": "Close", | ||||||
|     "confirm": "Confirm", |     "confirm": "Confirm", | ||||||
|     "create": "Create", |     "create": "Create", | ||||||
|  |     "created": "Created", | ||||||
|     "current-parenthesis": "(Current)", |     "current-parenthesis": "(Current)", | ||||||
|     "dashboard": "Dashboard", |     "dashboard": "Dashboard", | ||||||
|     "delete": "Delete", |     "delete": "Delete", | ||||||
| @@ -63,6 +64,7 @@ | |||||||
|     "no": "No", |     "no": "No", | ||||||
|     "ok": "OK", |     "ok": "OK", | ||||||
|     "options": "Options:", |     "options": "Options:", | ||||||
|  |     "rating": "Rating", | ||||||
|     "random": "Random", |     "random": "Random", | ||||||
|     "recent": "Recent", |     "recent": "Recent", | ||||||
|     "recipes": "Recipes", |     "recipes": "Recipes", | ||||||
| @@ -83,6 +85,7 @@ | |||||||
|     "token": "Token", |     "token": "Token", | ||||||
|     "tuesday": "Tuesday", |     "tuesday": "Tuesday", | ||||||
|     "update": "Update", |     "update": "Update", | ||||||
|  |     "updated": "Updated", | ||||||
|     "upload": "Upload", |     "upload": "Upload", | ||||||
|     "url": "URL", |     "url": "URL", | ||||||
|     "users": "Users", |     "users": "Users", | ||||||
|   | |||||||
| @@ -92,26 +92,36 @@ | |||||||
|           :label="$t('settings.theme.theme-name')" |           :label="$t('settings.theme.theme-name')" | ||||||
|           v-model="defaultData.name" |           v-model="defaultData.name" | ||||||
|           :rules="[rules.required]" |           :rules="[rules.required]" | ||||||
|  |           :append-outer-icon="jsonEditor ? 'mdi-form-select' : 'mdi-code-braces'" | ||||||
|  |           @click:append-outer="jsonEditor = !jsonEditor" | ||||||
|         ></v-text-field> |         ></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"> |           <v-col cols="12" sm="6" v-for="(_, key) in defaultData.colors" :key="key"> | ||||||
|             <ColorPickerDialog :button-text="labels[key]" v-model="defaultData.colors[key]" /> |             <ColorPickerDialog :button-text="labels[key]" v-model="defaultData.colors[key]" /> | ||||||
|           </v-col> |           </v-col> | ||||||
|         </v-row> |         </v-row> | ||||||
|  |         <VJsoneditor @error="logError()" v-else v-model="defaultData" height="250px" :options="jsonEditorOptions" /> | ||||||
|       </v-card-text> |       </v-card-text> | ||||||
|     </BaseDialog> |     </BaseDialog> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  | import VJsoneditor from "v-jsoneditor"; | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| import ColorPickerDialog from "@/components/FormHelpers/ColorPickerDialog"; | import ColorPickerDialog from "@/components/FormHelpers/ColorPickerDialog"; | ||||||
| import BaseDialog from "@/components/UI/Dialogs/BaseDialog"; | import BaseDialog from "@/components/UI/Dialogs/BaseDialog"; | ||||||
| import StatCard from "@/components/UI/StatCard"; | import StatCard from "@/components/UI/StatCard"; | ||||||
| export default { | export default { | ||||||
|   components: { StatCard, BaseDialog, ColorPickerDialog }, |   components: { StatCard, BaseDialog, ColorPickerDialog, VJsoneditor }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|  |       jsonEditor: true, | ||||||
|  |       jsonEditorOptions: { | ||||||
|  |         mode: "code", | ||||||
|  |         search: false, | ||||||
|  |         mainMenuBar: false, | ||||||
|  |       }, | ||||||
|       availableThemes: [], |       availableThemes: [], | ||||||
|       color: "accent", |       color: "accent", | ||||||
|       newTheme: false, |       newTheme: false, | ||||||
|   | |||||||
| @@ -54,7 +54,7 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| import utils from "@/utils"; | import { utils } from "@/utils"; | ||||||
| import NewMeal from "@/components/MealPlan/MealPlanNew"; | import NewMeal from "@/components/MealPlan/MealPlanNew"; | ||||||
| import EditPlan from "@/components/MealPlan/MealPlanEditor"; | import EditPlan from "@/components/MealPlan/MealPlanEditor"; | ||||||
| import ShoppingListDialog from "@/components/MealPlan/ShoppingListDialog"; | import ShoppingListDialog from "@/components/MealPlan/ShoppingListDialog"; | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
| import utils from "@/utils"; | import { utils } from "@/utils"; | ||||||
| export default { | export default { | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ | |||||||
|     <v-card v-else id="myRecipe"> |     <v-card v-else id="myRecipe"> | ||||||
|       <v-img height="400" :src="getImage(recipeDetails.slug)" class="d-print-none" :key="imageKey"> |       <v-img height="400" :src="getImage(recipeDetails.slug)" class="d-print-none" :key="imageKey"> | ||||||
|         <RecipeTimeCard |         <RecipeTimeCard | ||||||
|           class="force-bottom" |           :class="isMobile ? undefined : 'force-bottom'" | ||||||
|           :prepTime="recipeDetails.prepTime" |           :prepTime="recipeDetails.prepTime" | ||||||
|           :totalTime="recipeDetails.totalTime" |           :totalTime="recipeDetails.totalTime" | ||||||
|           :performTime="recipeDetails.performTime" |           :performTime="recipeDetails.performTime" | ||||||
| @@ -106,6 +106,9 @@ export default { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   computed: { |   computed: { | ||||||
|  |     isMobile() { | ||||||
|  |       return this.$vuetify.breakpoint.name === "xs"; | ||||||
|  |     }, | ||||||
|     currentRecipe() { |     currentRecipe() { | ||||||
|       return this.$route.params.recipe; |       return this.$route.params.recipe; | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -5,9 +5,8 @@ | |||||||
|       title-icon="" |       title-icon="" | ||||||
|       :sortable="true" |       :sortable="true" | ||||||
|       :title="$t('page.all-recipes')" |       :title="$t('page.all-recipes')" | ||||||
|       :recipes="allRecipes" |       :recipes="shownRecipes" | ||||||
|       @sort="sortAZ" |       @sort="assignSorted" | ||||||
|       @sort-recent="sortRecent" |  | ||||||
|     /> |     /> | ||||||
|   </v-container> |   </v-container> | ||||||
| </template> | </template> | ||||||
| @@ -22,6 +21,7 @@ export default { | |||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       loading: false, |       loading: false, | ||||||
|  |       sortedResults: [], | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   async mounted() { |   async mounted() { | ||||||
| @@ -35,13 +35,17 @@ export default { | |||||||
|     allRecipes() { |     allRecipes() { | ||||||
|       return this.$store.getters.getAllRecipes; |       return this.$store.getters.getAllRecipes; | ||||||
|     }, |     }, | ||||||
|  |     shownRecipes() { | ||||||
|  |       if (this.sortedResults.length > 0) { | ||||||
|  |         return this.sortedResults; | ||||||
|  |       } else { | ||||||
|  |         return this.allRecipes; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     sortAZ() { |     assignSorted(val) { | ||||||
|       this.allRecipes.sort((a, b) => (a.name > b.name ? 1 : -1)); |       this.sortedResults = val; | ||||||
|     }, |  | ||||||
|     sortRecent() { |  | ||||||
|       this.allRecipes.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1)); |  | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -47,31 +47,20 @@ | |||||||
|         </v-col> |         </v-col> | ||||||
|       </v-row> |       </v-row> | ||||||
|  |  | ||||||
|       <v-row v-if="fuzzyRecipes"> |       <CardSection title-icon="mdi-mag" :recipes="showRecipes" :hardLimit="maxResults" @sort="assignFuzzy" /> | ||||||
|         <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> |  | ||||||
|     </v-card> |     </v-card> | ||||||
|   </v-container> |   </v-container> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import Fuse from "fuse.js"; | import Fuse from "fuse.js"; | ||||||
| import RecipeCard from "@/components/Recipe/RecipeCard"; |  | ||||||
| import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector"; | import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector"; | ||||||
|  | import CardSection from "@/components/UI/CardSection"; | ||||||
| import FilterSelector from "./FilterSelector.vue"; | import FilterSelector from "./FilterSelector.vue"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     RecipeCard, |     CardSection, | ||||||
|     CategoryTagSelector, |     CategoryTagSelector, | ||||||
|     FilterSelector, |     FilterSelector, | ||||||
|   }, |   }, | ||||||
| @@ -88,6 +77,7 @@ export default { | |||||||
|         exclude: false, |         exclude: false, | ||||||
|         matchAny: false, |         matchAny: false, | ||||||
|       }, |       }, | ||||||
|  |       sortedResults: [], | ||||||
|       includeCategories: [], |       includeCategories: [], | ||||||
|       includeTags: [], |       includeTags: [], | ||||||
|       options: { |       options: { | ||||||
| @@ -126,16 +116,31 @@ export default { | |||||||
|     }, |     }, | ||||||
|     fuzzyRecipes() { |     fuzzyRecipes() { | ||||||
|       if (this.searchString.trim() === "") { |       if (this.searchString.trim() === "") { | ||||||
|         return this.filteredRecipes.map(x => ({ item: x })); |         return this.filteredRecipes; | ||||||
|       } |       } | ||||||
|       const result = this.fuse.search(this.searchString.trim()); |       const result = this.fuse.search(this.searchString.trim()); | ||||||
|       return result; |       return result.map(x => x.item); | ||||||
|     }, |     }, | ||||||
|     isSearching() { |     isSearching() { | ||||||
|       return this.searchString && this.searchString.length > 0; |       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: { |   methods: { | ||||||
|  |     assignFuzzy(val) { | ||||||
|  |       this.sortedResults = val; | ||||||
|  |     }, | ||||||
|     check(filterBy, recipeList, matchAny, exclude) { |     check(filterBy, recipeList, matchAny, exclude) { | ||||||
|       let isMatch = true; |       let isMatch = true; | ||||||
|       if (filterBy.length === 0) return isMatch; |       if (filterBy.length === 0) return isMatch; | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import ThisWeek from "@/pages/MealPlan/ThisWeek"; | |||||||
| import { api } from "@/api"; | import { api } from "@/api"; | ||||||
|  |  | ||||||
| import i18n from "@/i18n.js"; | import i18n from "@/i18n.js"; | ||||||
| import utils from "@/utils"; | import { utils } from "@/utils"; | ||||||
|  |  | ||||||
| export const mealRoutes = [ | export const mealRoutes = [ | ||||||
|   { |   { | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import { vueApp } from "../main"; | import { vueApp } from "../main"; | ||||||
|  | import { recipe } from "@/utils/recipe"; | ||||||
|  |  | ||||||
| // TODO: Migrate to Mixins | // TODO: Migrate to Mixins | ||||||
| const notifyHelpers = { | const notifyHelpers = { | ||||||
| @@ -9,7 +10,8 @@ const notifyHelpers = { | |||||||
|   info: "notify-info-color", |   info: "notify-info-color", | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default { | export const utils = { | ||||||
|  |   recipe: recipe, | ||||||
|   getImageURL(image) { |   getImageURL(image) { | ||||||
|     return `/api/recipes/${image}/image?image_type=small`; |     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-compose -f docker-compose.dev.yml -p dev-mealie up --build | ||||||
|  |  | ||||||
| docker-prod: ## Build and Start Docker Production Stack | 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 | code-gen: ## Run Code-Gen Scripts | ||||||
| 	poetry run python dev/scripts/app_routes_gen.py | 	poetry run python dev/scripts/app_routes_gen.py | ||||||
|   | |||||||
| @@ -37,24 +37,14 @@ class _Recipes(BaseDocument): | |||||||
|         return f"{slug}.{extension}" |         return f"{slug}.{extension}" | ||||||
|  |  | ||||||
|     def count_uncategorized(self, session: Session, count=True, override_schema=None) -> int: |     def count_uncategorized(self, session: Session, count=True, override_schema=None) -> int: | ||||||
|         eff_schema = override_schema or self.schema |         return self._countr_attribute( | ||||||
|         if count: |             session, attribute_name=RecipeModel.recipe_category, attr_match=None, count=True, override_schema=None | ||||||
|             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 |  | ||||||
|             ] |  | ||||||
|  |  | ||||||
|     def count_untagged(self, session: Session, count=True, override_schema=None) -> int: |     def count_untagged(self, session: Session, count=True, override_schema=None) -> int: | ||||||
|         eff_schema = override_schema or self.schema |         return self._countr_attribute( | ||||||
|         if count: |             session, attribute_name=RecipeModel.tags, attr_match=None, count=True, override_schema=None | ||||||
|             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 |  | ||||||
|             ] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class _Categories(BaseDocument): | 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.core.root_logger import get_logger | ||||||
| from mealie.db.models.model_base import SqlAlchemyBase | from mealie.db.models.model_base import SqlAlchemyBase | ||||||
| @@ -20,7 +20,7 @@ class BaseDocument: | |||||||
|     # TODO: Improve Get All Query Functionality |     # TODO: Improve Get All Query Functionality | ||||||
|     def get_all( |     def get_all( | ||||||
|         self, session: Session, limit: int = None, order_by: str = None, start=0, end=9999, override_schema=None |         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 |         eff_schema = override_schema or self.schema | ||||||
|  |  | ||||||
|         if order_by: |         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()] |         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 |         """Queries the database for the selected model. Restricts return responses to the | ||||||
|         keys specified under "fields" |         keys specified under "fields" | ||||||
|  |  | ||||||
|         Args: \n |         Args: \n | ||||||
|             session (Session): Database Session Object |             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 |             limit (int): A limit of values to return | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
| @@ -47,7 +47,7 @@ class BaseDocument: | |||||||
|         """ |         """ | ||||||
|         return session.query(self.sql_model).options(load_only(*fields)).limit(limit).all() |         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 |         """Queries the database of the selected model and returns a list | ||||||
|         of all primary_key values |         of all primary_key values | ||||||
|  |  | ||||||
| @@ -79,7 +79,7 @@ class BaseDocument: | |||||||
|  |  | ||||||
|     def get( |     def get( | ||||||
|         self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False |         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 |         """Retrieves an entry from the database by matching a key/value pair. If no | ||||||
|         key is provided the class objects primary key will be used to match against. |         key is provided the class objects primary key will be used to match against. | ||||||
|  |  | ||||||
| @@ -120,6 +120,8 @@ class BaseDocument: | |||||||
|         Returns: |         Returns: | ||||||
|             dict: A dictionary representation of the database entry |             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) |         new_document = self.sql_model(session=session, **document) | ||||||
|         session.add(new_document) |         session.add(new_document) | ||||||
|         session.commit() |         session.commit() | ||||||
| @@ -136,6 +138,7 @@ class BaseDocument: | |||||||
|         Returns: |         Returns: | ||||||
|             dict: Returns a dictionary representation of the database entry |             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 = self._query_one(session=session, match_value=match_value) | ||||||
|         entry.update(session=session, **new_data) |         entry.update(session=session, **new_data) | ||||||
| @@ -144,6 +147,8 @@ class BaseDocument: | |||||||
|         return self.schema.from_orm(entry) |         return self.schema.from_orm(entry) | ||||||
|  |  | ||||||
|     def patch(self, session: Session, match_value: str, new_data: dict) -> BaseModel: |     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) |         entry = self._query_one(session=session, match_value=match_value) | ||||||
|  |  | ||||||
|         if not entry: |         if not entry: | ||||||
| @@ -168,8 +173,21 @@ class BaseDocument: | |||||||
|         session.commit() |         session.commit() | ||||||
|  |  | ||||||
|     def count_all(self, session: Session, match_key=None, match_value=None) -> int: |     def count_all(self, session: Session, match_key=None, match_value=None) -> int: | ||||||
|  |  | ||||||
|         if None in [match_key, match_value]: |         if None in [match_key, match_value]: | ||||||
|             return session.query(self.sql_model).count() |             return session.query(self.sql_model).count() | ||||||
|         else: |         else: | ||||||
|             return session.query(self.sql_model).filter_by(**{match_key: match_value}).count() |             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): | 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): | def default_settings_init(session: Session): | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| import sqlalchemy as sa | import sqlalchemy as sa | ||||||
| import sqlalchemy.orm as orm | import sqlalchemy.orm as orm | ||||||
|  | from mealie.core.config import settings | ||||||
| from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase | from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase | ||||||
| from mealie.db.models.recipe.category import Category, group2categories | from mealie.db.models.recipe.category import Category, group2categories | ||||||
| from sqlalchemy.orm.session import Session | from sqlalchemy.orm.session import Session | ||||||
| from mealie.core.config import settings |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class WebhookURLModel(SqlAlchemyBase): | class WebhookURLModel(SqlAlchemyBase): | ||||||
| @@ -50,10 +50,6 @@ class Group(SqlAlchemyBase, BaseMixins): | |||||||
|         self.webhook_time = webhook_time |         self.webhook_time = webhook_time | ||||||
|         self.webhook_urls = [WebhookURLModel(url=x) for x in webhook_urls] |         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 |     @staticmethod | ||||||
|     def get_ref(session: Session, name: str): |     def get_ref(session: Session, name: str): | ||||||
|         item = session.query(Group).filter(Group.name == name).one_or_none() |         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) |     name = sa.Column(sa.String, nullable=False) | ||||||
|     description = sa.Column(sa.String) |     description = sa.Column(sa.String) | ||||||
|     image = sa.Column(sa.String) |     image = sa.Column(sa.String) | ||||||
|  |  | ||||||
|  |     # Time Related Properties | ||||||
|     total_time = sa.Column(sa.String) |     total_time = sa.Column(sa.String) | ||||||
|     prep_time = sa.Column(sa.String) |     prep_time = sa.Column(sa.String) | ||||||
|     perform_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) |     recipe_yield = sa.Column(sa.String) | ||||||
|     recipeCuisine = sa.Column(sa.String) |     recipeCuisine = sa.Column(sa.String) | ||||||
|  |  | ||||||
|     tools: list[Tool] = orm.relationship("Tool", cascade="all, delete-orphan") |     tools: list[Tool] = orm.relationship("Tool", cascade="all, delete-orphan") | ||||||
|     assets: list[RecipeAsset] = orm.relationship("RecipeAsset", 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") |     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) |     slug = sa.Column(sa.String, index=True, unique=True) | ||||||
|     settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan") |     settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan") | ||||||
|     tags: list[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes") |     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") |     notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan") | ||||||
|     rating = sa.Column(sa.Integer) |     rating = sa.Column(sa.Integer) | ||||||
|     org_url = sa.Column(sa.String) |     org_url = sa.Column(sa.String) | ||||||
|     extras: list[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan") |     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") |     @validates("name") | ||||||
|     def validate_name(self, key, name): |     def validate_name(self, key, name): | ||||||
|         assert name != "" |         assert name != "" | ||||||
| @@ -78,6 +85,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | |||||||
|         recipeCuisine: str = None, |         recipeCuisine: str = None, | ||||||
|         total_time: str = None, |         total_time: str = None, | ||||||
|         prep_time: str = None, |         prep_time: str = None, | ||||||
|  |         cook_time: str = None, | ||||||
|         nutrition: dict = None, |         nutrition: dict = None, | ||||||
|         tools: list[str] = None, |         tools: list[str] = None, | ||||||
|         perform_time: str = None, |         perform_time: str = None, | ||||||
| @@ -85,6 +93,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | |||||||
|         recipe_category: list[str] = None, |         recipe_category: list[str] = None, | ||||||
|         tags: list[str] = None, |         tags: list[str] = None, | ||||||
|         date_added: datetime.date = None, |         date_added: datetime.date = None, | ||||||
|  |         date_updated: datetime.datetime = None, | ||||||
|         notes: list[dict] = None, |         notes: list[dict] = None, | ||||||
|         rating: int = None, |         rating: int = None, | ||||||
|         org_url: str = None, |         org_url: str = None, | ||||||
| @@ -113,6 +122,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): | |||||||
|         self.total_time = total_time |         self.total_time = total_time | ||||||
|         self.prep_time = prep_time |         self.prep_time = prep_time | ||||||
|         self.perform_time = perform_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] |         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.settings = RecipeSettings(**settings) if settings else RecipeSettings() | ||||||
|         self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags] |         self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags] | ||||||
|         self.slug = slug |         self.slug = slug | ||||||
|         self.date_added = date_added |  | ||||||
|         self.notes = [Note(**note) for note in notes] |         self.notes = [Note(**note) for note in notes] | ||||||
|         self.rating = rating |         self.rating = rating | ||||||
|         self.org_url = org_url |         self.org_url = org_url | ||||||
|         self.extras = [ApiExtras(key=key, value=value) for key, value in extras.items()] |         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): |     def update(self, *args, **kwargs): | ||||||
|         """Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions""" |         """Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions""" | ||||||
|         self.__init__(*args, **kwargs) |         self.__init__(*args, **kwargs) | ||||||
|   | |||||||
| @@ -1,14 +1,13 @@ | |||||||
| import sqlalchemy as sa |  | ||||||
| import sqlalchemy.orm as orm | import sqlalchemy.orm as orm | ||||||
| from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase | 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): | class SiteThemeModel(SqlAlchemyBase, BaseMixins): | ||||||
|     __tablename__ = "site_theme" |     __tablename__ = "site_theme" | ||||||
|     id = sa.Column(Integer, primary_key=True, unique=True) |     id = Column(Integer, primary_key=True, unique=True) | ||||||
|     name = sa.Column(sa.String, nullable=False, unique=True) |     name = Column(String, nullable=False, unique=True) | ||||||
|     colors = orm.relationship("ThemeColorsModel", uselist=False, cascade="all, delete") |     colors = orm.relationship("ThemeColorsModel", uselist=False, single_parent=True, cascade="all, delete-orphan") | ||||||
|  |  | ||||||
|     def __init__(self, name: str, colors: dict, *arg, **kwargs) -> None: |     def __init__(self, name: str, colors: dict, *arg, **kwargs) -> None: | ||||||
|         self.name = name |         self.name = name | ||||||
| @@ -17,12 +16,12 @@ class SiteThemeModel(SqlAlchemyBase, BaseMixins): | |||||||
|  |  | ||||||
| class ThemeColorsModel(SqlAlchemyBase, BaseMixins): | class ThemeColorsModel(SqlAlchemyBase, BaseMixins): | ||||||
|     __tablename__ = "theme_colors" |     __tablename__ = "theme_colors" | ||||||
|     id = sa.Column(sa.Integer, primary_key=True) |     id = Column(Integer, primary_key=True) | ||||||
|     parent_id = sa.Column(sa.String, sa.ForeignKey("site_theme.name")) |     parent_id = Column(Integer, ForeignKey("site_theme.id")) | ||||||
|     primary = sa.Column(sa.String) |     primary = Column(String) | ||||||
|     accent = sa.Column(sa.String) |     accent = Column(String) | ||||||
|     secondary = sa.Column(sa.String) |     secondary = Column(String) | ||||||
|     success = sa.Column(sa.String) |     success = Column(String) | ||||||
|     info = sa.Column(sa.String) |     info = Column(String) | ||||||
|     warning = sa.Column(sa.String) |     warning = Column(String) | ||||||
|     error = sa.Column(sa.String) |     error = Column(String) | ||||||
|   | |||||||
| @@ -69,6 +69,9 @@ class RecipeSummary(CamelModel): | |||||||
|     tags: Optional[list[str]] = [] |     tags: Optional[list[str]] = [] | ||||||
|     rating: Optional[int] |     rating: Optional[int] | ||||||
|  |  | ||||||
|  |     date_added: Optional[datetime.date] | ||||||
|  |     date_updated: Optional[datetime.datetime] | ||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
| @@ -95,7 +98,6 @@ class Recipe(RecipeSummary): | |||||||
|     # Mealie Specific |     # Mealie Specific | ||||||
|     settings: Optional[RecipeSettings] |     settings: Optional[RecipeSettings] | ||||||
|     assets: Optional[list[RecipeAsset]] = [] |     assets: Optional[list[RecipeAsset]] = [] | ||||||
|     date_added: Optional[datetime.date] |  | ||||||
|     notes: Optional[list[RecipeNote]] = [] |     notes: Optional[list[RecipeNote]] = [] | ||||||
|     org_url: Optional[str] = Field(None, alias="orgURL") |     org_url: Optional[str] = Field(None, alias="orgURL") | ||||||
|     extras: Optional[dict] = {} |     extras: Optional[dict] = {} | ||||||
|   | |||||||
| @@ -8,8 +8,8 @@ class Colors(BaseModel): | |||||||
|     accent: str = "#00457A" |     accent: str = "#00457A" | ||||||
|     secondary: str = "#973542" |     secondary: str = "#973542" | ||||||
|     success: str = "#43A047" |     success: str = "#43A047" | ||||||
|     info: str = "#4990BA" |     info: str = "#1976D2" | ||||||
|     warning: str = "#FF4081" |     warning: str = "#FF6F00" | ||||||
|     error: str = "#EF5350" |     error: str = "#EF5350" | ||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ def default_theme(): | |||||||
| @pytest.fixture(scope="session") | @pytest.fixture(scope="session") | ||||||
| def new_theme(): | def new_theme(): | ||||||
|     return { |     return { | ||||||
|         "id": 2, |         "id": 3, | ||||||
|         "name": "myTestTheme", |         "name": "myTestTheme", | ||||||
|         "colors": { |         "colors": { | ||||||
|             "primary": "#E58325", |             "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): | def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme): | ||||||
|     response = api_client.get(api_routes.themes) |     response = api_client.get(api_routes.themes) | ||||||
|     assert response.status_code == 200 |     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): | def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user