Skip to content
Merged
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
30 changes: 30 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# .pre-commit-config.yaml
# Root-level config to handle both frontend and backend linting/formatting

default_language_version:
python: python3.12

default_stages: [pre-commit, pre-push]

repos:
# Backend: Ruff linting and formatting
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.6.7
hooks:
- id: ruff
args: [--fix, --line-length=120]
files: ^backend/
- id: ruff-format
args: [--line-length=120]
files: ^backend/

# Frontend: Prettier formatting
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
files: ^frontend/.*\.(ts|tsx|js|jsx|json|css|md)$
exclude: ^frontend/(node_modules|\.next|out|build)/
types_or: [file]
additional_dependencies:
- [email protected]
35 changes: 0 additions & 35 deletions backend/.pre-commit-config.yaml

This file was deleted.

34 changes: 34 additions & 0 deletions backend/app/models/AvailabilityTemplate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, Time
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func

from .Base import Base


class AvailabilityTemplate(Base):
"""
Stores recurring weekly availability patterns for volunteers.
Each template represents a time slot on a specific day of the week.
These templates are projected forward to create specific TimeBlocks for matches.
"""

__tablename__ = "availability_templates"

id = Column(Integer, primary_key=True)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)

# Day of week: 0=Monday, 1=Tuesday, ..., 6=Sunday
day_of_week = Column(Integer, nullable=False)

# Time of day (just time, no date)
start_time = Column(Time, nullable=False) # e.g., 14:00:00
end_time = Column(Time, nullable=False) # e.g., 16:00:00

# Optional: for future enhancements (e.g., temporarily disable a template)
is_active = Column(Boolean, default=True)

created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

user = relationship("User", back_populates="availability_templates")
3 changes: 0 additions & 3 deletions backend/app/models/TimeBlock.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,3 @@ class TimeBlock(Base):

# suggested matches
suggested_matches = relationship("Match", secondary="suggested_times", back_populates="suggested_time_blocks")

# the availability that the timeblock is a part of for a given user
users = relationship("User", secondary="available_times", back_populates="availability")
4 changes: 2 additions & 2 deletions backend/app/models/User.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ class User(Base):

role = relationship("Role")

# time blocks in an availability for a user
availability = relationship("TimeBlock", secondary="available_times", back_populates="users")
# recurring availability templates (day of week + time)
availability_templates = relationship("AvailabilityTemplate", back_populates="user")

participant_matches = relationship("Match", back_populates="participant", foreign_keys=[Match.participant_id])

Expand Down
5 changes: 2 additions & 3 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@

from app.utilities.constants import LOGGER_NAME

from .AvailableTime import available_times

# Make sure all models are here to reflect all current models
# when autogenerating new migration
from .AvailabilityTemplate import AvailabilityTemplate
from .Base import Base
from .Experience import Experience
from .Form import Form
Expand All @@ -35,8 +34,8 @@
"Match",
"MatchStatus",
"User",
"available_times",
"suggested_times",
"AvailabilityTemplate",
"UserData",
"Treatment",
"Experience",
Expand Down
2 changes: 1 addition & 1 deletion backend/app/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# TODO: ADD RATE LIMITING
@router.post("/register", response_model=UserCreateResponse)
async def register_user(user: UserCreateRequest, user_service: UserService = Depends(get_user_service)):
allowed_Admins = ["[email protected]", "[email protected]"]
allowed_Admins = ["[email protected]", "[email protected]", "[email protected]"]
if user.role == UserRole.ADMIN:
if user.email not in allowed_Admins:
raise HTTPException(status_code=403, detail="Access denied. Admin privileges required for admin portal")
Expand Down
34 changes: 34 additions & 0 deletions backend/app/routes/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
UserRole,
UserUpdateRequest,
)
from app.schemas.user_data import UserDataUpdateRequest
from app.services.implementations.user_service import UserService
from app.utilities.service_utils import get_user_service

