Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions backend/app/interfaces/user_data_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from abc import ABC, abstractmethod
from uuid import UUID

from app.schemas.user_data import UserDataCreateRequest, UserDataUpdateRequest


class IUserDataService(ABC):
"""Interface with user data management methods."""

@abstractmethod
def get_user_data_by_id(self, user_data_id: UUID):
"""
Get user data by its ID

:param user_data_id: user data's id
:type user_data_id: UUID
:return: a UserDataResponse with user data information
:rtype: UserDataResponse
:raises Exception: if user data retrieval fails
"""
pass

@abstractmethod
def get_user_data_by_user_id(self, user_id: UUID):
"""
Get user data associated with a user ID

:param user_id: user's id
:type user_id: UUID
:return: a UserDataResponse with user data information
:rtype: UserDataResponse
:raises Exception: if user data retrieval fails
"""
pass

@abstractmethod
def create_user_data(self, user_data: UserDataCreateRequest):
"""
Create user data for a user

:param user_data: the user data to be created
:type user_data: UserDataCreateRequest
:return: the created user data
:rtype: UserDataResponse
:raises Exception: if user data creation fails
"""
pass

@abstractmethod
def update_user_data_by_user_id(self, user_id: UUID, user_data: UserDataUpdateRequest):
"""
Update user data for a user

:param user_id: user's id
:type user_id: UUID
:param user_data: the user data to be updated
:type user_data: UserDataUpdateRequest
:return: the updated user data
:rtype: UserDataResponse
:raises Exception: if user data update fails
"""
pass

@abstractmethod
def delete_user_data_by_id(self, user_data_id: UUID):
"""
Delete user data by its ID

:param user_data_id: user data's id
:type user_data_id: UUID
:raises Exception: if user data deletion fails
"""
pass

@abstractmethod
def delete_user_data_by_user_id(self, user_id: UUID):
"""
Delete user data by user ID

:param user_id: user's id
:type user_id: UUID
:raises Exception: if user data deletion fails
"""
pass
1 change: 1 addition & 0 deletions backend/app/models/User.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class User(Base):
approved = Column(Boolean, default=False)

role = relationship("Role")
user_data = relationship("UserData", back_populates="user", uselist=False)

# time blocks in an availability for a user
availability = relationship("TimeBlock", secondary="available_times", back_populates="users")
Expand Down
37 changes: 37 additions & 0 deletions backend/app/models/UserData.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import uuid

from sqlalchemy import Boolean, Column, Date, ForeignKey, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship

from .Base import Base


class UserData(Base):
__tablename__ = "user_data"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, unique=True)
date_of_birth = Column(Date, nullable=True)
email = Column(String(120), nullable=True)
phone = Column(String(20), nullable=True)
postal_code = Column(String(10), nullable=True)
province = Column(String(50), nullable=True)
city = Column(String(100), nullable=True)
language = Column(String(50), nullable=True)
criminal_record_status = Column(Boolean, nullable=True)
blood_cancer_status = Column(Boolean, nullable=True)
caregiver_status = Column(Boolean, nullable=True)
caregiver_type = Column(String(100), nullable=True)
diagnostic = Column(String(200), nullable=True)
date_of_diagnosis = Column(Date, nullable=True)
gender_identity = Column(String(50), nullable=True)
pronouns = Column(String(50), nullable=True)
ethnicity = Column(String(100), nullable=True)
marital_status = Column(String(50), nullable=True)
children_status = Column(Boolean, nullable=True)
treatment = Column(Text, nullable=True)
experience = Column(Text, nullable=True)
preferences = Column(Text, nullable=True)

# Relationship to User table
user = relationship("User", back_populates="user_data")
3 changes: 2 additions & 1 deletion backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@
from .SuggestedTime import suggested_times
from .TimeBlock import TimeBlock
from .User import User
from .UserData import UserData

# Used to avoid import errors for the models
__all__ = [
"Base",
"User",
"Role",
"UserData",
"TimeBlock",
"Match",
"MatchStatus",
"User",
"available_times",
"suggested_times",
]
Expand Down
111 changes: 111 additions & 0 deletions backend/app/routes/user_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException

from app.middleware.auth import has_roles
from app.schemas.user import UserRole
from app.schemas.user_data import (
UserDataCreateRequest,
UserDataResponse,
UserDataUpdateRequest,
)
from app.services.implementations.user_data_service import UserDataService
from app.utilities.service_utils import get_user_data_service

router = APIRouter(
prefix="/user-data",
tags=["user-data"],
)


