recipe databse refactor

This commit is contained in:
hay-kot 2021-03-06 13:42:14 -09:00
commit 29d65fddc4
17 changed files with 337 additions and 204 deletions

View file

@ -118,7 +118,7 @@
chips
item-color="secondary"
deletable-chips
v-model="value.categories"
v-model="value.recipeCategory"
hide-selected
:items="categories"
text="name"
@ -359,7 +359,7 @@ export default {
this.value.notes.splice(index, 1);
},
removeCategory(index) {
this.value.categories.splice(index, 1);
this.value.recipeCategory.splice(index, 1);
},
removeTags(index) {
this.value.tags.splice(index, 1);

View file

@ -34,7 +34,7 @@
:description="recipeDetails.description"
:instructions="recipeDetails.recipeInstructions"
:tags="recipeDetails.tags"
:categories="recipeDetails.categories"
:categories="recipeDetails.recipeCategory"
:notes="recipeDetails.notes"
:rating="recipeDetails.rating"
:yields="recipeDetails.recipeYield"

View file

@ -2,7 +2,7 @@ from sqlalchemy.orm.session import Session
from db.db_base import BaseDocument
from db.models.mealplan import MealPlanModel
from db.models.recipe import Category, RecipeModel, Tag
from db.models.recipe.recipe import Category, RecipeModel, Tag
from db.models.settings import SiteSettingsModel
from db.models.sign_up import SignUp
from db.models.theme import SiteThemeModel

View file

@ -1,5 +1,5 @@
from db.models.mealplan import *
from db.models.recipe import *
from db.models.recipe.recipe import *
from db.models.settings import *
from db.models.theme import *
from db.models.users import *

View file

@ -0,0 +1,19 @@
from datetime import date
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
class ApiExtras(SqlAlchemyBase):
__tablename__ = "api_extras"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
key_name = sa.Column(sa.String, unique=True)
value = sa.Column(sa.String)
def __init__(self, key, value) -> None:
self.key_name = key
self.value = value
def dict(self):
return {self.key_name: self.value}

View file

@ -0,0 +1,65 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
from db.models.model_base import SqlAlchemyBase
from fastapi.logger import logger
from slugify import slugify
from sqlalchemy.orm import validates
recipes2categories = sa.Table(
"recipes2categories",
SqlAlchemyBase.metadata,
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
)
class Category(SqlAlchemyBase):
__tablename__ = "categories"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
recipes = orm.relationship(
"RecipeModel", secondary=recipes2categories, back_populates="recipeCategory"
)
@validates("name")
def validate_name(self, key, name):
assert not name == ""
return name
def __init__(self, name) -> None:
self.name = name.strip()
self.slug = slugify(name)
@staticmethod
def create_if_not_exist(session, name: str = None):
test_slug = slugify(name)
try:
result = session.query(Category).filter(Category.slug == test_slug).one()
if result:
logger.info("Category exists, associating recipe")
return result
else:
logger.info("Category doesn't exists, creating tag")
return Category(name=name)
except:
logger.info("Category doesn't exists, creating category")
return Category(name=name)
def to_str(self):
return self.name
def dict(self):
return {
"id": self.id,
"slug": self.slug,
"name": self.name,
"recipes": [x.dict() for x in self.recipes],
}
def dict_no_recipes(self):
return {
"id": self.id,
"slug": self.slug,
"name": self.name,
}

View file

@ -0,0 +1,16 @@
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
class RecipeIngredient(SqlAlchemyBase):
__tablename__ = "recipes_ingredients"
id = sa.Column(sa.Integer, primary_key=True)
position = sa.Column(sa.Integer)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
ingredient = sa.Column(sa.String)
def update(self, ingredient):
self.ingredient = ingredient
def to_str(self):
return self.ingredient

View file

@ -0,0 +1,16 @@
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
class RecipeInstruction(SqlAlchemyBase):
__tablename__ = "recipe_instructions"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
position = sa.Column(sa.Integer)
type = sa.Column(sa.String, default="")
text = sa.Column(sa.String)
def dict(self):
data = {"@type": self.type, "text": self.text}
return data

View file

@ -0,0 +1,17 @@
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
class Note(SqlAlchemyBase):
__tablename__ = "notes"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
title = sa.Column(sa.String)
text = sa.Column(sa.String)
def __init__(self, title, text) -> None:
self.title = title
self.text = text
def dict(self):
return {"title": self.title, "text": self.text}

View file

@ -0,0 +1,40 @@
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
class Nutrition(SqlAlchemyBase):
__tablename__ = "recipe_nutrition"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
calories = sa.Column(sa.Integer)
fatContent = sa.Column(sa.Integer)
fiberContent = sa.Column(sa.Integer)
proteinContent = sa.Column(sa.Integer)
sodiumContent = sa.Column(sa.Integer)
sugarContent = sa.Column(sa.Integer)
def __init__(
self,
calories=None,
fatContent=None,
fiberContent=None,
proteinContent=None,
sodiumContent=None,
sugarContent=None,
) -> None:
self.calories = calories
self.fatContent = fatContent
self.fiberContent = fiberContent
self.proteinContent = proteinContent
self.sodiumContent = sodiumContent
self.sugarContent = sugarContent
def dict(self) -> dict:
return {
"calories": self.calories,
"fatContent": self.fatContent,
"fiberContent": self.fiberContent,
"proteinContent": self.proteinContent,
"sodiumContent": self.sodiumContent,
"sugarContent": self.sugarContent,
}

View file

@ -5,182 +5,16 @@ from typing import List
import sqlalchemy as sa
import sqlalchemy.orm as orm
from db.models.model_base import BaseMixins, SqlAlchemyBase
from slugify import slugify
from db.models.recipe.api_extras import ApiExtras
from db.models.recipe.category import Category, recipes2categories
from db.models.recipe.ingredient import RecipeIngredient
from db.models.recipe.instruction import RecipeInstruction
from db.models.recipe.note import Note
from db.models.recipe.nutrition import Nutrition
from db.models.recipe.tag import Tag, recipes2tags
from db.models.recipe.tool import Tool
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.orm import validates
from fastapi.logger import logger
class ApiExtras(SqlAlchemyBase):
__tablename__ = "api_extras"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
key_name = sa.Column(sa.String, unique=True)
value = sa.Column(sa.String)
def __init__(self, key, value) -> None:
self.key_name = key
self.value = value
def dict(self):
return {self.key_name: self.value}
recipes2categories = sa.Table(
"recipes2categories",
SqlAlchemyBase.metadata,
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
)
recipes2tags = sa.Table(
"recipes2tags",
SqlAlchemyBase.metadata,
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
sa.Column("tag_slug", sa.Integer, sa.ForeignKey("tags.slug")),
)
class Category(SqlAlchemyBase):
__tablename__ = "categories"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
recipes = orm.relationship(
"RecipeModel", secondary=recipes2categories, back_populates="categories"
)
@validates("name")
def validate_name(self, key, name):
assert not name == ""
return name
def __init__(self, name) -> None:
self.name = name.strip()
self.slug = slugify(name)
@staticmethod
def create_if_not_exist(session, name: str = None):
test_slug = slugify(name)
try:
result = session.query(Category).filter(Category.slug == test_slug).one()
if result:
logger.info("Category exists, associating recipe")
return result
else:
logger.info("Category doesn't exists, creating tag")
return Category(name=name)
except:
logger.info("Category doesn't exists, creating category")
return Category(name=name)
def to_str(self):
return self.name
def dict(self):
return {
"id": self.id,
"slug": self.slug,
"name": self.name,
"recipes": [x.dict() for x in self.recipes],
}
def dict_no_recipes(self):
return {
"id": self.id,
"slug": self.slug,
"name": self.name,
}
class Tag(SqlAlchemyBase):
__tablename__ = "tags"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
recipes = orm.relationship(
"RecipeModel", secondary=recipes2tags, back_populates="tags"
)
@validates("name")
def validate_name(self, key, name):
assert not name == ""
return name
def to_str(self):
return self.name
def __init__(self, name) -> None:
self.name = name.strip()
self.slug = slugify(self.name)
def dict(self):
return {
"id": self.id,
"slug": self.slug,
"name": self.name,
"recipes": [x.dict() for x in self.recipes],
}
@staticmethod
def create_if_not_exist(session, name: str = None):
test_slug = slugify(name)
try:
result = session.query(Tag).filter(Tag.slug == test_slug).first()
if result:
logger.info("Tag exists, associating recipe")
return result
else:
logger.info("Tag doesn't exists, creating tag")
return Tag(name=name)
except:
logger.info("Tag doesn't exists, creating tag")
return Tag(name=name)
class Note(SqlAlchemyBase):
__tablename__ = "notes"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
title = sa.Column(sa.String)
text = sa.Column(sa.String)
def __init__(self, title, text) -> None:
self.title = title
self.text = text
def dict(self):
return {"title": self.title, "text": self.text}
class RecipeIngredient(SqlAlchemyBase):
__tablename__ = "recipes_ingredients"
id = sa.Column(sa.Integer, primary_key=True)
position = sa.Column(sa.Integer)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
ingredient = sa.Column(sa.String)
def update(self, ingredient):
self.ingredient = ingredient
def to_str(self):
return self.ingredient
class RecipeInstruction(SqlAlchemyBase):
__tablename__ = "recipe_instructions"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
position = sa.Column(sa.Integer)
type = sa.Column(sa.String, default="")
text = sa.Column(sa.String)
def dict(self):
data = {"@type": self.type, "text": self.text}
return data
class RecipeModel(SqlAlchemyBase, BaseMixins):
@ -192,7 +26,20 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
name = sa.Column(sa.String, nullable=False)
description = sa.Column(sa.String)
image = sa.Column(sa.String)
totalTime = sa.Column(sa.String)
prepTime = sa.Column(sa.String)
performTime = sa.Column(sa.String)
cookTime = sa.Column(sa.String)
recipeYield = sa.Column(sa.String)
recipeCuisine = sa.Column(sa.String)
tool: List[Tool] = orm.relationship("Tool", cascade="all, delete")
nutrition: Nutrition = orm.relationship(
"Nutrition", uselist=False, cascade="all, delete"
)
recipeCategory: List = orm.relationship(
"Category", secondary=recipes2categories, back_populates="recipes"
)
recipeIngredient: List[RecipeIngredient] = orm.relationship(
"RecipeIngredient",
cascade="all, delete",
@ -206,16 +53,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
collection_class=ordering_list("position"),
)
# How to Properties
totalTime = sa.Column(sa.String)
prepTime = sa.Column(sa.String)
performTime = sa.Column(sa.String)
# Mealie Specific
slug = sa.Column(sa.String, index=True, unique=True)
categories: List = orm.relationship(
"Category", secondary=recipes2categories, back_populates="recipes"
)
tags: List[Tag] = orm.relationship(
"Tag", secondary=recipes2tags, back_populates="recipes"
)
@ -239,11 +78,14 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
recipeYield: str = None,
recipeIngredient: List[str] = None,
recipeInstructions: List[dict] = None,
recipeCuisine: str = None,
totalTime: str = None,
prepTime: str = None,
nutrition: dict = None,
tool: list[str] = [],
performTime: str = None,
slug: str = None,
categories: List[str] = None,
recipeCategory: List[str] = None,
tags: List[str] = None,
dateAdded: datetime.date = None,
notes: List[dict] = None,
@ -254,6 +96,15 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
self.name = name
self.description = description
self.image = image
self.recipeCuisine = recipeCuisine
if self.nutrition:
self.nutrition = Nutrition(**nutrition)
else:
self.nutrition = Nutrition()
self.tool = [Tool(tool=x) for x in tool] if tool else []
self.recipeYield = recipeYield
self.recipeIngredient = [
RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient
@ -266,15 +117,14 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
self.prepTime = prepTime
self.performTime = performTime
# Mealie Specific
self.slug = slug
self.categories = [
self.recipeCategory = [
Category.create_if_not_exist(session=session, name=cat)
for cat in categories
for cat in recipeCategory
]
# Mealie Specific
self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags]
self.slug = slug
self.dateAdded = dateAdded
self.notes = [Note(**note) for note in notes]
self.rating = rating
@ -290,11 +140,14 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
recipeYield: str = None,
recipeIngredient: List[str] = None,
recipeInstructions: List[dict] = None,
recipeCuisine: str = None,
totalTime: str = None,
tool: list[str] = [],
prepTime: str = None,
performTime: str = None,
nutrition: dict = None,
slug: str = None,
categories: List[str] = None,
recipeCategory: List[str] = None,
tags: List[str] = None,
dateAdded: datetime.date = None,
notes: List[dict] = None,
@ -303,7 +156,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
extras: dict = None,
):
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
list_of_tables = [RecipeIngredient, RecipeInstruction, ApiExtras]
list_of_tables = [RecipeIngredient, RecipeInstruction, ApiExtras, Tool]
RecipeModel._sql_remove_list(session, list_of_tables, self.id)
self.__init__(
@ -315,10 +168,13 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
recipeIngredient=recipeIngredient,
recipeInstructions=recipeInstructions,
totalTime=totalTime,
recipeCuisine=recipeCuisine,
prepTime=prepTime,
performTime=performTime,
nutrition=nutrition,
tool=tool,
slug=slug,
categories=categories,
recipeCategory=recipeCategory,
tags=tags,
dateAdded=dateAdded,
notes=notes,
@ -333,14 +189,17 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
"description": self.description,
"image": self.image,
"recipeYield": self.recipeYield,
"recipeCuisine": self.recipeCuisine,
"recipeCategory": [x.to_str() for x in self.recipeCategory],
"recipeIngredient": [x.to_str() for x in self.recipeIngredient],
"recipeInstructions": [x.dict() for x in self.recipeInstructions],
"nutrition": self.nutrition.dict(),
"totalTime": self.totalTime,
"prepTime": self.prepTime,
"performTime": self.performTime,
# Mealie
"tool": [x.str() for x in self.tool],
# Mealie Specific
"slug": self.slug,
"categories": [x.to_str() for x in self.categories],
"tags": [x.to_str() for x in self.tags],
"dateAdded": self.dateAdded,
"notes": [x.dict() for x in self.notes],

View file

@ -0,0 +1,60 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
from db.models.model_base import SqlAlchemyBase
from fastapi.logger import logger
from slugify import slugify
from sqlalchemy.orm import validates
recipes2tags = sa.Table(
"recipes2tags",
SqlAlchemyBase.metadata,
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
sa.Column("tag_slug", sa.Integer, sa.ForeignKey("tags.slug")),
)
class Tag(SqlAlchemyBase):
__tablename__ = "tags"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, unique=True, nullable=False)
recipes = orm.relationship(
"RecipeModel", secondary=recipes2tags, back_populates="tags"
)
@validates("name")
def validate_name(self, key, name):
assert not name == ""
return name
def to_str(self):
return self.name
def __init__(self, name) -> None:
self.name = name.strip()
self.slug = slugify(self.name)
def dict(self):
return {
"id": self.id,
"slug": self.slug,
"name": self.name,
"recipes": [x.dict() for x in self.recipes],
}
@staticmethod
def create_if_not_exist(session, name: str = None):
test_slug = slugify(name)
try:
result = session.query(Tag).filter(Tag.slug == test_slug).first()
if result:
logger.info("Tag exists, associating recipe")
return result
else:
logger.info("Tag doesn't exists, creating tag")
return Tag(name=name)
except:
logger.info("Tag doesn't exists, creating tag")
return Tag(name=name)

View file

@ -0,0 +1,15 @@
import sqlalchemy as sa
from db.models.model_base import SqlAlchemyBase
class Tool(SqlAlchemyBase):
__tablename__ = "tools"
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
tool = sa.Column(sa.String)
def __init__(self, tool) -> None:
self.tool = tool
def str(self):
return self.tool

View file

@ -14,14 +14,25 @@ class RecipeStep(BaseModel):
text: str
class Nutrition(BaseModel):
calories: Optional[int]
fatContent: Optional[int]
fiberContent: Optional[int]
proteinContent: Optional[int]
sodiumContent: Optional[int]
sugarContent: Optional[int]
class Recipe(BaseModel):
# Standard Schema
name: str
description: Optional[str]
image: Optional[Any]
recipeYield: Optional[str]
recipeCategory: Optional[List[str]] = []
recipeIngredient: Optional[list]
recipeInstructions: Optional[list]
nutrition: Optional[Nutrition]
totalTime: Optional[str] = None
prepTime: Optional[str] = None
@ -29,7 +40,6 @@ class Recipe(BaseModel):
# Mealie Specific
slug: Optional[str] = ""
categories: Optional[List[str]] = []
tags: Optional[List[str]] = []
dateAdded: Optional[datetime.date]
notes: Optional[List[RecipeNote]] = []
@ -56,7 +66,7 @@ class Recipe(BaseModel):
],
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
"tags": ["favorite", "yummy!"],
"categories": ["Dinner", "Pasta"],
"recipeCategory": ["Dinner", "Pasta"],
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
"rating": 3,

