Add CRUD operations for users with tests and fixtures
All checks were successful
Build and Push Docker Images / changes (push) Successful in 4s
Build and Push Docker Images / build-backend (push) Successful in 54s
Build and Push Docker Images / build-frontend (push) Has been skipped

Implemented user CRUD operations including creation, retrieval, updating, and deletion through a generic CRUD base class. Enhanced user schemas with additional attributes and created tests to verify functionality, covering edge cases such as duplicates and pagination. Updated the test suite with new fixtures and methods to support the tests.
This commit is contained in:
2025-03-04 18:33:33 +01:00
parent c6fe040f21
commit 7d9f4ee4b2
7 changed files with 260 additions and 5 deletions

View File

62
backend/app/crud/base.py Normal file
View File

@@ -0,0 +1,62 @@
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.core.database import Base
ModelType = TypeVar("ModelType", bound=Base)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[ModelType]):
"""
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
Parameters:
model: A SQLAlchemy model class
"""
self.model = model
def get(self, db: Session, id: str) -> Optional[ModelType]:
return db.query(self.model).filter(self.model.id == id).first()
def get_multi(
self, db: Session, *, skip: int = 0, limit: int = 100
) -> List[ModelType]:
return db.query(self.model).offset(skip).limit(limit).all()
def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
self,
db: Session,
*,
db_obj: ModelType,
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
) -> ModelType:
obj_data = jsonable_encoder(db_obj)
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.model_dump(exclude_unset=True)
for field in obj_data:
if field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def remove(self, db: Session, *, id: str) -> ModelType:
obj = db.query(self.model).get(id)
db.delete(obj)
db.commit()
return obj

56
backend/app/crud/user.py Normal file
View File

@@ -0,0 +1,56 @@
# app/crud/user.py
from typing import Optional, Union, Dict, Any
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.user import User
from app.schemas.users import UserCreate, UserUpdate
from app.core.auth import get_password_hash
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
return db.query(User).filter(User.email == email).first()
def create(self, db: Session, *, obj_in: UserCreate) -> User:
db_obj = User(
email=obj_in.email,
password_hash=get_password_hash(obj_in.password),
first_name=obj_in.first_name,
last_name=obj_in.last_name,
phone_number=obj_in.phone_number if hasattr(obj_in, 'phone_number') else None,
is_superuser=obj_in.is_superuser if hasattr(obj_in, 'is_superuser') else False,
preferences={}
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
self,
db: Session,
*,
db_obj: User,
obj_in: Union[UserUpdate, Dict[str, Any]]
) -> User:
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.model_dump(exclude_unset=True)
# Handle password separately if it exists in update data
if "password" in update_data:
update_data["password_hash"] = get_password_hash(update_data["password"])
del update_data["password"]
return super().update(db, db_obj=db_obj, obj_in=update_data)
def is_active(self, user: User) -> bool:
return user.is_active
def is_superuser(self, user: User) -> bool:
return user.is_superuser
# Create a singleton instance for use across the application
user = CRUDUser(User)

View File

@@ -4,7 +4,6 @@ from datetime import datetime
from typing import Optional, Dict, Any
from uuid import UUID
import pydantic
from pydantic import BaseModel, EmailStr, field_validator, ConfigDict
@@ -27,6 +26,7 @@ class UserBase(BaseModel):
class UserCreate(UserBase):
password: str
is_superuser: bool = False
@field_validator('password')
@classmethod
@@ -46,7 +46,7 @@ class UserUpdate(BaseModel):
last_name: Optional[str] = None
phone_number: Optional[str] = None
preferences: Optional[Dict[str, Any]] = None
is_active: Optional[bool] = True
@field_validator('phone_number')
def validate_phone_number(cls, v: Optional[str]) -> Optional[str]:
if v is None:
@@ -81,7 +81,6 @@ class UserUpdate(BaseModel):
return cleaned
class UserInDB(UserBase):
id: UUID
is_active: bool
@@ -147,4 +146,4 @@ class LoginRequest(BaseModel):
class RefreshTokenRequest(BaseModel):
refresh_token: str
refresh_token: str