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
18 changes: 17 additions & 1 deletion backend/app/routes/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,24 @@ async def get_matches_for_participant(
match_service: MatchService = Depends(get_match_service),
_authorized: bool = has_roles([UserRole.ADMIN]),
):
"""Get matches for a participant (admin only). Includes all matches including awaiting_volunteer_acceptance."""
try:
return await match_service.get_matches_for_participant(participant_id)
return await match_service.get_all_matches_for_participant_admin(participant_id)
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.get("/volunteer/{volunteer_id}", response_model=MatchListForVolunteerResponse)
async def get_matches_for_volunteer_admin(
volunteer_id: UUID,
match_service: MatchService = Depends(get_match_service),
_authorized: bool = has_roles([UserRole.ADMIN]),
):
"""Get all matches for a volunteer (admin only)."""
try:
return await match_service.get_matches_for_volunteer(volunteer_id)
except HTTPException as http_ex:
raise http_ex
except Exception as e:
Expand Down
6 changes: 6 additions & 0 deletions backend/app/schemas/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class MatchVolunteerSummary(BaseModel):
treatments: List[str] = Field(default_factory=list)
experiences: List[str] = Field(default_factory=list)
overview: Optional[str] = None # Volunteer experience/overview from volunteer_data
loved_one_diagnosis: Optional[str] = None
loved_one_treatments: List[str] = Field(default_factory=list)
loved_one_experiences: List[str] = Field(default_factory=list)

model_config = ConfigDict(from_attributes=True)

Expand All @@ -70,6 +73,9 @@ class MatchParticipantSummary(BaseModel):
treatments: List[str] = Field(default_factory=list)
experiences: List[str] = Field(default_factory=list)
timezone: Optional[str] = None
loved_one_diagnosis: Optional[str] = None
loved_one_treatments: List[str] = Field(default_factory=list)
loved_one_experiences: List[str] = Field(default_factory=list)

model_config = ConfigDict(from_attributes=True)

Expand Down
12 changes: 12 additions & 0 deletions backend/app/schemas/matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,21 @@ class AdminMatchCandidate(BaseModel):
timezone: Optional[str]
age: Optional[int]
diagnosis: Optional[str]
date_of_diagnosis: Optional[str] = None # ISO format date string
treatments: List[str] = []
experiences: List[str] = []
match_score: float # 0-100 scale
match_count: int = 0 # Number of active matches for this volunteer
# Additional fields for dynamic columns based on preferences
marital_status: Optional[str] = None
gender_identity: Optional[str] = None
ethnic_group: Optional[List[str]] = None
has_kids: Optional[str] = None
loved_one_age: Optional[str] = None
loved_one_diagnosis: Optional[str] = None
loved_one_date_of_diagnosis: Optional[str] = None # ISO format date string
loved_one_treatments: List[str] = []
loved_one_experiences: List[str] = []


class AdminMatchesResponse(BaseModel):
Expand Down
3 changes: 3 additions & 0 deletions backend/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class UserCreateResponse(BaseModel):
auth_id: str
approved: bool
active: bool
pending_volunteer_request: bool
form_status: FormStatus
language: Language

Expand All @@ -148,12 +149,14 @@ class UserResponse(BaseModel):
auth_id: str
approved: bool
active: bool
pending_volunteer_request: bool
role: "RoleResponse"
form_status: FormStatus
user_data: Optional[UserDataResponse] = None
volunteer_data: Optional[VolunteerDataResponse] = None
availability: List[AvailabilityTemplateSlot] = []
language: Language
match_count: int = Field(default=0, description="Number of active matches for this user")

model_config = ConfigDict(from_attributes=True)

Expand Down
1 change: 1 addition & 0 deletions backend/app/schemas/user_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class UserDataUpdateRequest(BaseModel):
marital_status: Optional[str] = None
has_kids: Optional[str] = None
timezone: Optional[str] = None
language: Optional[str] = Field(None, description="Preferred language (en or fr)")