View file

@ -6,11 +6,11 @@ from typing import List
from core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR
from db.database import db
from fastapi.logger import logger
from schema.restore import RecipeImport, SettingsImport, ThemeImport
from schema.theme import SiteTheme
from services.recipe_services import Recipe
from sqlalchemy.orm.session import Session
from fastapi.logger import logger
class ImportDatabase:
@ -85,6 +85,11 @@ class ImportDatabase:
recipe_dict = json.loads(f.read())
recipe_dict = ImportDatabase._recipe_migration(recipe_dict)
try:
if recipe_dict.get("categories", False):
recipe_dict["recipeCategory"] = recipe_dict.get("categories")
del recipe_dict["categories"]
print(recipe_dict)
recipe_obj = Recipe(**recipe_dict)
recipe_obj.save_to_db(self.session)
import_status = RecipeImport(

View file

@ -19,14 +19,26 @@ class RecipeStep(BaseModel):
text: str
class Nutrition(BaseModel):
calories: Optional[int]
fatContent: Optional[int]
fiberContent: Optional[int]
proteinContent: Optional[int]
sodiumContent: Optional[int]
sugarContent: Optional[int]
class Recipe(BaseModel):
# Standard Schema
name: str
description: Optional[str]
image: Optional[Any]
nutrition: Optional[Nutrition]
recipeYield: Optional[str]
recipeCategory: Optional[List[str]] = []
recipeIngredient: Optional[list]
recipeInstructions: Optional[list]
tool: Optional[list[str]]
totalTime: Optional[str] = None
prepTime: Optional[str] = None
@ -34,7 +46,6 @@ class Recipe(BaseModel):
# Mealie Specific
slug: Optional[str] = ""
categories: Optional[List[str]] = []
tags: Optional[List[str]] = []
dateAdded: Optional[datetime.date]
notes: Optional[List[RecipeNote]] = []
@ -61,7 +72,7 @@ class Recipe(BaseModel):
],
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
"tags": ["favorite", "yummy!"],
"categories": ["Dinner", "Pasta"],
"recipeCategory": ["Dinner", "Pasta"],
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
"rating": 3,

View file

@ -34,7 +34,7 @@ def api_client():
yield TestClient(app)
# SQLITE_FILE.unlink()
SQLITE_FILE.unlink()
@fixture(scope="session")