mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -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 db.db_base import BaseDocument
|
||||
|
@ -28,13 +29,13 @@ class _Recipes(BaseDocument):
|
|||
|
||||
class _Categories(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "name"
|
||||
self.primary_key = "slug"
|
||||
self.sql_model = Category
|
||||
|
||||
|
||||
class _Tags(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "name"
|
||||
self.primary_key = "slug"
|
||||
self.sql_model = Tag
|
||||
|
||||
|
||||
|
|
|
@ -16,14 +16,43 @@ class BaseDocument:
|
|||
def get_all(
|
||||
self, session: Session, limit: int = None, order_by: str = None
|
||||
) -> 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:
|
||||
return list[0]
|
||||
|
||||
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(
|
||||
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
|
||||
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_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:
|
||||
"""Creates a new database entry for the given SQL Alchemy Model.
|
||||
|
||||
Args:
|
||||
Args: \n
|
||||
session (Session): A Database Session
|
||||
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:
|
||||
"""Update a database entry.
|
||||
|
||||
Args:
|
||||
Args: \n
|
||||
session (Session): Database Session
|
||||
match_value (str): Match "key"
|
||||
new_data (str): Match "value"
|
||||
|
@ -121,5 +150,4 @@ class BaseDocument:
|
|||
)
|
||||
|
||||
session.delete(result)
|
||||
|
||||
session.commit()
|
||||
|
|
|
@ -5,6 +5,7 @@ from typing import List
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
||||
from slugify import slugify
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from utils.logger import logger
|
||||
|
||||
|
@ -28,14 +29,14 @@ recipes2categories = sa.Table(
|
|||
"recipes2categories",
|
||||
SqlAlchemyBase.metadata,
|
||||
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",
|
||||
SqlAlchemyBase.metadata,
|
||||
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"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String, index=True)
|
||||
slug = sa.Column(sa.String, index=True, unique=True)
|
||||
recipes = orm.relationship(
|
||||
"RecipeModel", secondary=recipes2categories, back_populates="categories"
|
||||
)
|
||||
|
||||
def __init__(self, name) -> None:
|
||||
self.name = name
|
||||
|
||||
@classmethod
|
||||
def create_if_not_exist(cls, session, name: str):
|
||||
self.name = name.strip()
|
||||
self.slug = slugify(name)
|
||||
|
||||
@staticmethod
|
||||
def create_if_not_exist(session, name: str = None):
|
||||
try:
|
||||
result = session.query(Category).filter_by(**{"name": name}).one()
|
||||
logger.info("Category Exists, Associating Recipe")
|
||||
|
||||
result = session.query(Category).filter(Category.name == name.strip()).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 cls(name=name)
|
||||
return Category(name=name)
|
||||
|
||||
def to_str(self):
|
||||
return self.name
|
||||
|
||||
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):
|
||||
__tablename__ = "tags"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String, index=True)
|
||||
slug = sa.Column(sa.String, index=True, unique=True)
|
||||
recipes = orm.relationship(
|
||||
"RecipeModel", secondary=recipes2tags, back_populates="tags"
|
||||
)
|
||||
|
@ -81,22 +92,32 @@ class Tag(SqlAlchemyBase):
|
|||
return self.name
|
||||
|
||||
def __init__(self, name) -> None:
|
||||
self.name = name
|
||||
self.name = name.strip()
|
||||
self.slug = slugify(self.name)
|
||||
|
||||
def dict(self):
|
||||
return {"id": self.id, "name": self.name, "recipes": [x.dict() for x in self.recipes]}
|
||||
|
||||
@classmethod
|
||||
def create_if_not_exist(cls, session, name: str):
|
||||
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):
|
||||
try:
|
||||
result = session.query(Tag).filter_by(**{"name": name}).one()
|
||||
logger.info("Tag Exists, Associating Recipe")
|
||||
result = session.query(Tag).filter(Tag.name == name.strip()).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 cls(name=name)
|
||||
return Tag(name=name)
|
||||
|
||||
|
||||
class Note(SqlAlchemyBase):
|
||||
|
@ -213,7 +234,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||
RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient
|
||||
]
|
||||
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
|
||||
]
|
||||
self.totalTime = totalTime
|
||||
|
@ -222,11 +243,12 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
|||
|
||||
# Mealie Specific
|
||||
self.slug = slug
|
||||
|
||||
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.notes = [Note(**note) for note in notes]
|
||||
|
|
|
@ -7,6 +7,7 @@ from services.recipe_services import Recipe
|
|||
class RecipeCategoryResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
recipes: List[Recipe]
|
||||
|
||||
class Config:
|
||||
|
|
|
@ -4,8 +4,8 @@ import pydantic
|
|||
from pydantic.main import BaseModel
|
||||
|
||||
|
||||
class RecipeResponse(BaseModel):
|
||||
List
|
||||
class AllRecipeResponse(BaseModel):
|
||||
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from db.database import db
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from models.recipe_models import AllRecipeRequest
|
||||
from services.recipe_services import read_requested_values
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(tags=["Recipes"])
|
||||
|
||||
|
||||
@router.get("/api/all-recipes/", response_model=List[dict])
|
||||
@router.get("/api/all-recipes/")
|
||||
def get_all_recipes(
|
||||
keys: Optional[List[str]] = Query(...),
|
||||
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.
|
||||
|
@ -21,28 +21,51 @@ def get_all_recipes(
|
|||
recipes containing the slug, image, and name property. By default, responses
|
||||
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
|
||||
you may also use the post method and provide a body.
|
||||
See the *Post* method for more details.
|
||||
"""
|
||||
|
||||
all_recipes = read_requested_values(db, keys, num)
|
||||
return all_recipes
|
||||
return db.recipes.get_all_limit_columns(session, keys, limit=num)
|
||||
|
||||
|
||||
@router.post("/api/all-recipes/", response_model=List[dict])
|
||||
@router.post("/api/all-recipes/")
|
||||
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.
|
||||
For example, if slug, image, and name are provided you will recieve a list of
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
all_recipes = read_requested_values(db, body.properties, body.limit)
|
||||
|
||||
return all_recipes
|
||||
return db.recipes.get_all_limit_columns(session, body.properties, body.limit)
|
||||
|
|
|
@ -13,8 +13,7 @@ router = APIRouter(
|
|||
@router.get("/all/")
|
||||
async def get_all_recipe_categories(session: Session = Depends(generate_session)):
|
||||
""" Returns a list of available categories in the database """
|
||||
|
||||
return db.categories.get_all_primary_keys(session)
|
||||
return db.categories.get_all_limit_columns(session, ["slug", "name"])
|
||||
|
||||
|
||||
@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. """
|
||||
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)
|
||||
successful_imports.append(recipe.stem)
|
||||
logger.info(f"Imported: {recipe.stem}")
|
||||
except:
|
||||
except Exception as inst:
|
||||
logger.error(inst)
|
||||
logger.info(f"Failed Import: {recipe.stem}")
|
||||
failed_imports.append(recipe.stem)
|
||||
|
||||
|
|
|
@ -40,7 +40,6 @@ class Recipe(BaseModel):
|
|||
dateAdded: Optional[datetime.date]
|
||||
notes: Optional[List[RecipeNote]] = []
|
||||
rating: Optional[int]
|
||||
rating: Optional[int]
|
||||
orgURL: Optional[str]
|
||||
extras: Optional[dict] = {}
|
||||
|
||||
|
@ -138,33 +137,4 @@ class Recipe(BaseModel):
|
|||
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