@router.post("/", response_model=UserDataResponse)
async def create_user_data(
user_data: UserDataCreateRequest,
user_data_service: UserDataService = Depends(get_user_data_service),
authorized: bool = has_roles([UserRole.ADMIN, UserRole.PARTICIPANT, UserRole.VOLUNTEER]),
):
"""Create user data for intake form"""
try:
return user_data_service.create_user_data(user_data)
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.get("/user/{user_id}", response_model=UserDataResponse)
async def get_user_data_by_user_id(
user_id: UUID,
user_data_service: UserDataService = Depends(get_user_data_service),
authorized: bool = has_roles([UserRole.ADMIN, UserRole.PARTICIPANT, UserRole.VOLUNTEER]),
):
"""Get user data by user ID"""
try:
return user_data_service.get_user_data_by_user_id(user_id)
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.get("/{user_data_id}", response_model=UserDataResponse)
async def get_user_data_by_id(
user_data_id: UUID,
user_data_service: UserDataService = Depends(get_user_data_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
"""Get user data by its ID"""
try:
return user_data_service.get_user_data_by_id(user_data_id)
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.put("/user/{user_id}", response_model=UserDataResponse)
async def update_user_data_by_user_id(
user_id: UUID,
user_data: UserDataUpdateRequest,
user_data_service: UserDataService = Depends(get_user_data_service),
authorized: bool = has_roles([UserRole.ADMIN, UserRole.PARTICIPANT, UserRole.VOLUNTEER]),
):
"""Update user data by user ID"""
try:
return user_data_service.update_user_data_by_user_id(user_id, user_data)
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.delete("/{user_data_id}")
async def delete_user_data_by_id(
user_data_id: UUID,
user_data_service: UserDataService = Depends(get_user_data_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
"""Delete user data by its ID"""
try:
user_data_service.delete_user_data_by_id(user_data_id)
return {"message": f"User data {user_data_id} deleted successfully"}
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.delete("/user/{user_id}")
async def delete_user_data_by_user_id(
user_id: UUID,
user_data_service: UserDataService = Depends(get_user_data_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
"""Delete user data by user ID"""
try:
user_data_service.delete_user_data_by_user_id(user_id)
return {"message": f"User data for user {user_id} deleted successfully"}
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
68 changes: 68 additions & 0 deletions backend/app/schemas/user_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Pydantic schemas for user data validation and serialization.
Handles user data CRUD and response models for the API.
"""

from datetime import date
from typing import Optional
from uuid import UUID

from pydantic import BaseModel, ConfigDict, EmailStr, Field


class UserDataBase(BaseModel):
"""Base schema for user data with common attributes shared across schemas."""

date_of_birth: Optional[date] = None
email: Optional[EmailStr] = None
phone: Optional[str] = Field(None, max_length=20)
postal_code: Optional[str] = Field(None, max_length=10)
province: Optional[str] = Field(None, max_length=50)
city: Optional[str] = Field(None, max_length=100)
language: Optional[str] = Field(None, max_length=50)
criminal_record_status: Optional[bool] = None
blood_cancer_status: Optional[bool] = None
caregiver_status: Optional[bool] = None
caregiver_type: Optional[str] = Field(None, max_length=100)
diagnostic: Optional[str] = Field(None, max_length=200)
date_of_diagnosis: Optional[date] = None
gender_identity: Optional[str] = Field(None, max_length=50)
pronouns: Optional[str] = Field(None, max_length=50)
ethnicity: Optional[str] = Field(None, max_length=100)
marital_status: Optional[str] = Field(None, max_length=50)
children_status: Optional[bool] = None
treatment: Optional[str] = None
experience: Optional[str] = None
# NOTE: preferences can either be a comma-separated string or an array of strings coming from the
# client. We keep the underlying DB column as Text but allow the schema to accept both shapes so
# that the service layer can serialise the list form when necessary.
preferences: Optional[str | list[str]] = None # type: ignore[valid-type]


class UserDataCreateRequest(UserDataBase):
"""
Request schema for user data creation.
"""

user_id: UUID


class UserDataUpdateRequest(UserDataBase):
"""
Request schema for user data update.
All fields are optional for partial updates.
"""

pass


class UserDataResponse(UserDataBase):
"""
Response schema for user data, maps directly from ORM UserData object.
"""

id: UUID
user_id: UUID

# from_attributes enables automatic mapping from SQLAlchemy model to Pydantic model
model_config = ConfigDict(from_attributes=True)
3 changes: 2 additions & 1 deletion backend/app/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from . import models
from .middleware.auth_middleware import AuthMiddleware
from .routes import auth, availability, match, send_email, suggested_times, test, user
from .routes import auth, availability, match, send_email, suggested_times, test, user, user_data
from .utilities.constants import LOGGER_NAME
from .utilities.firebase_init import initialize_firebase
from .utilities.ses.ses_init import ensure_ses_templates
Expand Down Expand Up @@ -61,6 +61,7 @@ async def lifespan(_: FastAPI):
app.add_middleware(AuthMiddleware, public_paths=PUBLIC_PATHS)
app.include_router(auth.router)
app.include_router(user.router)
app.include_router(user_data.router)
app.include_router(availability.router)
app.include_router(suggested_times.router)
app.include_router(match.router)
Expand Down
Loading