mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
fix categories database errors
This commit is contained in:
parent
88baa46a33
commit
b6111afe69
9 changed files with 132 additions and 78 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
from sqlalchemy.orm import load_only
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from db.db_base import BaseDocument
|
from db.db_base import BaseDocument
|
||||||
|
@ -28,13 +29,13 @@ class _Recipes(BaseDocument):
|
||||||
|
|
||||||
class _Categories(BaseDocument):
|
class _Categories(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "name"
|
self.primary_key = "slug"
|
||||||
self.sql_model = Category
|
self.sql_model = Category
|
||||||
|
|
||||||
|
|
||||||
class _Tags(BaseDocument):
|
class _Tags(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "name"
|
self.primary_key = "slug"
|
||||||
self.sql_model = Tag
|
self.sql_model = Tag
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,14 +16,43 @@ class BaseDocument:
|
||||||
def get_all(
|
def get_all(
|
||||||
self, session: Session, limit: int = None, order_by: str = None
|
self, session: Session, limit: int = None, order_by: str = None
|
||||||
) -> List[dict]:
|
) -> List[dict]:
|
||||||
list = [x.dict() for x in session.query(self.sql_model).all()]
|
list = [x.dict() for x in session.query(self.sql_model).limit(limit).all()]
|
||||||
|
|
||||||
if limit == 1:
|
if limit == 1:
|
||||||
return list[0]
|
return list[0]
|
||||||
|
|
||||||
return list
|
return list
|
||||||
|
|
||||||
def get_all_primary_keys(self, session: Session):
|
def get_all_limit_columns(
|
||||||
|
self, session: Session, fields: List[str], limit: int = None
|
||||||
|
) -> list[SqlAlchemyBase]:
|
||||||
|
"""Queries the database for the selected model. Restricts return responses to the
|
||||||
|
keys specified under "fields"
|
||||||
|
|
||||||
|
Args: \n
|
||||||
|
session (Session): Database Session Object
|
||||||
|
fields (List[str]): List of column names to query
|
||||||
|
limit (int): A limit of values to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[SqlAlchemyBase]: Returns a list of ORM objects
|
||||||
|
"""
|
||||||
|
results = (
|
||||||
|
session.query(self.sql_model).options(load_only(*fields)).limit(limit).all()
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_all_primary_keys(self, session: Session) -> List[str]:
|
||||||
|
"""Queries the database of the selected model and returns a list
|
||||||
|
of all primary_key values
|
||||||
|
|
||||||
|
Args: \n
|
||||||
|
session (Session): Database Session object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]:
|
||||||
|
"""
|
||||||
results = session.query(self.sql_model).options(
|
results = session.query(self.sql_model).options(
|
||||||
load_only(str(self.primary_key))
|
load_only(str(self.primary_key))
|
||||||
)
|
)
|
||||||
|
@ -36,7 +65,7 @@ class BaseDocument:
|
||||||
"""Query the sql database for one item an return the sql alchemy model
|
"""Query the sql database for one item an return the sql alchemy model
|
||||||
object. If no match key is provided the primary_key attribute will be used.
|
object. If no match key is provided the primary_key attribute will be used.
|
||||||
|
|
||||||
Args:
|
Args: \n
|
||||||
match_value (str): The value to use in the query
|
match_value (str): The value to use in the query
|
||||||
match_key (str, optional): the key/property to match against. Defaults to None.
|
match_key (str, optional): the key/property to match against. Defaults to None.
|
||||||
|
|
||||||
|
@ -80,7 +109,7 @@ class BaseDocument:
|
||||||
def save_new(self, session: Session, document: dict) -> dict:
|
def save_new(self, session: Session, document: dict) -> dict:
|
||||||
"""Creates a new database entry for the given SQL Alchemy Model.
|
"""Creates a new database entry for the given SQL Alchemy Model.
|
||||||
|
|
||||||
Args:
|
Args: \n
|
||||||
session (Session): A Database Session
|
session (Session): A Database Session
|
||||||
document (dict): A python dictionary representing the data structure
|
document (dict): A python dictionary representing the data structure
|
||||||
|
|
||||||
|
@ -97,7 +126,7 @@ class BaseDocument:
|
||||||
def update(self, session: Session, match_value: str, new_data: str) -> dict:
|
def update(self, session: Session, match_value: str, new_data: str) -> dict:
|
||||||
"""Update a database entry.
|
"""Update a database entry.
|
||||||
|
|
||||||
Args:
|
Args: \n
|
||||||
session (Session): Database Session
|
session (Session): Database Session
|
||||||
match_value (str): Match "key"
|
match_value (str): Match "key"
|
||||||
new_data (str): Match "value"
|
new_data (str): Match "value"
|
||||||
|
@ -121,5 +150,4 @@ class BaseDocument:
|
||||||
)
|
)
|
||||||
|
|
||||||
session.delete(result)
|
session.delete(result)
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
|
@ -5,6 +5,7 @@ from typing import List
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
from slugify import slugify
|
||||||
from sqlalchemy.ext.orderinglist import ordering_list
|
from sqlalchemy.ext.orderinglist import ordering_list
|
||||||
from utils.logger import logger
|
from utils.logger import logger
|
||||||
|
|
||||||
|
@ -28,14 +29,14 @@ recipes2categories = sa.Table(
|
||||||
"recipes2categories",
|
"recipes2categories",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
||||||
sa.Column("category_name", sa.String, sa.ForeignKey("categories.name")),
|
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")),
|
||||||
)
|
)
|
||||||
|
|
||||||
recipes2tags = sa.Table(
|
recipes2tags = sa.Table(
|
||||||
"recipes2tags",
|
"recipes2tags",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
|
||||||
sa.Column("tag_id", sa.Integer, sa.ForeignKey("tags.id")),
|
sa.Column("tag_slug", sa.Integer, sa.ForeignKey("tags.slug")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,36 +44,46 @@ class Category(SqlAlchemyBase):
|
||||||
__tablename__ = "categories"
|
__tablename__ = "categories"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
name = sa.Column(sa.String, index=True)
|
name = sa.Column(sa.String, index=True)
|
||||||
|
slug = sa.Column(sa.String, index=True, unique=True)
|
||||||
recipes = orm.relationship(
|
recipes = orm.relationship(
|
||||||
"RecipeModel", secondary=recipes2categories, back_populates="categories"
|
"RecipeModel", secondary=recipes2categories, back_populates="categories"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, name) -> None:
|
def __init__(self, name) -> None:
|
||||||
self.name = name
|
self.name = name.strip()
|
||||||
|
self.slug = slugify(name)
|
||||||
@classmethod
|
|
||||||
def create_if_not_exist(cls, session, name: str):
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_if_not_exist(session, name: str = None):
|
||||||
try:
|
try:
|
||||||
result = session.query(Category).filter_by(**{"name": name}).one()
|
result = session.query(Category).filter(Category.name == name.strip()).one()
|
||||||
logger.info("Category Exists, Associating Recipe")
|
if result:
|
||||||
|
logger.info("Category exists, associating recipe")
|
||||||
return result
|
return result
|
||||||
|
else:
|
||||||
|
logger.info("Category doesn't exists, creating tag")
|
||||||
|
return Category(name=name)
|
||||||
except:
|
except:
|
||||||
logger.info("Category doesn't exists, creating category")
|
logger.info("Category doesn't exists, creating category")
|
||||||
return cls(name=name)
|
return Category(name=name)
|
||||||
|
|
||||||
def to_str(self):
|
def to_str(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
return {"id": self.id, "name": self.name, "recipes": [x.dict() for x in self.recipes]}
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"slug": self.slug,
|
||||||
|
"name": self.name,
|
||||||
|
"recipes": [x.dict() for x in self.recipes],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Tag(SqlAlchemyBase):
|
class Tag(SqlAlchemyBase):
|
||||||
__tablename__ = "tags"
|
__tablename__ = "tags"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
name = sa.Column(sa.String, index=True)
|
name = sa.Column(sa.String, index=True)
|
||||||
|
slug = sa.Column(sa.String, index=True, unique=True)
|
||||||
recipes = orm.relationship(
|
recipes = orm.relationship(
|
||||||
"RecipeModel", secondary=recipes2tags, back_populates="tags"
|
"RecipeModel", secondary=recipes2tags, back_populates="tags"
|
||||||
)
|
)
|
||||||
|
@ -81,22 +92,32 @@ class Tag(SqlAlchemyBase):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __init__(self, name) -> None:
|
def __init__(self, name) -> None:
|
||||||
self.name = name
|
self.name = name.strip()
|
||||||
|
self.slug = slugify(self.name)
|
||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
return {"id": self.id, "name": self.name, "recipes": [x.dict() for x in self.recipes]}
|
return {
|
||||||
|
"id": self.id,
|
||||||
@classmethod
|
"slug": self.slug,
|
||||||
def create_if_not_exist(cls, session, name: str):
|
"name": self.name,
|
||||||
|
"recipes": [x.dict() for x in self.recipes],
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_if_not_exist(session, name: str = None):
|
||||||
try:
|
try:
|
||||||
result = session.query(Tag).filter_by(**{"name": name}).one()
|
result = session.query(Tag).filter(Tag.name == name.strip()).first()
|
||||||
logger.info("Tag Exists, Associating Recipe")
|
|
||||||
|
|
||||||
return result
|
if result:
|
||||||
|
logger.info("Tag exists, associating recipe")
|
||||||
|
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
logger.info("Tag doesn't exists, creating tag")
|
||||||
|
return Tag(name=name)
|
||||||
except:
|
except:
|
||||||
logger.info("Tag doesn't exists, creating tag")
|
logger.info("Tag doesn't exists, creating tag")
|
||||||
return cls(name=name)
|
return Tag(name=name)
|
||||||
|
|
||||||
|
|
||||||
class Note(SqlAlchemyBase):
|
class Note(SqlAlchemyBase):
|
||||||
|
@ -213,7 +234,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||||
RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient
|
RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient
|
||||||
]
|
]
|
||||||
self.recipeInstructions = [
|
self.recipeInstructions = [
|
||||||
RecipeInstruction(text=instruc.get("text"), type=instruc.get("text"))
|
RecipeInstruction(text=instruc.get("text"), type=instruc.get("@type", None))
|
||||||
for instruc in recipeInstructions
|
for instruc in recipeInstructions
|
||||||
]
|
]
|
||||||
self.totalTime = totalTime
|
self.totalTime = totalTime
|
||||||
|
@ -222,11 +243,12 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||||
|
|
||||||
# Mealie Specific
|
# Mealie Specific
|
||||||
self.slug = slug
|
self.slug = slug
|
||||||
|
|
||||||
self.categories = [
|
self.categories = [
|
||||||
(Category.create_if_not_exist(session, cat)) for cat in categories
|
Category.create_if_not_exist(session=session, name=cat)
|
||||||
|
for cat in categories
|
||||||
]
|
]
|
||||||
self.tags = [Tag.create_if_not_exist(session, name=tag) for tag in tags]
|
|
||||||
|
self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags]
|
||||||
|
|
||||||
self.dateAdded = dateAdded
|
self.dateAdded = dateAdded
|
||||||
self.notes = [Note(**note) for note in notes]
|
self.notes = [Note(**note) for note in notes]
|
||||||
|
|
|
@ -7,6 +7,7 @@ from services.recipe_services import Recipe
|
||||||
class RecipeCategoryResponse(BaseModel):
|
class RecipeCategoryResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
|
slug: str
|
||||||
recipes: List[Recipe]
|
recipes: List[Recipe]
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|
|
@ -4,8 +4,8 @@ import pydantic
|
||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class RecipeResponse(BaseModel):
|
class AllRecipeResponse(BaseModel):
|
||||||
List
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
schema_extra = {
|
schema_extra = {
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from db.database import db
|
||||||
from db.db_setup import generate_session
|
from db.db_setup import generate_session
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
from models.recipe_models import AllRecipeRequest
|
from models.recipe_models import AllRecipeRequest
|
||||||
from services.recipe_services import read_requested_values
|
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
router = APIRouter(tags=["Recipes"])
|
router = APIRouter(tags=["Recipes"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/all-recipes/", response_model=List[dict])
|
@router.get("/api/all-recipes/")
|
||||||
def get_all_recipes(
|
def get_all_recipes(
|
||||||
keys: Optional[List[str]] = Query(...),
|
keys: Optional[List[str]] = Query(...),
|
||||||
num: Optional[int] = 100,
|
num: Optional[int] = 100,
|
||||||
db: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Returns key data for all recipes based off the query paramters provided.
|
Returns key data for all recipes based off the query paramters provided.
|
||||||
|
@ -21,28 +21,51 @@ def get_all_recipes(
|
||||||
recipes containing the slug, image, and name property. By default, responses
|
recipes containing the slug, image, and name property. By default, responses
|
||||||
are limited to 100.
|
are limited to 100.
|
||||||
|
|
||||||
|
At this time you can only query top level values:
|
||||||
|
|
||||||
|
- slug
|
||||||
|
- name
|
||||||
|
- description
|
||||||
|
- image
|
||||||
|
- recipeYield
|
||||||
|
- totalTime
|
||||||
|
- prepTime
|
||||||
|
- performTime
|
||||||
|
- rating
|
||||||
|
- orgURL
|
||||||
|
|
||||||
**Note:** You may experience problems with with query parameters. As an alternative
|
**Note:** You may experience problems with with query parameters. As an alternative
|
||||||
you may also use the post method and provide a body.
|
you may also use the post method and provide a body.
|
||||||
See the *Post* method for more details.
|
See the *Post* method for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
all_recipes = read_requested_values(db, keys, num)
|
return db.recipes.get_all_limit_columns(session, keys, limit=num)
|
||||||
return all_recipes
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/all-recipes/", response_model=List[dict])
|
@router.post("/api/all-recipes/")
|
||||||
def get_all_recipes_post(
|
def get_all_recipes_post(
|
||||||
body: AllRecipeRequest, db: Session = Depends(generate_session)
|
body: AllRecipeRequest, session: Session = Depends(generate_session)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Returns key data for all recipes based off the body data provided.
|
Returns key data for all recipes based off the body data provided.
|
||||||
For example, if slug, image, and name are provided you will recieve a list of
|
For example, if slug, image, and name are provided you will recieve a list of
|
||||||
recipes containing the slug, image, and name property.
|
recipes containing the slug, image, and name property.
|
||||||
|
|
||||||
|
At this time you can only query top level values:
|
||||||
|
|
||||||
|
- slug
|
||||||
|
- name
|
||||||
|
- description
|
||||||
|
- image
|
||||||
|
- recipeYield
|
||||||
|
- totalTime
|
||||||
|
- prepTime
|
||||||
|
- performTime
|
||||||
|
- rating
|
||||||
|
- orgURL
|
||||||
|
|
||||||
Refer to the body example for data formats.
|
Refer to the body example for data formats.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
all_recipes = read_requested_values(db, body.properties, body.limit)
|
return db.recipes.get_all_limit_columns(session, body.properties, body.limit)
|
||||||
|
|
||||||
return all_recipes
|
|
||||||
|
|
|
@ -13,8 +13,7 @@ router = APIRouter(
|
||||||
@router.get("/all/")
|
@router.get("/all/")
|
||||||
async def get_all_recipe_categories(session: Session = Depends(generate_session)):
|
async def get_all_recipe_categories(session: Session = Depends(generate_session)):
|
||||||
""" Returns a list of available categories in the database """
|
""" Returns a list of available categories in the database """
|
||||||
|
return db.categories.get_all_limit_columns(session, ["slug", "name"])
|
||||||
return db.categories.get_all_primary_keys(session)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{category}/", response_model=RecipeCategoryResponse)
|
@router.get("/{category}/", response_model=RecipeCategoryResponse)
|
||||||
|
@ -23,3 +22,12 @@ def get_all_recipes_by_category(
|
||||||
):
|
):
|
||||||
""" Returns a list of recipes associated with the provided category. """
|
""" Returns a list of recipes associated with the provided category. """
|
||||||
return db.categories.get(session, category)
|
return db.categories.get(session, category)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{category}/")
|
||||||
|
async def delete_recipe_category(
|
||||||
|
category: str, session: Session = Depends(generate_session)
|
||||||
|
):
|
||||||
|
""" Removes a recipe category from the database """
|
||||||
|
|
||||||
|
db.categories.delete(session, category)
|
||||||
|
|
|
@ -80,7 +80,8 @@ class ImportDatabase:
|
||||||
recipe_obj.save_to_db(self.session)
|
recipe_obj.save_to_db(self.session)
|
||||||
successful_imports.append(recipe.stem)
|
successful_imports.append(recipe.stem)
|
||||||
logger.info(f"Imported: {recipe.stem}")
|
logger.info(f"Imported: {recipe.stem}")
|
||||||
except:
|
except Exception as inst:
|
||||||
|
logger.error(inst)
|
||||||
logger.info(f"Failed Import: {recipe.stem}")
|
logger.info(f"Failed Import: {recipe.stem}")
|
||||||
failed_imports.append(recipe.stem)
|
failed_imports.append(recipe.stem)
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,6 @@ class Recipe(BaseModel):
|
||||||
dateAdded: Optional[datetime.date]
|
dateAdded: Optional[datetime.date]
|
||||||
notes: Optional[List[RecipeNote]] = []
|
notes: Optional[List[RecipeNote]] = []
|
||||||
rating: Optional[int]
|
rating: Optional[int]
|
||||||
rating: Optional[int]
|
|
||||||
orgURL: Optional[str]
|
orgURL: Optional[str]
|
||||||
extras: Optional[dict] = {}
|
extras: Optional[dict] = {}
|
||||||
|
|
||||||
|
@ -138,33 +137,4 @@ class Recipe(BaseModel):
|
||||||
return db.recipes.get_all(session)
|
return db.recipes.get_all(session)
|
||||||
|
|
||||||
|
|
||||||
def read_requested_values(
|
|
||||||
session: Session, keys: list, max_results: int = 0
|
|
||||||
) -> List[dict]:
|
|
||||||
"""
|
|
||||||
Pass in a list of key values to be run against the database. If a match is found
|
|
||||||
it is then added to a dictionary inside of a list. If a key does not exist the
|
|
||||||
it will simply not be added to the return data.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
keys: list
|
|
||||||
|
|
||||||
Returns: returns a list of dicts containing recipe data
|
|
||||||
|
|
||||||
"""
|
|
||||||
recipe_list = []
|
|
||||||
for recipe in db.recipes.get_all(
|
|
||||||
session=session, limit=max_results, order_by="dateAdded"
|
|
||||||
):
|
|
||||||
recipe_details = {}
|
|
||||||
for key in keys:
|
|
||||||
try:
|
|
||||||
recipe_key = {key: recipe[key]}
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
recipe_details.update(recipe_key)
|
|
||||||
|
|
||||||
recipe_list.append(recipe_details)
|
|
||||||
|
|
||||||
return recipe_list
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue