diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 5012329df..87a1ab9cc 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -1,10 +1,15 @@ # Release Notes ## 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 - Admin and User Roles - 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. - Nutrition Information - calories @@ -13,9 +18,18 @@ - proteinContent - sodiumContent - sugarContent - - recipeCuisine + - recipeCuisine has been added - "categories" has been migrated to "recipeCategory" to adhear closer to the standard schema - "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 diff --git a/frontend/src/components/Admin/ManageUsers/TheGroupTable.vue b/frontend/src/components/Admin/ManageUsers/TheGroupTable.vue index d81ec2e02..50c625190 100644 --- a/frontend/src/components/Admin/ManageUsers/TheGroupTable.vue +++ b/frontend/src/components/Admin/ManageUsers/TheGroupTable.vue @@ -63,8 +63,8 @@ @@ -143,7 +143,7 @@ export default { }, { text: "Full Name", value: "fullName" }, { text: "Email", value: "email" }, - { text: "Family", value: "family" }, + { text: "Group", value: "group" }, { text: "Admin", value: "admin" }, { text: "", value: "actions", sortable: false, align: "center" }, ], @@ -154,7 +154,7 @@ export default { fullName: "", password: "", email: "", - family: "", + group: "", admin: false, }, defaultItem: { @@ -162,7 +162,7 @@ export default { fullName: "", password: "", email: "", - family: "", + group: "", admin: false, }, }), diff --git a/frontend/src/components/Admin/ManageUsers/TheUserTable.vue b/frontend/src/components/Admin/ManageUsers/TheUserTable.vue index 862e796ab..fdf33eba5 100644 --- a/frontend/src/components/Admin/ManageUsers/TheUserTable.vue +++ b/frontend/src/components/Admin/ManageUsers/TheUserTable.vue @@ -63,8 +63,8 @@ @@ -143,7 +143,7 @@ export default { }, { text: "Full Name", value: "fullName" }, { text: "Email", value: "email" }, - { text: "Family", value: "family" }, + { text: "Group", value: "group" }, { text: "Admin", value: "admin" }, { text: "", value: "actions", sortable: false, align: "center" }, ], @@ -154,7 +154,7 @@ export default { fullName: "", password: "", email: "", - family: "", + group: "", admin: false, }, defaultItem: { @@ -162,7 +162,7 @@ export default { fullName: "", password: "", email: "", - family: "", + group: "", admin: false, }, }), diff --git a/frontend/src/components/Login/SignUpForm.vue b/frontend/src/components/Login/SignUpForm.vue index d07063a04..b81a15d6f 100644 --- a/frontend/src/components/Login/SignUpForm.vue +++ b/frontend/src/components/Login/SignUpForm.vue @@ -121,7 +121,7 @@ export default { const userData = { fullName: this.user.name, email: this.user.email, - family: "public", + group: "default", password: this.user.password, admin: false, }; diff --git a/frontend/src/pages/Admin/Profile/index.vue b/frontend/src/pages/Admin/Profile/index.vue index 1a8b73ef5..3e535ce84 100644 --- a/frontend/src/pages/Admin/Profile/index.vue +++ b/frontend/src/pages/Admin/Profile/index.vue @@ -55,11 +55,11 @@ > @@ -167,7 +167,7 @@ export default { user: { fullName: "Change Me", email: "changeme@email.com", - family: "public", + group: "public", admin: true, id: 1, }, diff --git a/mealie/app.py b/mealie/app.py index 250f0401a..689b88974 100644 --- a/mealie/app.py +++ b/mealie/app.py @@ -1,5 +1,6 @@ import uvicorn from fastapi import FastAPI +from fastapi.logger import logger # import utils.startup as startup from core.config import APP_VERSION, PORT, SECRET, docs_url, redoc_url @@ -13,6 +14,7 @@ from routes import ( setting_routes, theme_routes, ) +from routes.groups import groups from routes.recipe import ( all_recipe_routes, category_routes, @@ -20,7 +22,6 @@ from routes.recipe import ( tag_routes, ) from routes.users import users -from fastapi.logger import logger app = FastAPI( title="Mealie", @@ -42,11 +43,13 @@ def start_scheduler(): def api_routers(): # Authentication app.include_router(users.router) + app.include_router(groups.router) # Recipes app.include_router(all_recipe_routes.router) app.include_router(category_routes.router) app.include_router(tag_routes.router) app.include_router(recipe_crud_routes.router) + # Meal Routes app.include_router(meal_routes.router) # Settings Routes diff --git a/mealie/core/config.py b/mealie/core/config.py index af3ef619c..054d1ff41 100644 --- a/mealie/core/config.py +++ b/mealie/core/config.py @@ -83,6 +83,7 @@ else: # Mongo Database MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie") +DEFAULT_GROUP = os.getenv("default_group", "home") DB_USERNAME = os.getenv("db_username", "root") DB_PASSWORD = os.getenv("db_password", "example") DB_HOST = os.getenv("db_host", "mongo") diff --git a/mealie/db/database.py b/mealie/db/database.py index 12175565b..92a3e2a18 100644 --- a/mealie/db/database.py +++ b/mealie/db/database.py @@ -1,6 +1,8 @@ +from schema.user import GroupInDB, UserInDB from sqlalchemy.orm.session import Session from db.db_base import BaseDocument +from db.models.group import Group from db.models.mealplan import MealPlanModel from db.models.recipe.recipe import Category, RecipeModel, Tag 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.users import User -""" -# TODO - - [ ] Abstract Classes to use save_new, and update from base models -""" - class _Recipes(BaseDocument): def __init__(self) -> None: self.primary_key = "slug" self.sql_model: RecipeModel = RecipeModel + self.orm_mode = False def update_image(self, session: Session, slug: str, extension: str = None) -> str: entry: RecipeModel = self._query_one(session, match_value=slug) @@ -31,36 +29,43 @@ class _Categories(BaseDocument): def __init__(self) -> None: self.primary_key = "slug" self.sql_model = Category + self.orm_mode = False class _Tags(BaseDocument): def __init__(self) -> None: self.primary_key = "slug" self.sql_model = Tag + self.orm_mode = False class _Meals(BaseDocument): def __init__(self) -> None: self.primary_key = "uid" self.sql_model = MealPlanModel + self.orm_mode = False class _Settings(BaseDocument): def __init__(self) -> None: self.primary_key = "name" self.sql_model = SiteSettingsModel + self.orm_mode = False class _Themes(BaseDocument): def __init__(self) -> None: self.primary_key = "name" self.sql_model = SiteThemeModel + self.orm_mode = False class _Users(BaseDocument): def __init__(self) -> None: self.primary_key = "id" self.sql_model = User + self.orm_mode = True + self.schema = UserInDB def update_password(self, session, id, password: str): entry = self._query_one(session=session, match_value=id) @@ -71,16 +76,19 @@ class _Users(BaseDocument): return return_data -class _SignUps(BaseDocument): +class _Groups(BaseDocument): def __init__(self) -> None: - self.primary_key = "token" - self.sql_model = SignUp + self.primary_key = "id" + self.sql_model = Group + self.orm_mode = True + self.schema = GroupInDB class _SignUps(BaseDocument): def __init__(self) -> None: self.primary_key = "token" self.sql_model = SignUp + self.orm_mode = False class Database: @@ -93,6 +101,7 @@ class Database: self.tags = _Tags() self.users = _Users() self.sign_ups = _SignUps() + self.groups = _Groups() db = Database() diff --git a/mealie/db/db_base.py b/mealie/db/db_base.py index 6e1204c5c..62041f3c5 100644 --- a/mealie/db/db_base.py +++ b/mealie/db/db_base.py @@ -1,5 +1,6 @@ from typing import List +from pydantic import BaseModel from sqlalchemy.orm import load_only from sqlalchemy.orm.session import Session @@ -11,11 +12,20 @@ class BaseDocument: self.primary_key: str self.store: str self.sql_model: SqlAlchemyBase + self.orm_mode = False + self.schema: BaseModel # TODO: Improve Get All Query Functionality def get_all( self, session: Session, limit: int = None, order_by: str = None ) -> 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()] if limit == 1: @@ -105,6 +115,15 @@ class BaseDocument: .limit(limit) .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] if limit == 1: @@ -128,6 +147,10 @@ class BaseDocument: new_document = self.sql_model(session=session, **document) session.add(new_document) session.commit() + + if self.orm_mode: + return self.schema.from_orm(new_document) + return_data = new_document.dict() return return_data @@ -145,9 +168,13 @@ class BaseDocument: entry = self._query_one(session=session, match_value=match_value) entry.update(session=session, **new_data) + + if self.orm_mode: + session.commit() + return self.schema.from_orm(entry) + return_data = entry.dict() session.commit() - return return_data def delete(self, session: Session, primary_key_value) -> dict: diff --git a/mealie/db/init_db.py b/mealie/db/init_db.py index cf44ca5bc..539e3bda2 100644 --- a/mealie/db/init_db.py +++ b/mealie/db/init_db.py @@ -1,3 +1,4 @@ +from core.config import DEFAULT_GROUP from core.security import get_password_hash from fastapi.logger import logger from schema.settings import SiteSettings, Webhooks @@ -12,6 +13,7 @@ def init_db(db: Session = None) -> None: if not db: db = create_session() + default_group_init(db) default_settings_init(db) default_theme_init(db) default_user_init(db) @@ -50,12 +52,19 @@ def default_settings_init(session: Session): 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): default_user = { "full_name": "Change Me", "email": "changeme@email.com", "password": get_password_hash("MyPassword"), - "family": "public", + "group": DEFAULT_GROUP, "admin": True, } diff --git a/mealie/db/models/_all_models.py b/mealie/db/models/_all_models.py index 2debf7b2b..99a08f68e 100644 --- a/mealie/db/models/_all_models.py +++ b/mealie/db/models/_all_models.py @@ -4,3 +4,4 @@ from db.models.settings import * from db.models.theme import * from db.models.users import * from db.models.sign_up import * +from db.models.group import * diff --git a/mealie/db/models/group.py b/mealie/db/models/group.py index cd2c950a8..ed5e8e037 100644 --- a/mealie/db/models/group.py +++ b/mealie/db/models/group.py @@ -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 sqlalchemy import Boolean, Column, Integer, String +from fastapi.logger import logger +from slugify import slugify class Group(SqlAlchemyBase, BaseMixins): __tablename__ = "groups" - id = Column(Integer, primary_key=True) - name = Column(String, index=True) - slug = Column(String, unique=True, index=True) + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String, index=True, nullable=False, unique=True) + users = orm.relationship("User", back_populates="group") + mealplans = orm.relationship("MealPlanModel", back_populates="group") - def __init__( - self, - 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 + def __init__(self, name, session=None) -> None: + self.name = name + @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) diff --git a/mealie/db/models/mealplan.py b/mealie/db/models/mealplan.py index 973227bd3..e1e2f11bd 100644 --- a/mealie/db/models/mealplan.py +++ b/mealie/db/models/mealplan.py @@ -46,6 +46,8 @@ class MealPlanModel(SqlAlchemyBase, BaseMixins): startDate = sa.Column(sa.Date) endDate = sa.Column(sa.Date) 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: self.startDate = startDate diff --git a/mealie/db/models/recipe/nutrition.py b/mealie/db/models/recipe/nutrition.py index 9e11c67cb..d946da3a2 100644 --- a/mealie/db/models/recipe/nutrition.py +++ b/mealie/db/models/recipe/nutrition.py @@ -6,12 +6,12 @@ class Nutrition(SqlAlchemyBase): __tablename__ = "recipe_nutrition" id = sa.Column(sa.Integer, primary_key=True) parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id")) - calories = sa.Column(sa.Integer) - fatContent = sa.Column(sa.Integer) - fiberContent = sa.Column(sa.Integer) - proteinContent = sa.Column(sa.Integer) - sodiumContent = sa.Column(sa.Integer) - sugarContent = sa.Column(sa.Integer) + calories = sa.Column(sa.String) + fatContent = sa.Column(sa.String) + fiberContent = sa.Column(sa.String) + proteinContent = sa.Column(sa.String) + sodiumContent = sa.Column(sa.String) + sugarContent = sa.Column(sa.String) def __init__( self, diff --git a/mealie/db/models/users.py b/mealie/db/models/users.py index a24eab08d..422fc2ce0 100644 --- a/mealie/db/models/users.py +++ b/mealie/db/models/users.py @@ -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 sqlalchemy import Boolean, Column, Integer, String +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm class User(SqlAlchemyBase, BaseMixins): @@ -8,9 +10,9 @@ class User(SqlAlchemyBase, BaseMixins): full_name = Column(String, index=True) email = Column(String, unique=True, index=True) password = Column(String) - is_active = Column(Boolean(), default=True) - family = Column(String) - admin = Column(Boolean(), default=False) + group_id = Column(String, ForeignKey("groups.id")) + group = orm.relationship("Group", back_populates="users") + admin = Column(Boolean, default=False) def __init__( self, @@ -18,29 +20,21 @@ class User(SqlAlchemyBase, BaseMixins): full_name, email, password, - family="public", + group: str = DEFAULT_GROUP, admin=False, ) -> None: + + group = group if group else DEFAULT_GROUP self.full_name = full_name self.email = email - self.family = family + self.group = Group.create_if_not_exist(session, group) 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): + def update(self, full_name, email, group, admin, session=None): self.full_name = full_name self.email = email - self.family = family + self.group = Group.create_if_not_exist(session, group) self.admin = admin def update_password(self, password): diff --git a/mealie/routes/deps.py b/mealie/routes/deps.py index 8390d4153..334e6c140 100644 --- a/mealie/routes/deps.py +++ b/mealie/routes/deps.py @@ -20,5 +20,5 @@ def query_user(user_email: str, session: Session = None) -> UserInDB: session = session if session else create_session() user = db.users.get(session, user_email, "email") session.close() - return UserInDB(**user) + return user diff --git a/mealie/routes/groups/crud.py b/mealie/routes/groups/crud.py new file mode 100644 index 000000000..541fa0cf5 --- /dev/null +++ b/mealie/routes/groups/crud.py @@ -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) diff --git a/mealie/routes/groups/groups.py b/mealie/routes/groups/groups.py new file mode 100644 index 000000000..17aa1fc90 --- /dev/null +++ b/mealie/routes/groups/groups.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter +from routes.groups import crud + +router = APIRouter() + +router.include_router(crud.router) + diff --git a/mealie/routes/users/crud.py b/mealie/routes/users/crud.py index 8e8cfe53f..336376281 100644 --- a/mealie/routes/users/crud.py +++ b/mealie/routes/users/crud.py @@ -66,8 +66,8 @@ async def update_user( ): if current_user.id == id or current_user.admin: - updated_user = db.users.update(session, id, new_data.dict()) - email = updated_user.get("email") + updated_user: UserInDB = db.users.update(session, id, new_data.dict()) + email = updated_user.email if current_user.id == id: access_token = manager.create_access_token( data=dict(sub=email), expires=timedelta(hours=2) diff --git a/mealie/schema/recipe.py b/mealie/schema/recipe.py index 91adeef42..b176d48de 100644 --- a/mealie/schema/recipe.py +++ b/mealie/schema/recipe.py @@ -15,12 +15,12 @@ class RecipeStep(BaseModel): class Nutrition(BaseModel): - calories: Optional[int] - fatContent: Optional[int] - fiberContent: Optional[int] - proteinContent: Optional[int] - sodiumContent: Optional[int] - sugarContent: Optional[int] + calories: Optional[str] + fatContent: Optional[str] + fiberContent: Optional[str] + proteinContent: Optional[str] + sodiumContent: Optional[str] + sugarContent: Optional[str] class Recipe(BaseModel): diff --git a/mealie/schema/user.py b/mealie/schema/user.py index 25986f260..27b9f69a3 100644 --- a/mealie/schema/user.py +++ b/mealie/schema/user.py @@ -1,26 +1,35 @@ from typing import Optional +from core.config import DEFAULT_GROUP from fastapi_camelcase import CamelModel -# from pydantic import EmailStr - class ChangePassword(CamelModel): current_password: str new_password: str +class GroupBase(CamelModel): + name: str + + class Config: + orm_mode = True + + class UserBase(CamelModel): full_name: Optional[str] = None email: str - family: str admin: bool + group: Optional[str] + + class Config: + orm_mode = True class Config: schema_extra = { "fullName": "Change Me", "email": "changeme@email.com", - "family": "public", + "group": DEFAULT_GROUP, "admin": "false", } @@ -31,7 +40,24 @@ class UserIn(UserBase): class UserOut(UserBase): id: int + group: GroupBase + + class Config: + orm_mode = True -class UserInDB(UserIn, UserOut): +class UserInDB(UserOut): + password: str pass + + class Config: + orm_mode = True + + +class GroupInDB(GroupBase): + id: int + name: str + users: Optional[list[UserOut]] + + class Config: + orm_mode = True diff --git a/mealie/services/recipe_services.py b/mealie/services/recipe_services.py index 27e226640..82b2451e1 100644 --- a/mealie/services/recipe_services.py +++ b/mealie/services/recipe_services.py @@ -20,12 +20,12 @@ class RecipeStep(BaseModel): class Nutrition(BaseModel): - calories: Optional[int] - fatContent: Optional[int] - fiberContent: Optional[int] - proteinContent: Optional[int] - sodiumContent: Optional[int] - sugarContent: Optional[int] + calories: Optional[str] + fatContent: Optional[str] + fiberContent: Optional[str] + proteinContent: Optional[str] + sodiumContent: Optional[str] + sugarContent: Optional[str] class Recipe(BaseModel): diff --git a/mealie/services/scraper/cleaner.py b/mealie/services/scraper/cleaner.py index edf277945..2b94c9a94 100644 --- a/mealie/services/scraper/cleaner.py +++ b/mealie/services/scraper/cleaner.py @@ -31,6 +31,9 @@ class Cleaner: recipe_data["prepTime"] = Cleaner.time(recipe_data.get("prepTime", None)) recipe_data["performTime"] = Cleaner.time(recipe_data.get("performTime", 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.get("recipeYield") @@ -47,6 +50,13 @@ class Cleaner: return recipe_data + @staticmethod + def category(category: str): + if type(category) == type(str): + return [category] + else: + return [] + @staticmethod def html(raw_html): cleanr = re.compile("<.*?>") diff --git a/mealie/tests/conftest.py b/mealie/tests/conftest.py index 2852210ca..e73807528 100644 --- a/mealie/tests/conftest.py +++ b/mealie/tests/conftest.py @@ -34,7 +34,7 @@ def api_client(): yield TestClient(app) - SQLITE_FILE.unlink() + # SQLITE_FILE.unlink() @fixture(scope="session") diff --git a/mealie/tests/test_routes/test_recipe_routes.py b/mealie/tests/test_routes/test_recipe_routes.py index 63573af48..adc25867d 100644 --- a/mealie/tests/test_routes/test_recipe_routes.py +++ b/mealie/tests/test_routes/test_recipe_routes.py @@ -63,7 +63,7 @@ def test_read_update(api_client, recipe_data): recipe["notes"] = test_notes test_categories = ["one", "two", "three"] - recipe["categories"] = test_categories + recipe["recipeCategory"] = test_categories response = api_client.put( 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) 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) diff --git a/mealie/tests/test_routes/test_user_routes.py b/mealie/tests/test_routes/test_user_routes.py index 02a542d0e..e2c63b388 100644 --- a/mealie/tests/test_routes/test_user_routes.py +++ b/mealie/tests/test_routes/test_user_routes.py @@ -16,7 +16,9 @@ def default_user(): "id": 1, "fullName": "Change Me", "email": "changeme@email.com", - "family": "public", + "group": { + "name": "home" + }, "admin": True } @@ -27,7 +29,9 @@ def new_user(): "id": 2, "fullName": "My New User", "email": "newuser@email.com", - "family": "public", + "group": { + "name": "home" + }, "admin": False } @@ -54,7 +58,7 @@ def test_create_user(api_client: requests, token, new_user): "fullName": "My New User", "email": "newuser@email.com", "password": "MyStrongPassword", - "family": "public", + "group": "home", "admin": False } @@ -78,7 +82,7 @@ def test_update_user(api_client: requests, token): "id": 1, "fullName": "Updated Name", "email": "updated@email.com", - "family": "public", + "group": "home", "admin": True } response = api_client.put(f"{BASE}/1", headers=token, json=update_data)