mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
database refactoring
This commit is contained in:
parent
9507514935
commit
db61ac8a31
26 changed files with 234 additions and 113 deletions
|
@ -1,10 +1,15 @@
|
||||||
# Release Notes
|
# Release Notes
|
||||||
|
|
||||||
## v0.4.0 Whoa, What a Release! [DRAFT]
|
## v0.4.0 Whoa, What a Release! [DRAFT]
|
||||||
- Authentication! Tons of stuff went into creating a flexible authentication platform for a lot of different use cases. Review the documentation for more information on how to use the authentication, and how everything works together.
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
### Features and Improvements
|
||||||
|
- Authentication! Tons of stuff went into creating a flexible authentication platform for a lot of different use cases. Review the documentation for more information on how to use the authentication, and how everything works together. Some key features include
|
||||||
- Sign Up Links
|
- Sign Up Links
|
||||||
- Admin and User Roles
|
- Admin and User Roles
|
||||||
- Group Management
|
- Group Management
|
||||||
|
- Create/Edit/Delete Restrictions
|
||||||
- Recipe Database Refactoring. Tons of new information is now stored for recipes in the database. Not all is accessible via the UI, but it's coming.
|
- Recipe Database Refactoring. Tons of new information is now stored for recipes in the database. Not all is accessible via the UI, but it's coming.
|
||||||
- Nutrition Information
|
- Nutrition Information
|
||||||
- calories
|
- calories
|
||||||
|
@ -13,9 +18,18 @@
|
||||||
- proteinContent
|
- proteinContent
|
||||||
- sodiumContent
|
- sodiumContent
|
||||||
- sugarContent
|
- sugarContent
|
||||||
- recipeCuisine
|
- recipeCuisine has been added
|
||||||
- "categories" has been migrated to "recipeCategory" to adhear closer to the standard schema
|
- "categories" has been migrated to "recipeCategory" to adhear closer to the standard schema
|
||||||
- "tool" - a list of tools used for the recipe
|
- "tool" - a list of tools used for the recipe
|
||||||
|
- Removed CDN dependencies
|
||||||
|
- Completed Redesign of the Admin Panel
|
||||||
|
- Profile Pages
|
||||||
|
- Side Panel Menu
|
||||||
|
- Language selector is now displayed on all pages and does not require an account
|
||||||
|
|
||||||
|
### Development / Misc
|
||||||
|
- Database Model Refactoring
|
||||||
|
- File/Folder Name Refactoring
|
||||||
|
|
||||||
## v0.3.0
|
## v0.3.0
|
||||||
|
|
||||||
|
|
|
@ -63,8 +63,8 @@
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="12" md="6">
|
<v-col cols="12" sm="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="editedItem.family"
|
v-model="editedItem.group"
|
||||||
label="Family Group"
|
label="Group Group"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="12" md="6" v-if="showPassword">
|
<v-col cols="12" sm="12" md="6" v-if="showPassword">
|
||||||
|
@ -143,7 +143,7 @@ export default {
|
||||||
},
|
},
|
||||||
{ text: "Full Name", value: "fullName" },
|
{ text: "Full Name", value: "fullName" },
|
||||||
{ text: "Email", value: "email" },
|
{ text: "Email", value: "email" },
|
||||||
{ text: "Family", value: "family" },
|
{ text: "Group", value: "group" },
|
||||||
{ text: "Admin", value: "admin" },
|
{ text: "Admin", value: "admin" },
|
||||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||||
],
|
],
|
||||||
|
@ -154,7 +154,7 @@ export default {
|
||||||
fullName: "",
|
fullName: "",
|
||||||
password: "",
|
password: "",
|
||||||
email: "",
|
email: "",
|
||||||
family: "",
|
group: "",
|
||||||
admin: false,
|
admin: false,
|
||||||
},
|
},
|
||||||
defaultItem: {
|
defaultItem: {
|
||||||
|
@ -162,7 +162,7 @@ export default {
|
||||||
fullName: "",
|
fullName: "",
|
||||||
password: "",
|
password: "",
|
||||||
email: "",
|
email: "",
|
||||||
family: "",
|
group: "",
|
||||||
admin: false,
|
admin: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -63,8 +63,8 @@
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="12" md="6">
|
<v-col cols="12" sm="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="editedItem.family"
|
v-model="editedItem.group"
|
||||||
label="Family Group"
|
label="Group Group"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="12" md="6" v-if="showPassword">
|
<v-col cols="12" sm="12" md="6" v-if="showPassword">
|
||||||
|
@ -143,7 +143,7 @@ export default {
|
||||||
},
|
},
|
||||||
{ text: "Full Name", value: "fullName" },
|
{ text: "Full Name", value: "fullName" },
|
||||||
{ text: "Email", value: "email" },
|
{ text: "Email", value: "email" },
|
||||||
{ text: "Family", value: "family" },
|
{ text: "Group", value: "group" },
|
||||||
{ text: "Admin", value: "admin" },
|
{ text: "Admin", value: "admin" },
|
||||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||||
],
|
],
|
||||||
|
@ -154,7 +154,7 @@ export default {
|
||||||
fullName: "",
|
fullName: "",
|
||||||
password: "",
|
password: "",
|
||||||
email: "",
|
email: "",
|
||||||
family: "",
|
group: "",
|
||||||
admin: false,
|
admin: false,
|
||||||
},
|
},
|
||||||
defaultItem: {
|
defaultItem: {
|
||||||
|
@ -162,7 +162,7 @@ export default {
|
||||||
fullName: "",
|
fullName: "",
|
||||||
password: "",
|
password: "",
|
||||||
email: "",
|
email: "",
|
||||||
family: "",
|
group: "",
|
||||||
admin: false,
|
admin: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -121,7 +121,7 @@ export default {
|
||||||
const userData = {
|
const userData = {
|
||||||
fullName: this.user.name,
|
fullName: this.user.name,
|
||||||
email: this.user.email,
|
email: this.user.email,
|
||||||
family: "public",
|
group: "default",
|
||||||
password: this.user.password,
|
password: this.user.password,
|
||||||
admin: false,
|
admin: false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -55,11 +55,11 @@
|
||||||
>
|
>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="Family"
|
label="Group"
|
||||||
readonly
|
readonly
|
||||||
v-model="user.family"
|
v-model="user.group"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
hint="Family groups can only be set by administrators"
|
hint="Group groups can only be set by administrators"
|
||||||
>
|
>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
</v-form>
|
</v-form>
|
||||||
|
@ -167,7 +167,7 @@ export default {
|
||||||
user: {
|
user: {
|
||||||
fullName: "Change Me",
|
fullName: "Change Me",
|
||||||
email: "changeme@email.com",
|
email: "changeme@email.com",
|
||||||
family: "public",
|
group: "public",
|
||||||
admin: true,
|
admin: true,
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.logger import logger
|
||||||
|
|
||||||
# import utils.startup as startup
|
# import utils.startup as startup
|
||||||
from core.config import APP_VERSION, PORT, SECRET, docs_url, redoc_url
|
from core.config import APP_VERSION, PORT, SECRET, docs_url, redoc_url
|
||||||
|
@ -13,6 +14,7 @@ from routes import (
|
||||||
setting_routes,
|
setting_routes,
|
||||||
theme_routes,
|
theme_routes,
|
||||||
)
|
)
|
||||||
|
from routes.groups import groups
|
||||||
from routes.recipe import (
|
from routes.recipe import (
|
||||||
all_recipe_routes,
|
all_recipe_routes,
|
||||||
category_routes,
|
category_routes,
|
||||||
|
@ -20,7 +22,6 @@ from routes.recipe import (
|
||||||
tag_routes,
|
tag_routes,
|
||||||
)
|
)
|
||||||
from routes.users import users
|
from routes.users import users
|
||||||
from fastapi.logger import logger
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Mealie",
|
title="Mealie",
|
||||||
|
@ -42,11 +43,13 @@ def start_scheduler():
|
||||||
def api_routers():
|
def api_routers():
|
||||||
# Authentication
|
# Authentication
|
||||||
app.include_router(users.router)
|
app.include_router(users.router)
|
||||||
|
app.include_router(groups.router)
|
||||||
# Recipes
|
# Recipes
|
||||||
app.include_router(all_recipe_routes.router)
|
app.include_router(all_recipe_routes.router)
|
||||||
app.include_router(category_routes.router)
|
app.include_router(category_routes.router)
|
||||||
app.include_router(tag_routes.router)
|
app.include_router(tag_routes.router)
|
||||||
app.include_router(recipe_crud_routes.router)
|
app.include_router(recipe_crud_routes.router)
|
||||||
|
|
||||||
# Meal Routes
|
# Meal Routes
|
||||||
app.include_router(meal_routes.router)
|
app.include_router(meal_routes.router)
|
||||||
# Settings Routes
|
# Settings Routes
|
||||||
|
|
|
@ -83,6 +83,7 @@ else:
|
||||||
|
|
||||||
# Mongo Database
|
# Mongo Database
|
||||||
MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie")
|
MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie")
|
||||||
|
DEFAULT_GROUP = os.getenv("default_group", "home")
|
||||||
DB_USERNAME = os.getenv("db_username", "root")
|
DB_USERNAME = os.getenv("db_username", "root")
|
||||||
DB_PASSWORD = os.getenv("db_password", "example")
|
DB_PASSWORD = os.getenv("db_password", "example")
|
||||||
DB_HOST = os.getenv("db_host", "mongo")
|
DB_HOST = os.getenv("db_host", "mongo")
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
from schema.user import GroupInDB, UserInDB
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from db.db_base import BaseDocument
|
from db.db_base import BaseDocument
|
||||||
|
from db.models.group import Group
|
||||||
from db.models.mealplan import MealPlanModel
|
from db.models.mealplan import MealPlanModel
|
||||||
from db.models.recipe.recipe import Category, RecipeModel, Tag
|
from db.models.recipe.recipe import Category, RecipeModel, Tag
|
||||||
from db.models.settings import SiteSettingsModel
|
from db.models.settings import SiteSettingsModel
|
||||||
|
@ -8,16 +10,12 @@ from db.models.sign_up import SignUp
|
||||||
from db.models.theme import SiteThemeModel
|
from db.models.theme import SiteThemeModel
|
||||||
from db.models.users import User
|
from db.models.users import User
|
||||||
|
|
||||||
"""
|
|
||||||
# TODO
|
|
||||||
- [ ] Abstract Classes to use save_new, and update from base models
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class _Recipes(BaseDocument):
|
class _Recipes(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "slug"
|
self.primary_key = "slug"
|
||||||
self.sql_model: RecipeModel = RecipeModel
|
self.sql_model: RecipeModel = RecipeModel
|
||||||
|
self.orm_mode = False
|
||||||
|
|
||||||
def update_image(self, session: Session, slug: str, extension: str = None) -> str:
|
def update_image(self, session: Session, slug: str, extension: str = None) -> str:
|
||||||
entry: RecipeModel = self._query_one(session, match_value=slug)
|
entry: RecipeModel = self._query_one(session, match_value=slug)
|
||||||
|
@ -31,36 +29,43 @@ class _Categories(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "slug"
|
self.primary_key = "slug"
|
||||||
self.sql_model = Category
|
self.sql_model = Category
|
||||||
|
self.orm_mode = False
|
||||||
|
|
||||||
|
|
||||||
class _Tags(BaseDocument):
|
class _Tags(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "slug"
|
self.primary_key = "slug"
|
||||||
self.sql_model = Tag
|
self.sql_model = Tag
|
||||||
|
self.orm_mode = False
|
||||||
|
|
||||||
|
|
||||||
class _Meals(BaseDocument):
|
class _Meals(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "uid"
|
self.primary_key = "uid"
|
||||||
self.sql_model = MealPlanModel
|
self.sql_model = MealPlanModel
|
||||||
|
self.orm_mode = False
|
||||||
|
|
||||||
|
|
||||||
class _Settings(BaseDocument):
|
class _Settings(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "name"
|
self.primary_key = "name"
|
||||||
self.sql_model = SiteSettingsModel
|
self.sql_model = SiteSettingsModel
|
||||||
|
self.orm_mode = False
|
||||||
|
|
||||||
|
|
||||||
class _Themes(BaseDocument):
|
class _Themes(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "name"
|
self.primary_key = "name"
|
||||||
self.sql_model = SiteThemeModel
|
self.sql_model = SiteThemeModel
|
||||||
|
self.orm_mode = False
|
||||||
|
|
||||||
|
|
||||||
class _Users(BaseDocument):
|
class _Users(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "id"
|
self.primary_key = "id"
|
||||||
self.sql_model = User
|
self.sql_model = User
|
||||||
|
self.orm_mode = True
|
||||||
|
self.schema = UserInDB
|
||||||
|
|
||||||
def update_password(self, session, id, password: str):
|
def update_password(self, session, id, password: str):
|
||||||
entry = self._query_one(session=session, match_value=id)
|
entry = self._query_one(session=session, match_value=id)
|
||||||
|
@ -71,16 +76,19 @@ class _Users(BaseDocument):
|
||||||
return return_data
|
return return_data
|
||||||
|
|
||||||
|
|
||||||
class _SignUps(BaseDocument):
|
class _Groups(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "token"
|
self.primary_key = "id"
|
||||||
self.sql_model = SignUp
|
self.sql_model = Group
|
||||||
|
self.orm_mode = True
|
||||||
|
self.schema = GroupInDB
|
||||||
|
|
||||||
|
|
||||||
class _SignUps(BaseDocument):
|
class _SignUps(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "token"
|
self.primary_key = "token"
|
||||||
self.sql_model = SignUp
|
self.sql_model = SignUp
|
||||||
|
self.orm_mode = False
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
|
@ -93,6 +101,7 @@ class Database:
|
||||||
self.tags = _Tags()
|
self.tags = _Tags()
|
||||||
self.users = _Users()
|
self.users = _Users()
|
||||||
self.sign_ups = _SignUps()
|
self.sign_ups = _SignUps()
|
||||||
|
self.groups = _Groups()
|
||||||
|
|
||||||
|
|
||||||
db = Database()
|
db = Database()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.orm import load_only
|
from sqlalchemy.orm import load_only
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
@ -11,11 +12,20 @@ class BaseDocument:
|
||||||
self.primary_key: str
|
self.primary_key: str
|
||||||
self.store: str
|
self.store: str
|
||||||
self.sql_model: SqlAlchemyBase
|
self.sql_model: SqlAlchemyBase
|
||||||
|
self.orm_mode = False
|
||||||
|
self.schema: BaseModel
|
||||||
|
|
||||||
# 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
|
self, session: Session, limit: int = None, order_by: str = None
|
||||||
) -> List[dict]:
|
) -> List[dict]:
|
||||||
|
|
||||||
|
if self.orm_mode:
|
||||||
|
return [
|
||||||
|
self.schema.from_orm(x)
|
||||||
|
for x in session.query(self.sql_model).limit(limit).all()
|
||||||
|
]
|
||||||
|
|
||||||
list = [x.dict() for x in session.query(self.sql_model).limit(limit).all()]
|
list = [x.dict() for x in session.query(self.sql_model).limit(limit).all()]
|
||||||
|
|
||||||
if limit == 1:
|
if limit == 1:
|
||||||
|
@ -105,6 +115,15 @@ class BaseDocument:
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.orm_mode:
|
||||||
|
if limit == 1:
|
||||||
|
try:
|
||||||
|
return self.schema.from_orm(result[0])
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
return [self.schema(x) for x in result]
|
||||||
|
|
||||||
db_entries = [x.dict() for x in result]
|
db_entries = [x.dict() for x in result]
|
||||||
|
|
||||||
if limit == 1:
|
if limit == 1:
|
||||||
|
@ -128,6 +147,10 @@ class BaseDocument:
|
||||||
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()
|
||||||
|
|
||||||
|
if self.orm_mode:
|
||||||
|
return self.schema.from_orm(new_document)
|
||||||
|
|
||||||
return_data = new_document.dict()
|
return_data = new_document.dict()
|
||||||
return return_data
|
return return_data
|
||||||
|
|
||||||
|
@ -145,9 +168,13 @@ class BaseDocument:
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
if self.orm_mode:
|
||||||
|
session.commit()
|
||||||
|
return self.schema.from_orm(entry)
|
||||||
|
|
||||||
return_data = entry.dict()
|
return_data = entry.dict()
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
return return_data
|
return return_data
|
||||||
|
|
||||||
def delete(self, session: Session, primary_key_value) -> dict:
|
def delete(self, session: Session, primary_key_value) -> dict:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from core.config import DEFAULT_GROUP
|
||||||
from core.security import get_password_hash
|
from core.security import get_password_hash
|
||||||
from fastapi.logger import logger
|
from fastapi.logger import logger
|
||||||
from schema.settings import SiteSettings, Webhooks
|
from schema.settings import SiteSettings, Webhooks
|
||||||
|
@ -12,6 +13,7 @@ def init_db(db: Session = None) -> None:
|
||||||
if not db:
|
if not db:
|
||||||
db = create_session()
|
db = create_session()
|
||||||
|
|
||||||
|
default_group_init(db)
|
||||||
default_settings_init(db)
|
default_settings_init(db)
|
||||||
default_theme_init(db)
|
default_theme_init(db)
|
||||||
default_user_init(db)
|
default_user_init(db)
|
||||||
|
@ -50,12 +52,19 @@ def default_settings_init(session: Session):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def default_group_init(session: Session):
|
||||||
|
default_group = {"name": DEFAULT_GROUP}
|
||||||
|
logger.info("Generating Default Group")
|
||||||
|
db.groups.create(session, default_group)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def default_user_init(session: Session):
|
def default_user_init(session: Session):
|
||||||
default_user = {
|
default_user = {
|
||||||
"full_name": "Change Me",
|
"full_name": "Change Me",
|
||||||
"email": "changeme@email.com",
|
"email": "changeme@email.com",
|
||||||
"password": get_password_hash("MyPassword"),
|
"password": get_password_hash("MyPassword"),
|
||||||
"family": "public",
|
"group": DEFAULT_GROUP,
|
||||||
"admin": True,
|
"admin": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,3 +4,4 @@ from db.models.settings import *
|
||||||
from db.models.theme import *
|
from db.models.theme import *
|
||||||
from db.models.users import *
|
from db.models.users import *
|
||||||
from db.models.sign_up import *
|
from db.models.sign_up import *
|
||||||
|
from db.models.group import *
|
||||||
|
|
|
@ -1,41 +1,31 @@
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlalchemy.orm as orm
|
||||||
|
from core.config import DEFAULT_GROUP
|
||||||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
from sqlalchemy import Boolean, Column, Integer, String
|
from fastapi.logger import logger
|
||||||
|
from slugify import slugify
|
||||||
|
|
||||||
|
|
||||||
class Group(SqlAlchemyBase, BaseMixins):
|
class Group(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "groups"
|
__tablename__ = "groups"
|
||||||
id = Column(Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
name = Column(String, index=True)
|
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
|
||||||
slug = Column(String, unique=True, index=True)
|
users = orm.relationship("User", back_populates="group")
|
||||||
|
mealplans = orm.relationship("MealPlanModel", back_populates="group")
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, name, session=None) -> None:
|
||||||
self,
|
self.name = name
|
||||||
session,
|
|
||||||
full_name,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
family="public",
|
|
||||||
admin=False,
|
|
||||||
) -> None:
|
|
||||||
self.full_name = full_name
|
|
||||||
self.email = email
|
|
||||||
self.family = family
|
|
||||||
self.admin = admin
|
|
||||||
self.password = password
|
|
||||||
|
|
||||||
def dict(self):
|
|
||||||
return {
|
|
||||||
"id": self.id,
|
|
||||||
"full_name": self.full_name,
|
|
||||||
"email": self.email,
|
|
||||||
"admin": self.admin,
|
|
||||||
"family": self.family,
|
|
||||||
"password": self.password,
|
|
||||||
}
|
|
||||||
|
|
||||||
def update(self, full_name, email, family, admin, session=None):
|
|
||||||
self.full_name = full_name
|
|
||||||
self.email = email
|
|
||||||
self.family = family
|
|
||||||
self.admin = admin
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_if_not_exist(session, name: str = DEFAULT_GROUP):
|
||||||
|
try:
|
||||||
|
result = session.query(Group).filter(Group.name == name).one()
|
||||||
|
if result:
|
||||||
|
logger.info("Category exists, associating recipe")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
logger.info("Category doesn't exists, creating tag")
|
||||||
|
return Group(name=name)
|
||||||
|
except:
|
||||||
|
logger.info("Category doesn't exists, creating category")
|
||||||
|
return Group(name=name)
|
||||||
|
|
|
@ -46,6 +46,8 @@ class MealPlanModel(SqlAlchemyBase, BaseMixins):
|
||||||
startDate = sa.Column(sa.Date)
|
startDate = sa.Column(sa.Date)
|
||||||
endDate = sa.Column(sa.Date)
|
endDate = sa.Column(sa.Date)
|
||||||
meals: List[Meal] = orm.relation(Meal)
|
meals: List[Meal] = orm.relation(Meal)
|
||||||
|
group_id = sa.Column(sa.String, sa.ForeignKey("groups.id"))
|
||||||
|
group = orm.relationship("Group", back_populates="mealplans")
|
||||||
|
|
||||||
def __init__(self, startDate, endDate, meals, uid=None, session=None) -> None:
|
def __init__(self, startDate, endDate, meals, uid=None, session=None) -> None:
|
||||||
self.startDate = startDate
|
self.startDate = startDate
|
||||||
|
|
|
@ -6,12 +6,12 @@ class Nutrition(SqlAlchemyBase):
|
||||||
__tablename__ = "recipe_nutrition"
|
__tablename__ = "recipe_nutrition"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id"))
|
||||||
calories = sa.Column(sa.Integer)
|
calories = sa.Column(sa.String)
|
||||||
fatContent = sa.Column(sa.Integer)
|
fatContent = sa.Column(sa.String)
|
||||||
fiberContent = sa.Column(sa.Integer)
|
fiberContent = sa.Column(sa.String)
|
||||||
proteinContent = sa.Column(sa.Integer)
|
proteinContent = sa.Column(sa.String)
|
||||||
sodiumContent = sa.Column(sa.Integer)
|
sodiumContent = sa.Column(sa.String)
|
||||||
sugarContent = sa.Column(sa.Integer)
|
sugarContent = sa.Column(sa.String)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
from core.config import DEFAULT_GROUP
|
||||||
|
from db.models.group import Group
|
||||||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
from sqlalchemy import Boolean, Column, Integer, String
|
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
||||||
|
|
||||||
|
|
||||||
class User(SqlAlchemyBase, BaseMixins):
|
class User(SqlAlchemyBase, BaseMixins):
|
||||||
|
@ -8,9 +10,9 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
full_name = Column(String, index=True)
|
full_name = Column(String, index=True)
|
||||||
email = Column(String, unique=True, index=True)
|
email = Column(String, unique=True, index=True)
|
||||||
password = Column(String)
|
password = Column(String)
|
||||||
is_active = Column(Boolean(), default=True)
|
group_id = Column(String, ForeignKey("groups.id"))
|
||||||
family = Column(String)
|
group = orm.relationship("Group", back_populates="users")
|
||||||
admin = Column(Boolean(), default=False)
|
admin = Column(Boolean, default=False)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -18,29 +20,21 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||||
full_name,
|
full_name,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
family="public",
|
group: str = DEFAULT_GROUP,
|
||||||
admin=False,
|
admin=False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
group = group if group else DEFAULT_GROUP
|
||||||
self.full_name = full_name
|
self.full_name = full_name
|
||||||
self.email = email
|
self.email = email
|
||||||
self.family = family
|
self.group = Group.create_if_not_exist(session, group)
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
def dict(self):
|
def update(self, full_name, email, group, admin, session=None):
|
||||||
return {
|
|
||||||
"id": self.id,
|
|
||||||
"full_name": self.full_name,
|
|
||||||
"email": self.email,
|
|
||||||
"admin": self.admin,
|
|
||||||
"family": self.family,
|
|
||||||
"password": self.password,
|
|
||||||
}
|
|
||||||
|
|
||||||
def update(self, full_name, email, family, admin, session=None):
|
|
||||||
self.full_name = full_name
|
self.full_name = full_name
|
||||||
self.email = email
|
self.email = email
|
||||||
self.family = family
|
self.group = Group.create_if_not_exist(session, group)
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
|
|
||||||
def update_password(self, password):
|
def update_password(self, password):
|
||||||
|
|
|
@ -20,5 +20,5 @@ def query_user(user_email: str, session: Session = None) -> UserInDB:
|
||||||
session = session if session else create_session()
|
session = session if session else create_session()
|
||||||
user = db.users.get(session, user_email, "email")
|
user = db.users.get(session, user_email, "email")
|
||||||
session.close()
|
session.close()
|
||||||
return UserInDB(**user)
|
return user
|
||||||
|
|
||||||
|
|
24
mealie/routes/groups/crud.py
Normal file
24
mealie/routes/groups/crud.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import shutil
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from core.config import USER_DIR
|
||||||
|
from core.security import get_password_hash, verify_password
|
||||||
|
from db.database import db
|
||||||
|
from db.db_setup import generate_session
|
||||||
|
from fastapi import APIRouter, Depends, File, UploadFile
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from routes.deps import manager
|
||||||
|
from schema.snackbar import SnackResponse
|
||||||
|
from schema.user import GroupBase, GroupInDB
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/groups", tags=["Groups"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=list[GroupInDB])
|
||||||
|
async def get_all_groups(
|
||||||
|
current_user=Depends(manager),
|
||||||
|
session: Session = Depends(generate_session),
|
||||||
|
):
|
||||||
|
""" Returns a list of all groups in the database """
|
||||||
|
return db.groups.get_all(session)
|
7
mealie/routes/groups/groups.py
Normal file
7
mealie/routes/groups/groups.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from routes.groups import crud
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(crud.router)
|
||||||
|
|
|
@ -66,8 +66,8 @@ async def update_user(
|
||||||
):
|
):
|
||||||
|
|
||||||
if current_user.id == id or current_user.admin:
|
if current_user.id == id or current_user.admin:
|
||||||
updated_user = db.users.update(session, id, new_data.dict())
|
updated_user: UserInDB = db.users.update(session, id, new_data.dict())
|
||||||
email = updated_user.get("email")
|
email = updated_user.email
|
||||||
if current_user.id == id:
|
if current_user.id == id:
|
||||||
access_token = manager.create_access_token(
|
access_token = manager.create_access_token(
|
||||||
data=dict(sub=email), expires=timedelta(hours=2)
|
data=dict(sub=email), expires=timedelta(hours=2)
|
||||||
|
|
|
@ -15,12 +15,12 @@ class RecipeStep(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Nutrition(BaseModel):
|
class Nutrition(BaseModel):
|
||||||
calories: Optional[int]
|
calories: Optional[str]
|
||||||
fatContent: Optional[int]
|
fatContent: Optional[str]
|
||||||
fiberContent: Optional[int]
|
fiberContent: Optional[str]
|
||||||
proteinContent: Optional[int]
|
proteinContent: Optional[str]
|
||||||
sodiumContent: Optional[int]
|
sodiumContent: Optional[str]
|
||||||
sugarContent: Optional[int]
|
sugarContent: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class Recipe(BaseModel):
|
class Recipe(BaseModel):
|
||||||
|
|
|
@ -1,26 +1,35 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from core.config import DEFAULT_GROUP
|
||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
|
|
||||||
# from pydantic import EmailStr
|
|
||||||
|
|
||||||
|
|
||||||
class ChangePassword(CamelModel):
|
class ChangePassword(CamelModel):
|
||||||
current_password: str
|
current_password: str
|
||||||
new_password: str
|
new_password: str
|
||||||
|
|
||||||
|
|
||||||
|
class GroupBase(CamelModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
class UserBase(CamelModel):
|
class UserBase(CamelModel):
|
||||||
full_name: Optional[str] = None
|
full_name: Optional[str] = None
|
||||||
email: str
|
email: str
|
||||||
family: str
|
|
||||||
admin: bool
|
admin: bool
|
||||||
|
group: Optional[str]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
schema_extra = {
|
schema_extra = {
|
||||||
"fullName": "Change Me",
|
"fullName": "Change Me",
|
||||||
"email": "changeme@email.com",
|
"email": "changeme@email.com",
|
||||||
"family": "public",
|
"group": DEFAULT_GROUP,
|
||||||
"admin": "false",
|
"admin": "false",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +40,24 @@ class UserIn(UserBase):
|
||||||
|
|
||||||
class UserOut(UserBase):
|
class UserOut(UserBase):
|
||||||
id: int
|
id: int
|
||||||
|
group: GroupBase
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
class UserInDB(UserIn, UserOut):
|
class UserInDB(UserOut):
|
||||||
|
password: str
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class GroupInDB(GroupBase):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
users: Optional[list[UserOut]]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
|
@ -20,12 +20,12 @@ class RecipeStep(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class Nutrition(BaseModel):
|
class Nutrition(BaseModel):
|
||||||
calories: Optional[int]
|
calories: Optional[str]
|
||||||
fatContent: Optional[int]
|
fatContent: Optional[str]
|
||||||
fiberContent: Optional[int]
|
fiberContent: Optional[str]
|
||||||
proteinContent: Optional[int]
|
proteinContent: Optional[str]
|
||||||
sodiumContent: Optional[int]
|
sodiumContent: Optional[str]
|
||||||
sugarContent: Optional[int]
|
sugarContent: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class Recipe(BaseModel):
|
class Recipe(BaseModel):
|
||||||
|
|
|
@ -31,6 +31,9 @@ class Cleaner:
|
||||||
recipe_data["prepTime"] = Cleaner.time(recipe_data.get("prepTime", None))
|
recipe_data["prepTime"] = Cleaner.time(recipe_data.get("prepTime", None))
|
||||||
recipe_data["performTime"] = Cleaner.time(recipe_data.get("performTime", None))
|
recipe_data["performTime"] = Cleaner.time(recipe_data.get("performTime", None))
|
||||||
recipe_data["totalTime"] = Cleaner.time(recipe_data.get("totalTime", None))
|
recipe_data["totalTime"] = Cleaner.time(recipe_data.get("totalTime", None))
|
||||||
|
recipe_data["recipeCategory"] = Cleaner.category(
|
||||||
|
recipe_data.get("recipeCategory", [])
|
||||||
|
)
|
||||||
|
|
||||||
recipe_data["recipeYield"] = Cleaner.yield_amount(
|
recipe_data["recipeYield"] = Cleaner.yield_amount(
|
||||||
recipe_data.get("recipeYield")
|
recipe_data.get("recipeYield")
|
||||||
|
@ -47,6 +50,13 @@ class Cleaner:
|
||||||
|
|
||||||
return recipe_data
|
return recipe_data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def category(category: str):
|
||||||
|
if type(category) == type(str):
|
||||||
|
return [category]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def html(raw_html):
|
def html(raw_html):
|
||||||
cleanr = re.compile("<.*?>")
|
cleanr = re.compile("<.*?>")
|
||||||
|
|
|
@ -34,7 +34,7 @@ def api_client():
|
||||||
|
|
||||||
yield TestClient(app)
|
yield TestClient(app)
|
||||||
|
|
||||||
SQLITE_FILE.unlink()
|
# SQLITE_FILE.unlink()
|
||||||
|
|
||||||
|
|
||||||
@fixture(scope="session")
|
@fixture(scope="session")
|
||||||
|
|
|
@ -63,7 +63,7 @@ def test_read_update(api_client, recipe_data):
|
||||||
recipe["notes"] = test_notes
|
recipe["notes"] = test_notes
|
||||||
|
|
||||||
test_categories = ["one", "two", "three"]
|
test_categories = ["one", "two", "three"]
|
||||||
recipe["categories"] = test_categories
|
recipe["recipeCategory"] = test_categories
|
||||||
|
|
||||||
response = api_client.put(
|
response = api_client.put(
|
||||||
f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe
|
f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe
|
||||||
|
@ -77,7 +77,7 @@ def test_read_update(api_client, recipe_data):
|
||||||
recipe = json.loads(response.content)
|
recipe = json.loads(response.content)
|
||||||
|
|
||||||
assert recipe["notes"] == test_notes
|
assert recipe["notes"] == test_notes
|
||||||
assert recipe["categories"].sort() == test_categories.sort()
|
assert recipe["recipeCategory"].sort() == test_categories.sort()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("recipe_data", recipe_test_data)
|
@pytest.mark.parametrize("recipe_data", recipe_test_data)
|
||||||
|
|
|
@ -16,7 +16,9 @@ def default_user():
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"fullName": "Change Me",
|
"fullName": "Change Me",
|
||||||
"email": "changeme@email.com",
|
"email": "changeme@email.com",
|
||||||
"family": "public",
|
"group": {
|
||||||
|
"name": "home"
|
||||||
|
},
|
||||||
"admin": True
|
"admin": True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +29,9 @@ def new_user():
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"fullName": "My New User",
|
"fullName": "My New User",
|
||||||
"email": "newuser@email.com",
|
"email": "newuser@email.com",
|
||||||
"family": "public",
|
"group": {
|
||||||
|
"name": "home"
|
||||||
|
},
|
||||||
"admin": False
|
"admin": False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +58,7 @@ def test_create_user(api_client: requests, token, new_user):
|
||||||
"fullName": "My New User",
|
"fullName": "My New User",
|
||||||
"email": "newuser@email.com",
|
"email": "newuser@email.com",
|
||||||
"password": "MyStrongPassword",
|
"password": "MyStrongPassword",
|
||||||
"family": "public",
|
"group": "home",
|
||||||
"admin": False
|
"admin": False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +82,7 @@ def test_update_user(api_client: requests, token):
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"fullName": "Updated Name",
|
"fullName": "Updated Name",
|
||||||
"email": "updated@email.com",
|
"email": "updated@email.com",
|
||||||
"family": "public",
|
"group": "home",
|
||||||
"admin": True
|
"admin": True
|
||||||
}
|
}
|
||||||
response = api_client.put(f"{BASE}/1", headers=token, json=update_data)
|
response = api_client.put(f"{BASE}/1", headers=token, json=update_data)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue