Skip to content

Commit 0c57f7c

Browse files
authored
Updated admin-facing matching and status screens (#93)
## Notion ticket link Created admin-facing status screens for matching and also made some upgrades to the admin-facing matching screen like rendering extra columns based on the user's ranking preferences. Also some small bug fixes related to forms and the matching algo. <!-- Please replace with your ticket's URL --> [Ticket Name](https://www.notion.so/uwblueprintexecs/Task-Board-db95cd7b93f245f78ee85e3a8a6a316d) <!-- Give a quick summary of the implementation details, provide design justifications if necessary --> ## Implementation description * <!-- What should the reviewer do to verify your changes? Describe expected results and include screenshots when appropriate --> ## Steps to test 1. <!-- Draw attention to the substantial parts of your PR or anything you'd like a second opinion on --> ## What should reviewers focus on? * ## Checklist - [ ] My PR name is descriptive and in imperative tense - [ ] My commit messages are descriptive and in imperative tense. My commits are atomic and trivial commits are squashed or fixup'd into non-trivial commits - [ ] I have run the appropriate linter(s) - [ ] I have requested a review from the PL, as well as other devs who have background knowledge on this PR or who will be building on top of this PR
1 parent 64328c2 commit 0c57f7c

31 files changed

+2413
-348
lines changed

backend/app/routes/match.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,24 @@ async def get_matches_for_participant(
8989
match_service: MatchService = Depends(get_match_service),
9090
_authorized: bool = has_roles([UserRole.ADMIN]),
9191
):
92+
"""Get matches for a participant (admin only). Includes all matches including awaiting_volunteer_acceptance."""
9293
try:
93-
return await match_service.get_matches_for_participant(participant_id)
94+
return await match_service.get_all_matches_for_participant_admin(participant_id)
95+
except HTTPException as http_ex:
96+
raise http_ex
97+
except Exception as e:
98+
raise HTTPException(status_code=500, detail=str(e))
99+
100+
101+
@router.get("/volunteer/{volunteer_id}", response_model=MatchListForVolunteerResponse)
102+
async def get_matches_for_volunteer_admin(
103+
volunteer_id: UUID,
104+
match_service: MatchService = Depends(get_match_service),
105+
_authorized: bool = has_roles([UserRole.ADMIN]),
106+
):
107+
"""Get all matches for a volunteer (admin only)."""
108+
try:
109+
return await match_service.get_matches_for_volunteer(volunteer_id)
94110
except HTTPException as http_ex:
95111
raise http_ex
96112
except Exception as e:

backend/app/schemas/match.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class MatchVolunteerSummary(BaseModel):
5555
treatments: List[str] = Field(default_factory=list)
5656
experiences: List[str] = Field(default_factory=list)
5757
overview: Optional[str] = None # Volunteer experience/overview from volunteer_data
58+
loved_one_diagnosis: Optional[str] = None
59+
loved_one_treatments: List[str] = Field(default_factory=list)
60+
loved_one_experiences: List[str] = Field(default_factory=list)
5861

5962
model_config = ConfigDict(from_attributes=True)
6063

@@ -70,6 +73,9 @@ class MatchParticipantSummary(BaseModel):
7073
treatments: List[str] = Field(default_factory=list)
7174
experiences: List[str] = Field(default_factory=list)
7275
timezone: Optional[str] = None
76+
loved_one_diagnosis: Optional[str] = None
77+
loved_one_treatments: List[str] = Field(default_factory=list)
78+
loved_one_experiences: List[str] = Field(default_factory=list)
7379

7480
model_config = ConfigDict(from_attributes=True)
7581

backend/app/schemas/matching.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,21 @@ class AdminMatchCandidate(BaseModel):
4040
timezone: Optional[str]
4141
age: Optional[int]
4242
diagnosis: Optional[str]
43+
date_of_diagnosis: Optional[str] = None # ISO format date string
4344
treatments: List[str] = []
4445
experiences: List[str] = []
4546
match_score: float # 0-100 scale
47+
match_count: int = 0 # Number of active matches for this volunteer
48+
# Additional fields for dynamic columns based on preferences
49+
marital_status: Optional[str] = None
50+
gender_identity: Optional[str] = None
51+
ethnic_group: Optional[List[str]] = None
52+
has_kids: Optional[str] = None
53+
loved_one_age: Optional[str] = None
54+
loved_one_diagnosis: Optional[str] = None
55+
loved_one_date_of_diagnosis: Optional[str] = None # ISO format date string
56+
loved_one_treatments: List[str] = []
57+
loved_one_experiences: List[str] = []
4658

4759

4860
class AdminMatchesResponse(BaseModel):

backend/app/schemas/user.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class UserCreateResponse(BaseModel):
128128
auth_id: str
129129
approved: bool
130130
active: bool
131+
pending_volunteer_request: bool
131132
form_status: FormStatus
132133
language: Language
133134

@@ -148,12 +149,14 @@ class UserResponse(BaseModel):
148149
auth_id: str
149150
approved: bool
150151
active: bool
152+
pending_volunteer_request: bool
151153
role: "RoleResponse"
152154
form_status: FormStatus
153155
user_data: Optional[UserDataResponse] = None
154156
volunteer_data: Optional[VolunteerDataResponse] = None
155157
availability: List[AvailabilityTemplateSlot] = []
156158
language: Language
159+
match_count: int = Field(default=0, description="Number of active matches for this user")
157160

158161
model_config = ConfigDict(from_attributes=True)
159162

backend/app/schemas/user_data.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ class UserDataUpdateRequest(BaseModel):
9191
marital_status: Optional[str] = None
9292
has_kids: Optional[str] = None
9393
timezone: Optional[str] = None
94+
language: Optional[str] = Field(None, description="Preferred language (en or fr)")
9495

9596
# User's Cancer Experience
9697
diagnosis: Optional[str] = None

backend/app/seeds/users.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ def seed_users(session: Session) -> None:
357357
"city": "Calgary",
358358
"province": "Alberta",
359359
"postal_code": "T2P 1J4",
360-
"gender_identity": "Man",
360+
"gender_identity": "Male",
361361
"pronouns": ["he", "him"],
362362
"ethnic_group": ["White/Caucasian"],
363363
"marital_status": "Single",
@@ -366,10 +366,15 @@ def seed_users(session: Session) -> None:
366366
"diagnosis": "Non-Hodgkin Lymphoma",
367367
"date_of_diagnosis": date(2019, 6, 10),
368368
"has_blood_cancer": "yes",
369-
"caring_for_someone": "no",
369+
"caring_for_someone": "yes",
370+
"loved_one_age": "26",
371+
"loved_one_diagnosis": "Blood Cancer",
372+
"loved_one_date_of_diagnosis": date(2023, 2, 20),
370373
},
371374
"treatments": [3, 6, 14], # Chemotherapy, Radiation, BTK Inhibitors
372375
"experiences": [1, 3, 4, 7], # Brain Fog, Feeling Overwhelmed, Fatigue, Returning to work
376+
"loved_one_treatments": [3, 6], # Chemotherapy, Radiation
377+
"loved_one_experiences": [1, 3, 4, 7], # Brain Fog, Feeling Overwhelmed, Fatigue, Returning to work
373378
},
374379
{
375380
"role": "volunteer",

backend/app/server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
)
2828
from .services.implementations.match_completion_service import MatchCompletionService
2929
from .utilities.constants import LOGGER_NAME
30+
from .utilities.db_utils import engine
3031
from .utilities.firebase_init import initialize_firebase
3132
from .utilities.ses.ses_init import ensure_ses_templates
3233

