multiple recipes per day

This commit is contained in:
hay-kot 2021-05-21 23:05:45 -07:00
commit c204a5e628
13 changed files with 323 additions and 159 deletions

View file

@ -1,14 +1,45 @@
<template>
<v-row>
<SearchDialog ref="mealselect" @select="setSlug" />
<v-col cols="12" sm="12" md="6" lg="4" xl="3" v-for="(meal, index) in value" :key="index">
<v-col cols="12" sm="12" md="6" lg="4" xl="3" v-for="(planDay, index) in value" :key="index">
<v-hover v-slot="{ hover }" :open-delay="50">
<v-card :class="{ 'on-hover': hover }" :elevation="hover ? 12 : 2">
<v-img height="200" :src="getImage(meal.slug)" @click="openSearch(index)"></v-img>
<CardImage large :slug="planDay.meals[0].slug" icon-size="200" @click="openSearch(index, modes.primary)">
</CardImage>
<v-card-title class="my-n3 mb-n6">
{{ $d(new Date(meal.date.split("-")), "short") }}
{{ $d(new Date(planDay.date.split("-")), "short") }}
</v-card-title>
<v-card-subtitle> {{ meal.name }}</v-card-subtitle>
<v-card-subtitle class="mb-0 pb-0"> {{ planDay.meals[0].name }}</v-card-subtitle>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="info" outlined small @click="openSearch(index, modes.sides)">
<v-icon small class="mr-1">
mdi-plus
</v-icon>
Side
</v-btn>
</v-card-actions>
<v-divider class="mx-2"></v-divider>
<v-list dense>
<v-list-item v-for="(recipe, i) in planDay.meals.slice(1)" :key="i">
<v-list-item-avatar>
<v-img :alt="recipe.slug" :src="getImage(recipe.slug)"></v-img>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="recipe.name"></v-list-item-title>
</v-list-item-content>
<v-list-item-icon>
<v-btn icon @click="removeSide(index, i + 1)">
<v-icon color="error">
mdi-delete
</v-icon>
</v-btn>
</v-list-item-icon>
</v-list-item>
</v-list>
</v-card>
</v-hover>
</v-col>
@ -18,37 +49,71 @@
<script>
import SearchDialog from "../UI/Search/SearchDialog";
import { api } from "@/api";
import CardImage from "../Recipe/CardImage.vue";
export default {
components: {
SearchDialog,
CardImage,
},
props: {
value: Array,
},
data() {
return {
recipeData: [],
cardData: [],
activeIndex: 0,
mode: "PRIMARY",
modes: {
primary: "PRIMARY",
sides: "SIDES",
},
};
},
watch: {
value(val) {
console.log(val);
},
},
mounted() {
console.log(this.value);
},
methods: {
getImage(slug) {
if (slug) {
return api.recipes.recipeSmallImage(slug);
}
},
setSlug(name, slug) {
let index = this.activeIndex;
this.value[index]["slug"] = slug;
this.value[index]["name"] = name;
setSide(name, slug) {
const meal = { name: name, slug: slug };
this.value[this.activeIndex]["meals"].push(meal);
},
openSearch(index) {
setPrimary(name, slug) {
this.value[this.activeIndex]["meals"][0]["slug"] = slug;
this.value[this.activeIndex]["meals"][0]["name"] = name;
},
setSlug(name, slug) {
switch (this.mode) {
case this.modes.primary:
this.setPrimary(name, slug);
break;
default:
this.setSide(name, slug);
break;
}
},
openSearch(index, mode) {
this.mode = mode;
this.activeIndex = index;
this.$refs.mealselect.open();
},
removeSide(dayIndex, sideIndex) {
this.value[dayIndex]["meals"].splice(sideIndex, 1);
},
},
};
</script>
<style></style>
<style>
.relative-card {
position: relative;
}
</style>

View file

@ -63,14 +63,14 @@
</v-card-text>
<v-card-text v-if="startDate">
<MealPlanCard v-model="meals" />
<MealPlanCard v-model="planDays" />
</v-card-text>
<v-row align="center" justify="end">
<v-card-actions class="mr-5">
<v-btn color="success" @click="random" v-if="meals.length > 0" text>
<v-btn color="success" @click="random" v-if="planDays.length > 0" text>
{{ $t("general.random") }}
</v-btn>
<v-btn color="success" @click="save" text :disabled="meals.length == 0">
<v-btn color="success" @click="save" text :disabled="planDays.length == 0">
{{ $t("general.save") }}
</v-btn>
</v-card-actions>
@ -92,7 +92,7 @@ export default {
data() {
return {
isLoading: false,
meals: [],
planDays: [],
items: [],
// Dates
@ -106,11 +106,17 @@ export default {
watch: {
dateDif() {
this.meals = [];
this.planDays = [];
for (let i = 0; i < this.dateDif; i++) {
this.meals.push({
slug: "empty",
this.planDays.push({
date: this.getDate(i),
meals: [
{
name: "",
slug: "empty",
description: "empty",
},
],
});
}
},
@ -172,10 +178,10 @@ export default {
},
random() {
this.usedRecipes = [1];
this.meals.forEach((element, index) => {
this.planDays.forEach((element, index) => {
let recipe = this.getRandom(this.filteredRecipes);
this.meals[index]["slug"] = recipe.slug;
this.meals[index]["name"] = recipe.name;
this.planDays[index]["meals"][0]["slug"] = recipe.slug;
this.planDays[index]["meals"][0]["name"] = recipe.name;
this.usedRecipes.push(recipe);
});
},
@ -193,11 +199,11 @@ export default {
group: this.groupSettings.name,
startDate: this.startDate,
endDate: this.endDate,
meals: this.meals,
planDays: this.planDays,
};
if (await api.mealPlans.create(mealBody)) {
this.$emit(CREATE_EVENT);
this.meals = [];
this.planDays = [];
this.startDate = null;
this.endDate = null;
}

View file

@ -0,0 +1,85 @@
<template>
<div @click="$emit('click')">
<v-img
:height="height"
v-if="!fallBackImage"
:src="getImage(slug)"
@load="fallBackImage = false"
@error="fallBackImage = true"
>
<slot></slot>
</v-img>
<v-icon v-else color="primary" class="icon-position" :size="iconSize">
mdi-silverware-variant
</v-icon>
</div>
</template>
<script>
import { api } from "@/api";
export default {
props: {
tiny: {
type: Boolean,
default: null,
},
small: {
type: Boolean,
default: null,
},
large: {
type: Boolean,
default: null,
},
iconSize: {
default: 100,
},
slug: {
default: null,
},
height: {
default: 200,
},
},
computed: {
imageSize() {
if (this.tiny) return "tiny";
if (this.small) return "small";
if (this.large) return "large";
return "large";
},
},
watch: {
slug() {
this.fallBackImage = false;
},
},
data() {
return {
fallBackImage: false,
};
},
methods: {
getImage(image) {
switch (this.imageSize) {
case "tiny":
return api.recipes.recipeTinyImage(image);
case "small":
return api.recipes.recipeSmallImage(image);
case "large":
return api.recipes.recipeImage(image);
}
},
},
};
</script>
<style scoped>
.icon-position {
opacity: 0.8;
display: flex !important;
position: relative;
margin-left: auto !important;
margin-right: auto !important;
}
</style>

View file

@ -92,11 +92,5 @@ export default {
text-overflow: ellipsis;
}
.icon-position {
opacity: 0.8;
display: flex !important;
position: relative;
margin-left: auto !important;
margin-right: auto !important;
}
</style>

View file

@ -17,10 +17,22 @@
{{ $d(new Date(mealplan.startDate.split("-")), "short") }} -
{{ $d(new Date(mealplan.endDate.split("-")), "short") }}
</v-card-title>
<v-list nav>
<v-list-item-group color="primary">
<v-list>
<v-list-group v-for="(planDay, pdi) in mealplan.planDays" :key="`planDays-${pdi}`">
<template v-slot:activator>
<v-list-item-avatar color="primary" class="headline font-weight-light white--text">
<v-img :src="getImage(planDay['meals'][0].slug)"></v-img>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-html="$d(new Date(planDay.date.split('-')), 'short')"></v-list-item-title>
<v-list-item-subtitle v-html="planDay['meals'][0].name"></v-list-item-subtitle>
</v-list-item-content>
</template>
<v-list-item
v-for="(meal, index) in mealplan.meals"
three-line
v-for="(meal, index) in planDay.meals"
:key="generateKey(meal.slug, index)"
:to="meal.slug ? `/recipe/${meal.slug}` : null"
>
@ -28,12 +40,13 @@
<v-img :src="getImage(meal.slug)"></v-img>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="meal.name"></v-list-item-title>
<v-list-item-subtitle v-text="$d(new Date(meal.date.split('-')), 'short')"> </v-list-item-subtitle>
<v-list-item-title v-html="meal.name"></v-list-item-title>
<v-list-item-subtitle v-html="meal.description"> </v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list-group>
</v-list>
<v-card-actions class="mt-n5">
<v-btn color="accent lighten-2" class="mx-0" text @click="openShoppingList(mealplan.uid)">
{{ $t("meal-plan.shopping-list") }}
@ -76,6 +89,7 @@ export default {
async requestMeals() {
const response = await api.mealPlans.all();
this.plannedMeals = response.data;
console.log(this.plannedMeals);
},
generateKey(name, index) {
return utils.generateUniqueKey(name, index);

View file

@ -3,7 +3,7 @@ from logging import getLogger
from mealie.db.db_base import BaseDocument
from mealie.db.models.event import Event, EventNotification
from mealie.db.models.group import Group
from mealie.db.models.mealplan import MealPlanModel
from mealie.db.models.mealplan import MealPlan
from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
from mealie.db.models.settings import CustomPage, SiteSettings
from mealie.db.models.sign_up import SignUp
@ -12,7 +12,7 @@ from mealie.db.models.users import LongLiveToken, User
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
from mealie.schema.event_notifications import EventNotificationIn
from mealie.schema.events import Event as EventSchema
from mealie.schema.meal import MealPlanInDB
from mealie.schema.meal import MealPlanOut
from mealie.schema.recipe import Recipe
from mealie.schema.settings import CustomPageOut
from mealie.schema.settings import SiteSettings as SiteSettingsSchema
@ -75,8 +75,8 @@ class _Tags(BaseDocument):
class _Meals(BaseDocument):
def __init__(self) -> None:
self.primary_key = "uid"
self.sql_model = MealPlanModel
self.schema = MealPlanInDB
self.sql_model = MealPlan
self.schema = MealPlanOut
class _Settings(BaseDocument):
@ -120,7 +120,7 @@ class _Groups(BaseDocument):
self.sql_model = Group
self.schema = GroupInDB
def get_meals(self, session: Session, match_value: str, match_key: str = "name") -> list[MealPlanInDB]:
def get_meals(self, session: Session, match_value: str, match_key: str = "name") -> list[MealPlanOut]:
"""A Helper function to get the group from the database and return a sorted list of
Args:
@ -129,7 +129,7 @@ class _Groups(BaseDocument):
match_key (str, optional): Match Key. Defaults to "name".
Returns:
list[MealPlanInDB]: [description]
list[MealPlanOut]: [description]
"""
group: GroupInDB = session.query(self.sql_model).filter_by(**{match_key: match_value}).one_or_none()

View file

@ -19,10 +19,10 @@ class Group(SqlAlchemyBase, BaseMixins):
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
users = orm.relationship("User", back_populates="group")
mealplans = orm.relationship(
"MealPlanModel",
"MealPlan",
back_populates="group",
single_parent=True,
order_by="MealPlanModel.startDate",
order_by="MealPlan.start_date",
)
categories = orm.relationship("Category", secondary=group2categories, single_parent=True)

View file

@ -1,50 +1,63 @@
from typing import List
import sqlalchemy as sa
import sqlalchemy.orm as orm
from mealie.db.models.group import Group
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.recipe.recipe import RecipeModel
from sqlalchemy import Column, Date, ForeignKey, Integer, String
from sqlalchemy.ext.orderinglist import ordering_list
class Meal(SqlAlchemyBase):
__tablename__ = "meal"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.Integer, sa.ForeignKey("mealplan.uid"))
slug = sa.Column(sa.String)
name = sa.Column(sa.String)
date = sa.Column(sa.Date)
image = sa.Column(sa.String)
description = sa.Column(sa.String)
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("mealdays.id"))
position = Column(Integer)
name = Column(String)
slug = Column(String)
description = Column(String)
def __init__(self, slug, name="", description="", session=None) -> None:
if slug and slug != "":
recipe: RecipeModel = session.query(RecipeModel).filter(RecipeModel.slug == slug).one_or_none()
if recipe:
name = recipe.name
self.slug = recipe.slug
description = recipe.description
def __init__(self, slug, name, date, image, description, session=None) -> None:
self.slug = slug
self.name = name
self.date = date
self.image = image
self.description = description
class MealPlanModel(SqlAlchemyBase, BaseMixins):
class MealDay(SqlAlchemyBase, BaseMixins):
__tablename__ = "mealdays"
id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey("mealplan.uid"))
date = Column(Date)
meals: list[Meal] = orm.relationship(
Meal,
cascade="all, delete, delete-orphan",
order_by="Meal.position",
collection_class=ordering_list("position"),
)
def __init__(self, date, meals: list, session=None):
self.date = date
self.meals = [Meal(**m, session=session) for m in meals]
class MealPlan(SqlAlchemyBase, BaseMixins):
__tablename__ = "mealplan"
uid = sa.Column(sa.Integer, primary_key=True, unique=True) # ! Probably Bad?
startDate = sa.Column(sa.Date)
endDate = sa.Column(sa.Date)
meals: List[Meal] = orm.relationship(Meal, cascade="all, delete, delete-orphan")
group_id = sa.Column(sa.Integer, sa.ForeignKey("groups.id"))
uid = Column(Integer, primary_key=True, unique=True)
start_date = Column(Date)
end_date = Column(Date)
plan_days: list[MealDay] = orm.relationship(MealDay, cascade="all, delete, delete-orphan")
group_id = Column(Integer, ForeignKey("groups.id"))
group = orm.relationship("Group", back_populates="mealplans")
def __init__(self, startDate, endDate, meals, group: str, uid=None, session=None) -> None:
self.startDate = startDate
self.endDate = endDate
def __init__(self, start_date, end_date, plan_days, group: str, uid=None, session=None) -> None:
self.start_date = start_date
self.end_date = end_date
self.group = Group.get_ref(session, group)
self.meals = [Meal(**meal) for meal in meals]
def update(self, session, startDate, endDate, meals, uid, group) -> None:
self.__init__(
startDate=startDate,
endDate=endDate,
meals=meals,
group=group,
session=session,
)
self.plan_days = [MealDay(**day, session=session) for day in plan_days]

View file

@ -2,18 +2,18 @@ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.meal import MealPlanIn, MealPlanInDB
from mealie.schema.meal import MealPlanIn, MealPlanOut
from mealie.schema.user import GroupInDB, UserInDB
from mealie.services.events import create_group_event
from mealie.services.image import image
from mealie.services.meal_services import get_todays_meal, process_meals
from mealie.services.meal_services import get_todays_meal, set_mealplan_dates
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
@router.get("/all", response_model=list[MealPlanInDB])
@router.get("/all", response_model=list[MealPlanOut])
def get_all_meals(
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
@ -31,11 +31,11 @@ def create_meal_plan(
current_user: UserInDB = Depends(get_current_user),
):
""" Creates a meal plan database entry """
processed_plan = process_meals(session, data)
set_mealplan_dates(data)
background_tasks.add_task(
create_group_event, "Meal Plan Created", f"Mealplan Created for '{current_user.group}'", session=session
)
return db.meals.create(session, processed_plan.dict())
return db.meals.create(session, data.dict())
@router.put("/{plan_id}")
@ -47,8 +47,8 @@ def update_meal_plan(
current_user: UserInDB = Depends(get_current_user),
):
""" Updates a meal plan based off ID """
processed_plan = process_meals(session, meal_plan)
processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict())
set_mealplan_dates(meal_plan)
processed_plan = MealPlanOut(uid=plan_id, **meal_plan.dict())
try:
db.meals.update(session, plan_id, processed_plan.dict())
background_tasks.add_task(
@ -76,7 +76,7 @@ def delete_meal_plan(
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.get("/this-week", response_model=MealPlanInDB)
@router.get("/this-week", response_model=MealPlanOut)
def get_this_week(session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)):
""" Returns the meal plan data for this week """
plans = db.groups.get_meals(session, current_user.group)

View file

@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.meal import MealPlanInDB
from mealie.schema.meal import MealPlanOut
from mealie.schema.recipe import Recipe
from sqlalchemy.orm.session import Session
@ -18,7 +18,7 @@ def get_shopping_list(
# ! Refactor into Single Database Call
mealplan = db.meals.get(session, id)
mealplan: MealPlanInDB
mealplan: MealPlanOut
slugs = [x.slug for x in mealplan.meals]
recipes: list[Recipe] = [db.recipes.get(session, x) for x in slugs]
return [{"name": x.name, "recipe_ingredient": x.recipe_ingredient} for x in recipes if x]

View file

@ -1,50 +1,60 @@
from datetime import date
from typing import List, Optional
from typing import Optional
from mealie.db.models.mealplan import MealPlanModel
from pydantic import BaseModel, validator
from fastapi_camelcase import CamelModel
from mealie.db.models.mealplan import MealPlan
from pydantic import validator
from pydantic.utils import GetterDict
class MealIn(BaseModel):
name: Optional[str]
class MealIn(CamelModel):
slug: Optional[str]
date: Optional[date]
class MealOut(MealIn):
image: Optional[str]
name: Optional[str]
description: Optional[str]
class Config:
orm_mode = True
class MealPlanIn(BaseModel):
group: str
startDate: date
endDate: date
meals: List[MealIn]
class MealDayIn(CamelModel):
date: Optional[date]
meals: list[MealIn]
@validator("endDate")
def endDate_after_startDate(v, values, config, field):
if "startDate" in values and v < values["startDate"]:
class Config:
orm_mode = True
class MealDayOut(MealDayIn):
id: int
class Config:
orm_mode = True
class MealPlanIn(CamelModel):
group: str
start_date: date
end_date: date
plan_days: list[MealDayIn]
@validator("end_date")
def end_date_after_start_date(v, values, config, field):
if "start_date" in values and v < values["start_date"]:
raise ValueError("EndDate should be greater than StartDate")
return v
class MealPlanProcessed(MealPlanIn):
meals: list[MealOut]
class Config:
orm_mode = True
class MealPlanInDB(MealPlanProcessed):
uid: str
class MealPlanOut(MealPlanIn):
uid: int
class Config:
orm_mode = True
@classmethod
def getter_dict(_cls, name_orm: MealPlanModel):
def getter_dict(_cls, name_orm: MealPlan):
return {
**GetterDict(name_orm),
"group": name_orm.group.name,

View file

@ -5,7 +5,7 @@ from mealie.core.config import settings
from mealie.db.models.group import Group
from mealie.db.models.users import User
from mealie.schema.category import CategoryBase
from mealie.schema.meal import MealPlanInDB
from mealie.schema.meal import MealPlanOut
from pydantic.types import constr
from pydantic.utils import GetterDict
@ -105,7 +105,7 @@ class UpdateGroup(GroupBase):
class GroupInDB(UpdateGroup):
users: Optional[list[UserOut]]
mealplans: Optional[list[MealPlanInDB]]
mealplans: Optional[list[MealPlanOut]]
class Config:
orm_mode = True

View file

@ -3,41 +3,17 @@ from typing import Union
from mealie.db.database import db
from mealie.db.db_setup import create_session
from mealie.schema.meal import MealIn, MealOut, MealPlanIn, MealPlanInDB, MealPlanProcessed
from mealie.schema.meal import MealDayIn, MealPlanIn
from mealie.schema.recipe import Recipe
from mealie.schema.user import GroupInDB
from sqlalchemy.orm.session import Session
def process_meals(session: Session, meal_plan_base: MealPlanIn) -> MealPlanProcessed:
meals = []
for x, meal in enumerate(meal_plan_base.meals):
meal: MealIn
try:
recipe: Recipe = db.recipes.get(session, meal.slug)
def set_mealplan_dates(meal_plan_base: MealPlanIn) -> MealPlanIn:
for x, plan_days in enumerate(meal_plan_base.plan_days):
plan_days: MealDayIn
meal_data = MealOut(
slug=recipe.slug,
name=recipe.name,
date=meal_plan_base.startDate + timedelta(days=x),
image=recipe.image,
description=recipe.description,
)
except Exception:
meal_data = MealOut(
date=meal_plan_base.startDate + timedelta(days=x),
)
meals.append(meal_data)
return MealPlanProcessed(
group=meal_plan_base.group,
meals=meals,
startDate=meal_plan_base.startDate,
endDate=meal_plan_base.endDate,
)
plan_days.date = meal_plan_base.start_date + timedelta(days=x)
def get_todays_meal(session: Session, group: Union[int, GroupInDB]) -> Recipe:
@ -52,22 +28,23 @@ def get_todays_meal(session: Session, group: Union[int, GroupInDB]) -> Recipe:
Returns:
Recipe: Pydantic Recipe Object
"""
session = session or create_session()
if isinstance(group, int):
group: GroupInDB = db.groups.get(session, group)
return
today_slug = None
# session = session or create_session()
for mealplan in group.mealplans:
mealplan: MealPlanInDB
for meal in mealplan.meals:
meal: MealOut
if meal.date == date.today():
today_slug = meal.slug
break
# if isinstance(group, int):
# group: GroupInDB = db.groups.get(session, group)
if today_slug:
return db.recipes.get(session, today_slug)
else:
return None
# today_slug = None
# for mealplan in group.mealplans:
# for meal in mealplan.meals:
# if meal.date == date.today():
# today_slug = meal.slug
# break
# if today_slug:
# return db.recipes.get(session, today_slug)
# else:
# return None