Expand Down Expand Up @@ -89,6 +90,22 @@ async def update_user(
raise HTTPException(status_code=500, detail=str(e))


# admin only update user_data (cancer experience, treatments, experiences, etc.)
@router.patch("/{user_id}/user-data", response_model=UserResponse)
async def update_user_data(
user_id: str,
user_data_update: UserDataUpdateRequest,
user_service: UserService = Depends(get_user_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
try:
return await user_service.update_user_data_by_id(user_id, user_data_update)
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


# admin only delete user
@router.delete("/{user_id}")
async def delete_user(
Expand All @@ -110,6 +127,7 @@ async def delete_user(
async def deactivate_user(
user_id: str,
user_service: UserService = Depends(get_user_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
try:
await user_service.soft_delete_user_by_id(user_id)
Expand All @@ -118,3 +136,19 @@ async def deactivate_user(
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


# reactivate user
@router.post("/{user_id}/reactivate")
async def reactivate_user(
user_id: str,
user_service: UserService = Depends(get_user_service),
authorized: bool = has_roles([UserRole.ADMIN]),
):
try:
await user_service.reactivate_user_by_id(user_id)
return {"message": "User reactivated successfully"}
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
19 changes: 12 additions & 7 deletions backend/app/schemas/availability.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
from datetime import time
from typing import List
from uuid import UUID

from pydantic import BaseModel

from app.schemas.time_block import TimeBlockEntity, TimeRange

class AvailabilityTemplateSlot(BaseModel):
"""Represents a single availability template slot (day of week + time range)"""

day_of_week: int # 0=Monday, 1=Tuesday, ..., 6=Sunday
start_time: time # e.g., 14:00:00
end_time: time # e.g., 16:00:00


class CreateAvailabilityRequest(BaseModel):
user_id: UUID
available_times: List[TimeRange]
templates: List[AvailabilityTemplateSlot]


class CreateAvailabilityResponse(BaseModel):
Expand All @@ -22,17 +29,15 @@ class GetAvailabilityRequest(BaseModel):

class AvailabilityEntity(BaseModel):
user_id: UUID
available_times: List[TimeBlockEntity]
templates: List[AvailabilityTemplateSlot]


class DeleteAvailabilityRequest(BaseModel):
user_id: UUID
delete: list[TimeRange] = []
templates: List[AvailabilityTemplateSlot] = []


class DeleteAvailabilityResponse(BaseModel):
user_id: UUID
deleted: int

# return the user’s availability after the update
availability: List[TimeBlockEntity]
templates: List[AvailabilityTemplateSlot] # remaining templates after deletion
8 changes: 8 additions & 0 deletions backend/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator

from .availability import AvailabilityTemplateSlot
from .user_data import UserDataResponse
from .volunteer_data import VolunteerDataResponse

# TODO:
# confirm complexity rules for fields (such as password)

Expand Down Expand Up @@ -135,8 +139,12 @@ class UserResponse(BaseModel):
role_id: int
auth_id: str
approved: bool
active: bool
role: "RoleResponse"
form_status: FormStatus
user_data: Optional[UserDataResponse] = None
volunteer_data: Optional[VolunteerDataResponse] = None
availability: List[AvailabilityTemplateSlot] = []

model_config = ConfigDict(from_attributes=True)

Expand Down
112 changes: 112 additions & 0 deletions backend/app/schemas/user_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from datetime import date
from typing import List, Optional
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field


class TreatmentResponse(BaseModel):
id: int
name: str

model_config = ConfigDict(from_attributes=True)


class ExperienceResponse(BaseModel):
id: int
name: str

model_config = ConfigDict(from_attributes=True)


class UserDataResponse(BaseModel):
id: UUID
user_id: UUID

# Personal Information
first_name: Optional[str]
last_name: Optional[str]
date_of_birth: Optional[date]
email: Optional[str]
phone: Optional[str]
city: Optional[str]
province: Optional[str]
postal_code: Optional[str]

# Demographics
gender_identity: Optional[str]
pronouns: Optional[List[str]]
ethnic_group: Optional[List[str]]
marital_status: Optional[str]
has_kids: Optional[str]
timezone: Optional[str]

# Cancer Experience
diagnosis: Optional[str]
date_of_diagnosis: Optional[date]

# Custom entries
other_ethnic_group: Optional[str]
gender_identity_custom: Optional[str]
additional_info: Optional[str]

# Flow control
has_blood_cancer: Optional[str]
caring_for_someone: Optional[str]

# Loved One Info
loved_one_gender_identity: Optional[str]
loved_one_age: Optional[str]
loved_one_diagnosis: Optional[str]
loved_one_date_of_diagnosis: Optional[date]

# Relations
treatments: List[TreatmentResponse] = []
experiences: List[ExperienceResponse] = []
loved_one_treatments: List[TreatmentResponse] = []
loved_one_experiences: List[ExperienceResponse] = []

model_config = ConfigDict(from_attributes=True)


class UserDataUpdateRequest(BaseModel):
"""
Request schema for user_data updates, all fields optional.
Supports partial updates for user's own data and loved one's data.
"""

# Personal Information
first_name: Optional[str] = None
last_name: Optional[str] = None
date_of_birth: Optional[date] = None
phone: Optional[str] = None
city: Optional[str] = None
province: Optional[str] = None
postal_code: Optional[str] = None

# Demographics
gender_identity: Optional[str] = None
pronouns: Optional[List[str]] = Field(None, description="List of pronoun strings")
ethnic_group: Optional[List[str]] = Field(None, description="List of ethnic group strings")
marital_status: Optional[str] = None
has_kids: Optional[str] = None
timezone: Optional[str] = None

# User's Cancer Experience
diagnosis: Optional[str] = None
date_of_diagnosis: Optional[date] = None
treatments: Optional[List[str]] = Field(None, description="List of treatment names")
experiences: Optional[List[str]] = Field(None, description="List of experience names")
additional_info: Optional[str] = None

# Loved One Demographics
loved_one_gender_identity: Optional[str] = None
loved_one_age: Optional[str] = None

# Loved One's Cancer Experience
loved_one_diagnosis: Optional[str] = None
loved_one_date_of_diagnosis: Optional[date] = None
loved_one_treatments: Optional[List[str]] = Field(None, description="List of treatment names")
loved_one_experiences: Optional[List[str]] = Field(None, description="List of experience names")

model_config = ConfigDict(from_attributes=True)
Loading