@@ -87,6 +88,11 @@ async def lifespan(_: FastAPI):
8788
# Shutdown scheduler gracefully
8889
log.info("Shutting down scheduler...")
8990
scheduler.shutdown()
91+
92+
# Dispose database engine to close all connection pools
93+
# This prevents async generator cleanup errors during shutdown
94+
log.info("Disposing database engine...")
95+
engine.dispose()
9096
log.info("Shutting down...")
9197

9298

backend/app/services/implementations/form_processor.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ def _process_ranking_form(self, submission: FormSubmission, user: User) -> None:
106106
if not target:
107107
raise ValueError("Ranking form missing 'target' field")
108108

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

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

138+
# Set pending_volunteer_request flag so admin knows to create matches
139+
user.pending_volunteer_request = True
140+
138141
# Create a MATCHING task so admins know this participant is ready to be matched
139142
try:
140143
matching_task = Task(

backend/app/services/implementations/match_service.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,8 @@ async def get_matches_for_participant(self, participant_id: UUID) -> MatchListRe
660660
.options(
661661
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.treatments),
662662
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.experiences),
663+
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.loved_one_treatments),
664+
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.loved_one_experiences),
663665
joinedload(Match.volunteer).joinedload(User.volunteer_data),
664666
joinedload(Match.match_status),
665667
joinedload(Match.suggested_time_blocks),
@@ -685,6 +687,46 @@ async def get_matches_for_participant(self, participant_id: UUID) -> MatchListRe
685687
self.logger.error(f"Error fetching matches for participant {participant_id}: {exc}")
686688
raise HTTPException(status_code=500, detail="Failed to fetch matches")
687689

