mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-12 21:35:18 -05:00
Refactor/user database models (#775)
* fix build error * drop frontend.old * improve auto_init decorator * purge depreciated site settings * formatting * update init function * fix(backend): 🐛 Fix password reset bug Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
@@ -14,11 +14,9 @@ from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUn
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from mealie.db.models.recipe.tag import Tag
|
||||
from mealie.db.models.server.task import ServerTaskModel
|
||||
from mealie.db.models.settings import SiteSettings
|
||||
from mealie.db.models.sign_up import SignUp
|
||||
from mealie.db.models.users import LongLiveToken, User
|
||||
from mealie.db.models.users.password_reset import PasswordResetModel
|
||||
from mealie.schema.admin import SiteSettings as SiteSettingsSchema
|
||||
from mealie.schema.cookbook.cookbook import ReadCookBook
|
||||
from mealie.schema.events import Event as EventSchema
|
||||
from mealie.schema.events import EventNotificationIn
|
||||
@@ -94,10 +92,6 @@ class Database:
|
||||
# ================================================================
|
||||
# Site Items
|
||||
|
||||
@cached_property
|
||||
def settings(self) -> AccessModel[SiteSettingsSchema, SiteSettings]:
|
||||
return AccessModel(self.session, pk_id, SiteSettings, SiteSettingsSchema)
|
||||
|
||||
@cached_property
|
||||
def sign_up(self) -> AccessModel[SignUpOut, SignUp]:
|
||||
return AccessModel(self.session, pk_id, SignUp, SignUpOut)
|
||||
|
||||
@@ -6,7 +6,6 @@ from mealie.db.data_initialization.init_users import default_user_init
|
||||
from mealie.db.database import get_database
|
||||
from mealie.db.db_setup import create_session, engine
|
||||
from mealie.db.models._model_base import SqlAlchemyBase
|
||||
from mealie.schema.admin import SiteSettings
|
||||
from mealie.schema.user.user import GroupBase
|
||||
from mealie.services.events import create_general_event
|
||||
from mealie.services.group_services.group_utils import create_new_group
|
||||
@@ -24,16 +23,10 @@ def create_all_models():
|
||||
|
||||
def init_db(db: Database) -> None:
|
||||
default_group_init(db)
|
||||
default_settings_init(db)
|
||||
default_user_init(db)
|
||||
default_recipe_unit_init(db)
|
||||
|
||||
|
||||
def default_settings_init(db: Database):
|
||||
document = db.settings.create(SiteSettings().dict())
|
||||
logger.info(f"Created Site Settings: \n {document}")
|
||||
|
||||
|
||||
def default_group_init(db: Database):
|
||||
logger.info("Generating Default Group")
|
||||
create_new_group(db, GroupBase(name=settings.DEFAULT_GROUP))
|
||||
|
||||
@@ -2,6 +2,5 @@ from .event import *
|
||||
from .group import *
|
||||
from .recipe.recipe import *
|
||||
from .server import *
|
||||
from .settings import *
|
||||
from .sign_up import *
|
||||
from .users import *
|
||||
|
||||
@@ -19,9 +19,6 @@ class BaseMixins:
|
||||
`cls.get_ref` method which will return the object from the database or none. Useful for many-to-many relationships.
|
||||
"""
|
||||
|
||||
class Config:
|
||||
get_attr = "id"
|
||||
|
||||
def update(self, *args, **kwarg):
|
||||
self.__init__(*args, **kwarg)
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
from functools import wraps
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY
|
||||
|
||||
|
||||
def handle_one_to_many_list(get_attr, relation_cls, all_elements: list[dict]):
|
||||
elems_to_create = []
|
||||
updated_elems = []
|
||||
|
||||
for elem in all_elements:
|
||||
elem_id = elem.get(get_attr, None)
|
||||
|
||||
existing_elem = relation_cls.get_ref(match_value=elem_id)
|
||||
|
||||
if existing_elem is None:
|
||||
|
||||
elems_to_create.append(elem)
|
||||
|
||||
else:
|
||||
for key, value in elem.items():
|
||||
setattr(existing_elem, key, value)
|
||||
|
||||
updated_elems.append(existing_elem)
|
||||
|
||||
new_elems = []
|
||||
for elem in elems_to_create:
|
||||
new_elems = [relation_cls(**elem) for elem in all_elements]
|
||||
|
||||
return new_elems
|
||||
|
||||
|
||||
def auto_init(exclude: Union[set, list] = None): # sourcery no-metrics
|
||||
"""Wraps the `__init__` method of a class to automatically set the common
|
||||
attributes.
|
||||
|
||||
Args:
|
||||
exclude (Union[set, list], optional): [description]. Defaults to None.
|
||||
"""
|
||||
|
||||
exclude = exclude or set()
|
||||
exclude.add("id")
|
||||
|
||||
def decorator(init):
|
||||
@wraps(init)
|
||||
def wrapper(self, *args, **kwargs): # sourcery no-metrics
|
||||
"""
|
||||
Custom initializer that allows nested children initialization.
|
||||
Only keys that are present as instance's class attributes are allowed.
|
||||
These could be, for example, any mapped columns or relationships.
|
||||
|
||||
Code inspired from GitHub.
|
||||
Ref: https://github.com/tiangolo/fastapi/issues/2194
|
||||
"""
|
||||
cls = self.__class__
|
||||
model_columns = self.__mapper__.columns
|
||||
relationships = self.__mapper__.relationships
|
||||
|
||||
session = kwargs.get("session", None)
|
||||
|
||||
for key, val in kwargs.items():
|
||||
if key in exclude:
|
||||
continue
|
||||
|
||||
if not hasattr(cls, key):
|
||||
continue
|
||||
# raise TypeError(f"Invalid keyword argument: {key}")
|
||||
|
||||
if key in model_columns:
|
||||
setattr(self, key, val)
|
||||
continue
|
||||
|
||||
if key in relationships:
|
||||
relation_dir = relationships[key].direction.name
|
||||
relation_cls = relationships[key].mapper.entity
|
||||
use_list = relationships[key].uselist
|
||||
|
||||
try:
|
||||
get_attr = relation_cls.Config.get_attr
|
||||
if get_attr is None:
|
||||
get_attr = "id"
|
||||
except Exception:
|
||||
get_attr = "id"
|
||||
|
||||
if relation_dir == ONETOMANY.name and use_list:
|
||||
instances = handle_one_to_many_list(get_attr, relation_cls, val)
|
||||
setattr(self, key, instances)
|
||||
|
||||
if relation_dir == ONETOMANY.name and not use_list:
|
||||
instance = relation_cls(**val)
|
||||
setattr(self, key, instance)
|
||||
|
||||
elif relation_dir == MANYTOONE.name and not use_list:
|
||||
if isinstance(val, dict):
|
||||
val = val.get(get_attr)
|
||||
|
||||
if val is None:
|
||||
raise ValueError(f"Expected 'id' to be provided for {key}")
|
||||
|
||||
if isinstance(val, (str, int)):
|
||||
instance = relation_cls.get_ref(match_value=val, session=session)
|
||||
setattr(self, key, instance)
|
||||
|
||||
elif relation_dir == MANYTOMANY.name:
|
||||
|
||||
if not isinstance(val, list):
|
||||
raise ValueError(f"Expected many to many input to be of type list for {key}")
|
||||
|
||||
if len(val) > 0 and isinstance(val[0], dict):
|
||||
val = [elem.get(get_attr) for elem in val]
|
||||
|
||||
instances = [x for x in [relation_cls.get_ref(elem, session=session) for elem in val] if x]
|
||||
setattr(self, key, instances)
|
||||
|
||||
return init(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
1
mealie/db/models/_model_utils/__init__.py
Normal file
1
mealie/db/models/_model_utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .auto_init import auto_init
|
||||
184
mealie/db/models/_model_utils/auto_init.py
Normal file
184
mealie/db/models/_model_utils/auto_init.py
Normal file
@@ -0,0 +1,184 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY, Session
|
||||
from sqlalchemy.orm.decl_api import DeclarativeMeta
|
||||
from sqlalchemy.orm.mapper import Mapper
|
||||
from sqlalchemy.orm.relationships import RelationshipProperty
|
||||
from sqlalchemy.sql.base import ColumnCollection
|
||||
from sqlalchemy.util._collections import ImmutableProperties
|
||||
|
||||
from .helpers import safe_call
|
||||
|
||||
|
||||
def _default_exclusion() -> set[str]:
|
||||
return {"id"}
|
||||
|
||||
|
||||
class AutoInitConfig(BaseModel):
|
||||
"""
|
||||
Config class for `auto_init` decorator.
|
||||
"""
|
||||
|
||||
get_attr: str = None
|
||||
exclude: set = Field(default_factory=_default_exclusion)
|
||||
# auto_create: bool = False
|
||||
|
||||
|
||||
def _get_config(relation_cls: DeclarativeMeta) -> AutoInitConfig:
|
||||
"""
|
||||
Returns the config for the given class.
|
||||
"""
|
||||
cfg = AutoInitConfig()
|
||||
cfgKeys = cfg.dict().keys()
|
||||
# Get the config for the class
|
||||
try:
|
||||
class_config: AutoInitConfig = relation_cls.Config
|
||||
except AttributeError:
|
||||
return cfg
|
||||
# Map all matching attributes in Config to all AutoInitConfig attributes
|
||||
for attr in dir(class_config):
|
||||
if attr in cfgKeys:
|
||||
setattr(cfg, attr, getattr(class_config, attr))
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
def get_lookup_attr(relation_cls: DeclarativeMeta) -> str:
|
||||
"""Returns the primary key attribute of the related class as a string.
|
||||
|
||||
Args:
|
||||
relation_cls (DeclarativeMeta): The SQLAlchemy class to get the primary_key from
|
||||
|
||||
Returns:
|
||||
Any: [description]
|
||||
"""
|
||||
|
||||
cfg = _get_config(relation_cls)
|
||||
|
||||
try:
|
||||
get_attr = cfg.get_attr
|
||||
if get_attr is None:
|
||||
get_attr = relation_cls.__table__.primary_key.columns.keys()[0]
|
||||
except Exception:
|
||||
get_attr = "id"
|
||||
return get_attr
|
||||
|
||||
|
||||
def handle_many_to_many(session, get_attr, relation_cls, all_elements: list[dict]):
|
||||
"""
|
||||
Proxy call to `handle_one_to_many_list` for many-to-many relationships. Because functionally, they do the same
|
||||
"""
|
||||
return handle_one_to_many_list(session, get_attr, relation_cls, all_elements)
|
||||
|
||||
|
||||
def handle_one_to_many_list(session: Session, get_attr, relation_cls, all_elements: list[dict]):
|
||||
elems_to_create: list[dict] = []
|
||||
updated_elems: list[dict] = []
|
||||
|
||||
for elem in all_elements:
|
||||
elem_id = elem.get(get_attr, None)
|
||||
|
||||
existing_elem = session.query(relation_cls).filter_by(**{get_attr: elem_id}).one_or_none()
|
||||
|
||||
if existing_elem is None:
|
||||
elems_to_create.append(elem)
|
||||
|
||||
else:
|
||||
for key, value in elem.items():
|
||||
setattr(existing_elem, key, value)
|
||||
|
||||
updated_elems.append(existing_elem)
|
||||
|
||||
new_elems = [safe_call(relation_cls, elem) for elem in elems_to_create]
|
||||
return new_elems + updated_elems
|
||||
|
||||
|
||||
def auto_init(): # sourcery no-metrics
|
||||
"""Wraps the `__init__` method of a class to automatically set the common
|
||||
attributes.
|
||||
|
||||
Args:
|
||||
exclude (Union[set, list], optional): [description]. Defaults to None.
|
||||
"""
|
||||
|
||||
def decorator(init):
|
||||
@wraps(init)
|
||||
def wrapper(self: DeclarativeMeta, *args, **kwargs): # sourcery no-metrics
|
||||
"""
|
||||
Custom initializer that allows nested children initialization.
|
||||
Only keys that are present as instance's class attributes are allowed.
|
||||
These could be, for example, any mapped columns or relationships.
|
||||
|
||||
Code inspired from GitHub.
|
||||
Ref: https://github.com/tiangolo/fastapi/issues/2194
|
||||
"""
|
||||
cls = self.__class__
|
||||
|
||||
exclude = _get_config(cls).exclude
|
||||
|
||||
alchemy_mapper: Mapper = self.__mapper__
|
||||
model_columns: ColumnCollection = alchemy_mapper.columns
|
||||
relationships: ImmutableProperties = alchemy_mapper.relationships
|
||||
|
||||
session = kwargs.get("session", None)
|
||||
|
||||
if session is None:
|
||||
raise ValueError("Session is required to initialize the model with `auto_init`")
|
||||
|
||||
for key, val in kwargs.items():
|
||||
if key in exclude:
|
||||
continue
|
||||
|
||||
if not hasattr(cls, key):
|
||||
continue
|
||||
# raise TypeError(f"Invalid keyword argument: {key}")
|
||||
|
||||
if key in model_columns:
|
||||
setattr(self, key, val)
|
||||
continue
|
||||
|
||||
if key in relationships:
|
||||
prop: RelationshipProperty = relationships[key]
|
||||
|
||||
# Identifies the type of relationship (ONETOMANY, MANYTOONE, many-to-one, many-to-many)
|
||||
relation_dir = prop.direction
|
||||
|
||||
# Identifies the parent class of the related object.
|
||||
relation_cls: DeclarativeMeta = prop.mapper.entity
|
||||
|
||||
# Identifies if the relationship was declared with use_list=True
|
||||
use_list: bool = prop.uselist
|
||||
|
||||
get_attr = get_lookup_attr(relation_cls)
|
||||
|
||||
if relation_dir == ONETOMANY and use_list:
|
||||
instances = handle_one_to_many_list(session, get_attr, relation_cls, val)
|
||||
setattr(self, key, instances)
|
||||
|
||||
elif relation_dir == ONETOMANY:
|
||||
instance = safe_call(relation_cls, val)
|
||||
setattr(self, key, instance)
|
||||
|
||||
elif relation_dir == MANYTOONE and not use_list:
|
||||
if isinstance(val, dict):
|
||||
val = val.get(get_attr)
|
||||
|
||||
if val is None:
|
||||
raise ValueError(f"Expected 'id' to be provided for {key}")
|
||||
|
||||
if isinstance(val, (str, int)):
|
||||
instance = session.query(relation_cls).filter_by(**{get_attr: val}).one_or_none()
|
||||
setattr(self, key, instance)
|
||||
|
||||
elif relation_dir == MANYTOMANY:
|
||||
instances = handle_many_to_many(session, get_attr, relation_cls, val)
|
||||
setattr(self, key, instances)
|
||||
|
||||
return init(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
39
mealie/db/models/_model_utils/helpers.py
Normal file
39
mealie/db/models/_model_utils/helpers.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import inspect
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
def get_valid_call(func: Callable, args_dict) -> dict:
|
||||
"""
|
||||
Returns a dictionary of valid arguemnts for the supplied function. if kwargs are accepted,
|
||||
the original dictionary will be returned.
|
||||
"""
|
||||
|
||||
def get_valid_args(func: Callable) -> tuple:
|
||||
"""
|
||||
Returns a tuple of valid arguemnts for the supplied function.
|
||||
"""
|
||||
return inspect.getfullargspec(func).args
|
||||
|
||||
def accepts_kwargs(func: Callable) -> bool:
|
||||
"""
|
||||
Returns True if the function accepts keyword arguments.
|
||||
"""
|
||||
return inspect.getfullargspec(func).varkw is not None
|
||||
|
||||
if accepts_kwargs(func):
|
||||
return args_dict
|
||||
|
||||
valid_args = get_valid_args(func)
|
||||
|
||||
return {k: v for k, v in args_dict.items() if k in valid_args}
|
||||
|
||||
|
||||
def safe_call(func, dict) -> Any:
|
||||
"""
|
||||
Safely calls the supplied function with the supplied dictionary of arguments.
|
||||
by removing any invalid arguments.
|
||||
"""
|
||||
try:
|
||||
return func(**get_valid_call(func, dict))
|
||||
except TypeError:
|
||||
return func(**dict)
|
||||
@@ -46,7 +46,10 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||
server_tasks = orm.relationship(ServerTaskModel, back_populates="group", single_parent=True)
|
||||
shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True)
|
||||
|
||||
@auto_init({"users", "webhooks", "shopping_lists", "cookbooks", "preferences", "invite_tokens", "mealplans"})
|
||||
class Config:
|
||||
exclude = {"users", "webhooks", "shopping_lists", "cookbooks", "preferences", "invite_tokens", "mealplans"}
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@@ -11,12 +11,7 @@ class RecipeAsset(SqlAlchemyBase):
|
||||
icon = sa.Column(sa.String)
|
||||
file_name = sa.Column(sa.String)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name=None,
|
||||
icon=None,
|
||||
file_name=None,
|
||||
) -> None:
|
||||
def __init__(self, name=None, icon=None, file_name=None) -> None:
|
||||
self.name = name
|
||||
self.file_name = file_name
|
||||
self.icon = icon
|
||||
|
||||
@@ -8,12 +8,6 @@ from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
|
||||
logger = root_logger.get_logger()
|
||||
|
||||
site_settings2categories = sa.Table(
|
||||
"site_settings2categories",
|
||||
SqlAlchemyBase.metadata,
|
||||
sa.Column("site_settings.id", sa.Integer, sa.ForeignKey("site_settings.id")),
|
||||
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
|
||||
)
|
||||
|
||||
group2categories = sa.Table(
|
||||
"group2categories",
|
||||
|
||||
@@ -86,14 +86,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
class Config:
|
||||
get_attr = "slug"
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert name != ""
|
||||
return name
|
||||
|
||||
@auto_init(
|
||||
{
|
||||
exclude = {
|
||||
"assets",
|
||||
"extras",
|
||||
"notes",
|
||||
@@ -103,7 +96,13 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
"settings",
|
||||
"tools",
|
||||
}
|
||||
)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert name != ""
|
||||
return name
|
||||
|
||||
@auto_init()
|
||||
def __init__(
|
||||
self,
|
||||
session,
|
||||
@@ -115,7 +114,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
recipe_instructions: list[dict] = None,
|
||||
settings: dict = None,
|
||||
tools: list[str] = None,
|
||||
**_
|
||||
**_,
|
||||
) -> None:
|
||||
self.nutrition = Nutrition(**nutrition) if nutrition else Nutrition()
|
||||
self.tools = [Tool(tool=x) for x in tools] if tools else []
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models.recipe.category import Category, site_settings2categories
|
||||
|
||||
|
||||
class SiteSettings(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "site_settings"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
language = sa.Column(sa.String)
|
||||
first_day_of_week = sa.Column(sa.Integer)
|
||||
categories = orm.relationship("Category", secondary=site_settings2categories, single_parent=True)
|
||||
show_recent = sa.Column(sa.Boolean, default=True)
|
||||
cards_per_section = sa.Column(sa.Integer)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: Session = None,
|
||||
language="en",
|
||||
first_day_of_week: int = 0,
|
||||
categories: list = [],
|
||||
show_recent=True,
|
||||
cards_per_section: int = 9,
|
||||
) -> None:
|
||||
session.commit()
|
||||
self.language = language
|
||||
self.first_day_of_week = first_day_of_week
|
||||
self.cards_per_section = cards_per_section
|
||||
self.show_recent = show_recent
|
||||
self.categories = [Category.get_ref(session=session, slug=cat.get("slug")) for cat in categories]
|
||||
|
||||
def update(self, *args, **kwarg):
|
||||
self.__init__(*args, **kwarg)
|
||||
@@ -11,7 +11,6 @@ settings = get_app_settings()
|
||||
|
||||
class LongLiveToken(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "long_live_tokens"
|
||||
id = Column(Integer, primary_key=True)
|
||||
parent_id = Column(Integer, ForeignKey("users.id"))
|
||||
name = Column(String, nullable=False)
|
||||
token = Column(String, nullable=False)
|
||||
@@ -25,7 +24,6 @@ class LongLiveToken(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
class User(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "users"
|
||||
id = Column(Integer, primary_key=True)
|
||||
full_name = Column(String, index=True)
|
||||
username = Column(String, index=True, unique=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
@@ -41,7 +39,6 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
can_invite = Column(Boolean, default=False)
|
||||
can_organize = Column(Boolean, default=False)
|
||||
|
||||
# Recipes
|
||||
tokens: list[LongLiveToken] = orm.relationship(
|
||||
LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
|
||||
)
|
||||
@@ -67,67 +64,52 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
password,
|
||||
favorite_recipes: list[str] = None,
|
||||
group: str = settings.DEFAULT_GROUP,
|
||||
admin=False,
|
||||
advanced=False,
|
||||
can_manage=False,
|
||||
can_invite=False,
|
||||
can_organize=False,
|
||||
**_
|
||||
**kwargs
|
||||
) -> None:
|
||||
|
||||
group = group or settings.DEFAULT_GROUP
|
||||
favorite_recipes = favorite_recipes or []
|
||||
self.group = Group.get_ref(session, group)
|
||||
|
||||
self.full_name = full_name
|
||||
self.email = email
|
||||
self.group = Group.get_ref(session, group)
|
||||
self.admin = admin
|
||||
self.password = password
|
||||
self.advanced = advanced
|
||||
|
||||
if self.admin:
|
||||
self.can_manage = True
|
||||
self.can_invite = True
|
||||
self.can_organize = True
|
||||
else:
|
||||
self.can_manage = can_manage
|
||||
self.can_invite = can_invite
|
||||
self.can_organize = can_organize
|
||||
|
||||
self.favorite_recipes = []
|
||||
|
||||
if self.username is None:
|
||||
self.username = full_name
|
||||
|
||||
def update(
|
||||
self,
|
||||
full_name,
|
||||
email,
|
||||
group,
|
||||
admin,
|
||||
username,
|
||||
session=None,
|
||||
favorite_recipes=None,
|
||||
password=None,
|
||||
advanced=False,
|
||||
can_manage=False,
|
||||
can_invite=False,
|
||||
can_organize=False,
|
||||
**_
|
||||
):
|
||||
self._set_permissions(**kwargs)
|
||||
|
||||
def update(self, full_name, email, group, username, session=None, favorite_recipes=None, advanced=False, **kwargs):
|
||||
favorite_recipes = favorite_recipes or []
|
||||
self.username = username
|
||||
self.full_name = full_name
|
||||
self.email = email
|
||||
|
||||
self.group = Group.get_ref(session, group)
|
||||
self.admin = admin
|
||||
self.advanced = advanced
|
||||
|
||||
if self.username is None:
|
||||
self.username = full_name
|
||||
|
||||
if password:
|
||||
self.password = password
|
||||
self._set_permissions(**kwargs)
|
||||
|
||||
def update_password(self, password):
|
||||
self.password = password
|
||||
|
||||
def _set_permissions(self, admin, can_manage=False, can_invite=False, can_organize=False, **_):
|
||||
"""Set user permissions based on the admin flag and the passed in kwargs
|
||||
|
||||
Args:
|
||||
admin (bool):
|
||||
can_manage (bool):
|
||||
can_invite (bool):
|
||||
can_organize (bool):
|
||||
"""
|
||||
self.admin = admin
|
||||
if self.admin:
|
||||
self.can_manage = True
|
||||
self.can_invite = True
|
||||
@@ -137,9 +119,6 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
self.can_invite = can_invite
|
||||
self.can_organize = can_organize
|
||||
|
||||
def update_password(self, password):
|
||||
self.password = password
|
||||
|
||||
@staticmethod
|
||||
def get_ref(session, id: str):
|
||||
return session.query(User).filter(User.id == id).one()
|
||||
|
||||
@@ -5,7 +5,6 @@ from mealie.core.dependencies import get_current_user
|
||||
from mealie.db.database import get_database
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.schema.admin import SiteSettings
|
||||
from mealie.schema.user import GroupInDB, PrivateUser
|
||||
from mealie.utils.post_webhooks import post_webhooks
|
||||
|
||||
@@ -21,16 +20,6 @@ def get_main_settings(session: Session = Depends(generate_session)):
|
||||
return db.settings.get(1)
|
||||
|
||||
|
||||
@admin_router.put("")
|
||||
def update_settings(
|
||||
data: SiteSettings,
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns Site Settings """
|
||||
db = get_database(session)
|
||||
db.settings.update(1, data.dict())
|
||||
|
||||
|
||||
@admin_router.post("/webhooks/test")
|
||||
def test_webhooks(
|
||||
current_user: PrivateUser = Depends(get_current_user),
|
||||
|
||||
@@ -24,10 +24,9 @@ async def reset_user_password(id: int, session: Session = Depends(generate_sessi
|
||||
db.users.update_password(id, new_password)
|
||||
|
||||
|
||||
@user_router.put("/{id}/password")
|
||||
@user_router.put("/{item_id}/password")
|
||||
def update_password(password_change: ChangePassword, user_service: UserService = Depends(UserService.write_existing)):
|
||||
""" Resets the User Password"""
|
||||
|
||||
return user_service.change_password(password_change)
|
||||
|
||||
|
||||
|
||||
@@ -4,31 +4,7 @@ from fastapi_camelcase import CamelModel
|
||||
from pydantic import validator
|
||||
from slugify import slugify
|
||||
|
||||
from ..recipe.recipe_category import CategoryBase, RecipeCategoryResponse
|
||||
|
||||
|
||||
class SiteSettings(CamelModel):
|
||||
language: str = "en-US"
|
||||
first_day_of_week: int = 0
|
||||
show_recent: bool = True
|
||||
cards_per_section: int = 9
|
||||
categories: Optional[list[CategoryBase]] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"language": "en",
|
||||
"firstDayOfWeek": 0,
|
||||
"showRecent": True,
|
||||
"categories": [
|
||||
{"id": 1, "name": "thanksgiving", "slug": "thanksgiving"},
|
||||
{"id": 2, "name": "homechef", "slug": "homechef"},
|
||||
{"id": 3, "name": "potatoes", "slug": "potatoes"},
|
||||
],
|
||||
}
|
||||
}
|
||||
from ..recipe.recipe_category import RecipeCategoryResponse
|
||||
|
||||
|
||||
class CustomPageBase(CamelModel):
|
||||
|
||||
@@ -11,15 +11,7 @@ from mealie.core.config import get_app_dirs
|
||||
|
||||
app_dirs = get_app_dirs()
|
||||
from mealie.db.database import get_database
|
||||
from mealie.schema.admin import (
|
||||
CommentImport,
|
||||
GroupImport,
|
||||
NotificationImport,
|
||||
RecipeImport,
|
||||
SettingsImport,
|
||||
SiteSettings,
|
||||
UserImport,
|
||||
)
|
||||
from mealie.schema.admin import CommentImport, GroupImport, NotificationImport, RecipeImport, UserImport
|
||||
from mealie.schema.events import EventNotificationIn
|
||||
from mealie.schema.recipe import CommentOut, Recipe
|
||||
from mealie.schema.user import PrivateUser, UpdateGroup
|
||||
@@ -181,19 +173,7 @@ class ImportDatabase:
|
||||
return import_notifications
|
||||
|
||||
def import_settings(self):
|
||||
settings_file = self.import_dir.joinpath("settings", "settings.json")
|
||||
settings = ImportDatabase.read_models_file(settings_file, SiteSettings)
|
||||
settings = settings[0]
|
||||
|
||||
try:
|
||||
self.db.settings.update(1, settings.dict())
|
||||
import_status = SettingsImport(name="Site Settings", status=True)
|
||||
|
||||
except Exception as inst:
|
||||
self.session.rollback()
|
||||
import_status = SettingsImport(name="Site Settings", status=False, exception=str(inst))
|
||||
|
||||
return [import_status]
|
||||
return []
|
||||
|
||||
def import_groups(self):
|
||||
groups_file = self.import_dir.joinpath("groups", "groups.json")
|
||||
|
||||
@@ -181,7 +181,7 @@ class MigrationBase(BaseModel):
|
||||
|
||||
except Exception as inst:
|
||||
exception = inst
|
||||
logger.error(inst)
|
||||
logger.exception(inst)
|
||||
self.session.rollback()
|
||||
|
||||
import_status = MigrationImport(slug=recipe.slug, name=recipe.name, status=status, exception=str(exception))
|
||||
|
||||
@@ -13,7 +13,12 @@ class UserService(UserHttpService[int, str]):
|
||||
event_func = create_user_event
|
||||
acting_user: PrivateUser = None
|
||||
|
||||
def populate_item(self, item_id: int) -> None:
|
||||
self.acting_user = self.db.users.get_one(item_id)
|
||||
return self.acting_user
|
||||
|
||||
def assert_existing(self, id) -> PrivateUser:
|
||||
self.populate_item(id)
|
||||
self._populate_target_user(id)
|
||||
self._assert_user_change_allowed()
|
||||
return self.target_user
|
||||
@@ -32,7 +37,6 @@ class UserService(UserHttpService[int, str]):
|
||||
self.target_user = self.acting_user
|
||||
|
||||
def change_password(self, password_change: ChangePassword) -> PrivateUser:
|
||||
""""""
|
||||
if not verify_password(password_change.current_password, self.target_user.password):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user