# User's Cancer Experience
diagnosis: Optional[str] = None
Expand Down
9 changes: 7 additions & 2 deletions backend/app/seeds/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ def seed_users(session: Session) -> None:
"city": "Calgary",
"province": "Alberta",
"postal_code": "T2P 1J4",
"gender_identity": "Man",
"gender_identity": "Male",
"pronouns": ["he", "him"],
"ethnic_group": ["White/Caucasian"],
"marital_status": "Single",
Expand All @@ -366,10 +366,15 @@ def seed_users(session: Session) -> None:
"diagnosis": "Non-Hodgkin Lymphoma",
"date_of_diagnosis": date(2019, 6, 10),
"has_blood_cancer": "yes",
"caring_for_someone": "no",
"caring_for_someone": "yes",
"loved_one_age": "26",
"loved_one_diagnosis": "Blood Cancer",
"loved_one_date_of_diagnosis": date(2023, 2, 20),
},
"treatments": [3, 6, 14], # Chemotherapy, Radiation, BTK Inhibitors
"experiences": [1, 3, 4, 7], # Brain Fog, Feeling Overwhelmed, Fatigue, Returning to work
"loved_one_treatments": [3, 6], # Chemotherapy, Radiation
"loved_one_experiences": [1, 3, 4, 7], # Brain Fog, Feeling Overwhelmed, Fatigue, Returning to work
},
{
"role": "volunteer",
Expand Down
6 changes: 6 additions & 0 deletions backend/app/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
)
from .services.implementations.match_completion_service import MatchCompletionService
from .utilities.constants import LOGGER_NAME
from .utilities.db_utils import engine
from .utilities.firebase_init import initialize_firebase
from .utilities.ses.ses_init import ensure_ses_templates

Expand Down Expand Up @@ -87,6 +88,11 @@ async def lifespan(_: FastAPI):
# Shutdown scheduler gracefully
log.info("Shutting down scheduler...")
scheduler.shutdown()

# Dispose database engine to close all connection pools
# This prevents async generator cleanup errors during shutdown
log.info("Disposing database engine...")
engine.dispose()
log.info("Shutting down...")


Expand Down
7 changes: 5 additions & 2 deletions backend/app/services/implementations/form_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ def _process_ranking_form(self, submission: FormSubmission, user: User) -> None:
if not target:
raise ValueError("Ranking form missing 'target' field")

# Delete existing preferences for this user/target
# Delete ALL existing preferences for this user (not just for this target_role)
# This ensures a user can only have preferences for one target_role at a time
self.db.query(RankingPreference).filter(
RankingPreference.user_id == user.id,
RankingPreference.target_role == target,
).delete(synchronize_session=False)

# Create new preference records
Expand All @@ -135,6 +135,9 @@ def _process_ranking_form(self, submission: FormSubmission, user: User) -> None:
if user.form_status in (FormStatus.RANKING_TODO, FormStatus.RANKING_SUBMITTED):
user.form_status = FormStatus.COMPLETED

# Set pending_volunteer_request flag so admin knows to create matches
user.pending_volunteer_request = True

# Create a MATCHING task so admins know this participant is ready to be matched
try:
matching_task = Task(
Expand Down
70 changes: 70 additions & 0 deletions backend/app/services/implementations/match_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,8 @@ async def get_matches_for_participant(self, participant_id: UUID) -> MatchListRe
.options(
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.treatments),
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.experiences),
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.loved_one_treatments),
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.loved_one_experiences),
joinedload(Match.volunteer).joinedload(User.volunteer_data),
joinedload(Match.match_status),
joinedload(Match.suggested_time_blocks),
Expand All @@ -685,6 +687,46 @@ async def get_matches_for_participant(self, participant_id: UUID) -> MatchListRe
self.logger.error(f"Error fetching matches for participant {participant_id}: {exc}")
raise HTTPException(status_code=500, detail="Failed to fetch matches")

async def get_all_matches_for_participant_admin(self, participant_id: UUID) -> MatchListResponse:
"""Get all matches for a participant including those awaiting volunteer acceptance (admin only)."""
try:
# Get participant to check pending request flag
participant: User | None = self.db.get(User, participant_id)
if not participant:
raise HTTPException(404, f"Participant {participant_id} not found")

# Get ALL matches including those awaiting volunteer acceptance
matches: List[Match] = (
self.db.query(Match)
.options(
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.treatments),
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.experiences),
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.loved_one_treatments),
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.loved_one_experiences),
joinedload(Match.volunteer).joinedload(User.volunteer_data),
joinedload(Match.match_status),
joinedload(Match.suggested_time_blocks),
joinedload(Match.confirmed_time),
)
.filter(
Match.participant_id == participant_id,
Match.deleted_at.is_(None),
)
.order_by(Match.created_at.desc())
.all()
)