690+
async def get_all_matches_for_participant_admin(self, participant_id: UUID) -> MatchListResponse:
691+
"""Get all matches for a participant including those awaiting volunteer acceptance (admin only)."""
692+
try:
693+
# Get participant to check pending request flag
694+
participant: User | None = self.db.get(User, participant_id)
695+
if not participant:
696+
raise HTTPException(404, f"Participant {participant_id} not found")
697+
698+
# Get ALL matches including those awaiting volunteer acceptance
699+
matches: List[Match] = (
700+
self.db.query(Match)
701+
.options(
702+
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.treatments),
703+
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.experiences),
704+
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.loved_one_treatments),
705+
joinedload(Match.volunteer).joinedload(User.user_data).joinedload(UserData.loved_one_experiences),
706+
joinedload(Match.volunteer).joinedload(User.volunteer_data),
707+
joinedload(Match.match_status),
708+
joinedload(Match.suggested_time_blocks),
709+
joinedload(Match.confirmed_time),
710+
)
711+
.filter(
712+
Match.participant_id == participant_id,
713+
Match.deleted_at.is_(None),
714+
)
715+
.order_by(Match.created_at.desc())
716+
.all()
717+
)
718+
719+
responses = [self._build_match_detail(match) for match in matches]
720+
return MatchListResponse(
721+
matches=responses,
722+
has_pending_request=participant.pending_volunteer_request or False,
723+
)
724+
except HTTPException:
725+
raise
726+
except Exception as exc:
727+
self.logger.error(f"Error fetching all matches for participant {participant_id}: {exc}")
728+
raise HTTPException(status_code=500, detail="Failed to fetch matches")
729+
688730
async def get_matches_for_volunteer(self, volunteer_id: UUID) -> MatchListForVolunteerResponse:
689731
"""Get all matches for a volunteer, including those awaiting acceptance."""
690732
try:
@@ -699,6 +741,8 @@ async def get_matches_for_volunteer(self, volunteer_id: UUID) -> MatchListForVol
699741
.options(
700742
joinedload(Match.participant).joinedload(User.user_data).joinedload(UserData.treatments),
701743
joinedload(Match.participant).joinedload(User.user_data).joinedload(UserData.experiences),
744+
joinedload(Match.participant).joinedload(User.user_data).joinedload(UserData.loved_one_treatments),
745+
joinedload(Match.participant).joinedload(User.user_data).joinedload(UserData.loved_one_experiences),
702746
joinedload(Match.match_status),
703747
)
704748
.filter(Match.volunteer_id == volunteer_id, Match.deleted_at.is_(None))
@@ -822,6 +866,9 @@ def _build_match_detail_for_volunteer(self, match: Match) -> MatchDetailForVolun
822866
treatments: List[str] = []
823867
experiences: List[str] = []
824868
timezone: Optional[str] = None
869+
loved_one_diagnosis: Optional[str] = None
870+
loved_one_treatments: List[str] = []
871+
loved_one_experiences: List[str] = []
825872

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

840887
timezone = participant_data.timezone
841888

889+
# Add loved one data
890+
loved_one_diagnosis = participant_data.loved_one_diagnosis
891+
if participant_data.loved_one_treatments:
892+
loved_one_treatments = [t.name for t in participant_data.loved_one_treatments if t and t.name]
893+
if participant_data.loved_one_experiences:
894+
loved_one_experiences = [e.name for e in participant_data.loved_one_experiences if e and e.name]
895+
842896
participant_summary = MatchParticipantSummary(
843897
id=participant.id,
844898
first_name=participant.first_name,
@@ -850,6 +904,9 @@ def _build_match_detail_for_volunteer(self, match: Match) -> MatchDetailForVolun
850904
treatments=treatments,
851905
experiences=experiences,
852906
timezone=timezone,
907+
loved_one_diagnosis=loved_one_diagnosis,
908+
loved_one_treatments=loved_one_treatments,
909+
loved_one_experiences=loved_one_experiences,
853910
)
854911

855912
match_status_name = match.match_status.name if match.match_status else ""
@@ -940,6 +997,9 @@ def _build_match_detail(self, match: Match) -> MatchDetailResponse:
940997
treatments: List[str] = []
941998
experiences: List[str] = []
942999
overview: Optional[str] = None
1000+
loved_one_diagnosis: Optional[str] = None
1001+
loved_one_treatments: List[str] = []
1002+
loved_one_experiences: List[str] = []
9431003

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

1020+
# Add loved one data
1021+
loved_one_diagnosis = volunteer_data.loved_one_diagnosis
1022+
if volunteer_data.loved_one_treatments:
1023+
loved_one_treatments = [t.name for t in volunteer_data.loved_one_treatments if t and t.name]
1024+
if volunteer_data.loved_one_experiences:
1025+
loved_one_experiences = [e.name for e in volunteer_data.loved_one_experiences if e and e.name]
1026+
9601027
# Get overview/experience from volunteer_data table
9611028
if volunteer_data_record and volunteer_data_record.experience:
9621029
overview = volunteer_data_record.experience
@@ -974,6 +1041,9 @@ def _build_match_detail(self, match: Match) -> MatchDetailResponse:
9741041
treatments=treatments,
9751042
experiences=experiences,
9761043
overview=overview,
1044+
loved_one_diagnosis=loved_one_diagnosis,
1045+
loved_one_treatments=loved_one_treatments,
1046+
loved_one_experiences=loved_one_experiences,
9771047
)
9781048

9791049
suggested_blocks = [

0 commit comments

Comments
 (0)