responses = [self._build_match_detail(match) for match in matches]
return MatchListResponse(
matches=responses,
has_pending_request=participant.pending_volunteer_request or False,
)
except HTTPException:
raise
except Exception as exc:
self.logger.error(f"Error fetching all matches for participant {participant_id}: {exc}")
raise HTTPException(status_code=500, detail="Failed to fetch matches")

async def get_matches_for_volunteer(self, volunteer_id: UUID) -> MatchListForVolunteerResponse:
"""Get all matches for a volunteer, including those awaiting acceptance."""
try:
Expand All @@ -699,6 +741,8 @@ async def get_matches_for_volunteer(self, volunteer_id: UUID) -> MatchListForVol
.options(
joinedload(Match.participant).joinedload(User.user_data).joinedload(UserData.treatments),
joinedload(Match.participant).joinedload(User.user_data).joinedload(UserData.experiences),
joinedload(Match.participant).joinedload(User.user_data).joinedload(UserData.loved_one_treatments),
joinedload(Match.participant).joinedload(User.user_data).joinedload(UserData.loved_one_experiences),
joinedload(Match.match_status),
)
.filter(Match.volunteer_id == volunteer_id, Match.deleted_at.is_(None))
Expand Down Expand Up @@ -822,6 +866,9 @@ def _build_match_detail_for_volunteer(self, match: Match) -> MatchDetailForVolun
treatments: List[str] = []
experiences: List[str] = []
timezone: Optional[str] = None
loved_one_diagnosis: Optional[str] = None
loved_one_treatments: List[str] = []
loved_one_experiences: List[str] = []

if participant_data:
if participant_data.pronouns:
Expand All @@ -839,6 +886,13 @@ def _build_match_detail_for_volunteer(self, match: Match) -> MatchDetailForVolun

timezone = participant_data.timezone

# Add loved one data
loved_one_diagnosis = participant_data.loved_one_diagnosis
if participant_data.loved_one_treatments:
loved_one_treatments = [t.name for t in participant_data.loved_one_treatments if t and t.name]
if participant_data.loved_one_experiences:
loved_one_experiences = [e.name for e in participant_data.loved_one_experiences if e and e.name]

participant_summary = MatchParticipantSummary(
id=participant.id,
first_name=participant.first_name,
Expand All @@ -850,6 +904,9 @@ def _build_match_detail_for_volunteer(self, match: Match) -> MatchDetailForVolun
treatments=treatments,
experiences=experiences,
timezone=timezone,
loved_one_diagnosis=loved_one_diagnosis,
loved_one_treatments=loved_one_treatments,
loved_one_experiences=loved_one_experiences,
)

match_status_name = match.match_status.name if match.match_status else ""
Expand Down Expand Up @@ -940,6 +997,9 @@ def _build_match_detail(self, match: Match) -> MatchDetailResponse:
treatments: List[str] = []
experiences: List[str] = []
overview: Optional[str] = None
loved_one_diagnosis: Optional[str] = None
loved_one_treatments: List[str] = []
loved_one_experiences: List[str] = []

if volunteer_data:
if volunteer_data.pronouns:
Expand All @@ -957,6 +1017,13 @@ def _build_match_detail(self, match: Match) -> MatchDetailResponse:
if volunteer_data.experiences:
experiences = [e.name for e in volunteer_data.experiences if e and e.name]

# Add loved one data
loved_one_diagnosis = volunteer_data.loved_one_diagnosis
if volunteer_data.loved_one_treatments:
loved_one_treatments = [t.name for t in volunteer_data.loved_one_treatments if t and t.name]
if volunteer_data.loved_one_experiences:
loved_one_experiences = [e.name for e in volunteer_data.loved_one_experiences if e and e.name]

# Get overview/experience from volunteer_data table
if volunteer_data_record and volunteer_data_record.experience:
overview = volunteer_data_record.experience
Expand All @@ -974,6 +1041,9 @@ def _build_match_detail(self, match: Match) -> MatchDetailResponse:
treatments=treatments,
experiences=experiences,
overview=overview,
loved_one_diagnosis=loved_one_diagnosis,
loved_one_treatments=loved_one_treatments,
loved_one_experiences=loved_one_experiences,
)

suggested_blocks = [
Expand Down
Loading