Skip to content

Commit 84470d0

Browse files
authored
Volunteer dash (#80)
## Notion ticket link <!-- 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 f59ad23 commit 84470d0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+6030
-27
lines changed

backend/app/routes/auth.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717
# TODO: ADD RATE LIMITING
1818
@router.post("/register", response_model=UserCreateResponse)
1919
async def register_user(user: UserCreateRequest, user_service: UserService = Depends(get_user_service)):
20-
20+
allowed_Admins = [
21+
22+
23+
24+
25+
]
2126
if user.role == UserRole.ADMIN:
2227
if user.email not in allowed_Admins:
2328
raise HTTPException(status_code=403, detail="Access denied. Admin privileges required for admin portal")

backend/app/routes/availability.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,26 @@ async def create_availability(
6060
raise HTTPException(status_code=500, detail=str(e))
6161

6262

63+
@router.put("/", response_model=CreateAvailabilityResponse)
64+
async def update_availability(
65+
availability: CreateAvailabilityRequest,
66+
availability_service: AvailabilityService = Depends(get_availability_service),
67+
authorized: bool = has_roles([UserRole.ADMIN, UserRole.VOLUNTEER]),
68+
):
69+
"""
70+
Completely replaces user's availability with the provided time slots.
71+
Deletes all existing availability and creates new ones.
72+
"""
73+
try:
74+
updated = await availability_service.update_availability(availability)
75+
return updated
76+
except HTTPException as http_ex:
77+
raise http_ex
78+
except Exception as e:
79+
print(e)
80+
raise HTTPException(status_code=500, detail=str(e))
81+
82+
6383
@router.delete("/", response_model=DeleteAvailabilityResponse)
6484
async def delete_availability(
6585
availability: DeleteAvailabilityRequest,

backend/app/routes/user_data.py

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
from datetime import datetime as dt
2+
from typing import List, Optional
3+
4+
from fastapi import APIRouter, Depends, HTTPException, Request
5+
from pydantic import BaseModel, ConfigDict
6+
from sqlalchemy.orm import Session
7+
8+
from app.middleware.auth import has_roles
9+
from app.models import Experience, Treatment, User, UserData
10+
from app.schemas.user import UserRole
11+
from app.utilities.db_utils import get_db
12+
13+
router = APIRouter(
14+
prefix="/user-data",
15+
tags=["user-data"],
16+
)
17+
18+
19+
# ===== Schemas =====
20+
21+
22+
class TreatmentResponse(BaseModel):
23+
id: int
24+
name: str
25+
model_config = ConfigDict(from_attributes=True)
26+
27+
28+
class ExperienceResponse(BaseModel):
29+
id: int
30+
name: str
31+
scope: str
32+
model_config = ConfigDict(from_attributes=True)
33+
34+
35+
class AvailabilityTemplateResponse(BaseModel):
36+
"""Response schema for availability template"""
37+
38+
day_of_week: int # 0=Monday, 1=Tuesday, ..., 6=Sunday
39+
start_time: str # Time string in format "HH:MM:SS"
40+
end_time: str # Time string in format "HH:MM:SS"
41+
model_config = ConfigDict(from_attributes=True)
42+
43+
44+
class UserDataResponse(BaseModel):
45+
"""Response schema for UserData with all relationships resolved"""
46+
47+
# Personal Information
48+
first_name: Optional[str] = None
49+
last_name: Optional[str] = None
50+
email: Optional[str] = None
51+
phone: Optional[str] = None
52+
date_of_birth: Optional[str] = None
53+
city: Optional[str] = None
54+
province: Optional[str] = None
55+
postal_code: Optional[str] = None
56+
57+
# Demographics
58+
gender_identity: Optional[str] = None
59+
pronouns: Optional[List[str]] = None
60+
ethnic_group: Optional[List[str]] = None
61+
marital_status: Optional[str] = None
62+
has_kids: Optional[str] = None
63+
other_ethnic_group: Optional[str] = None
64+
gender_identity_custom: Optional[str] = None
65+
66+
# Cancer Experience
67+
diagnosis: Optional[str] = None
68+
date_of_diagnosis: Optional[str] = None
69+
treatments: List[str] = []
70+
experiences: List[str] = []
71+
72+
# Loved One Information
73+
loved_one_gender_identity: Optional[str] = None
74+
loved_one_age: Optional[str] = None
75+
loved_one_diagnosis: Optional[str] = None
76+
loved_one_date_of_diagnosis: Optional[str] = None
77+
loved_one_treatments: List[str] = []
78+
loved_one_experiences: List[str] = []
79+
80+
# Flow Control
81+
has_blood_cancer: Optional[bool] = None
82+
caring_for_someone: Optional[bool] = None
83+
84+
# Availability (list of availability templates)
85+
availability: List[AvailabilityTemplateResponse] = []
86+
87+
88+
# ===== Endpoints =====
89+
90+
91+
@router.get("/me", response_model=UserDataResponse)
92+
async def get_my_user_data(
93+
request: Request,
94+
db: Session = Depends(get_db),
95+
authorized: bool = has_roles([UserRole.PARTICIPANT, UserRole.VOLUNTEER, UserRole.ADMIN]),
96+
):
97+
"""
98+
Get the current user's UserData with all relationships resolved.
99+
100+
Returns all fields from UserData including:
101+
- Personal information
102+
- Demographics
103+
- Cancer experience with treatments and experiences
104+
- Loved one information with treatments and experiences
105+
"""
106+
try:
107+
# Get current user from auth middleware
108+
current_user_auth_id = request.state.user_id
109+
current_user = db.query(User).filter(User.auth_id == current_user_auth_id).first()
110+
111+
if not current_user:
112+
raise HTTPException(status_code=401, detail="User not found")
113+
114+
# Get UserData for current user
115+
user_data = db.query(UserData).filter(UserData.user_id == current_user.id).first()
116+
117+
if not user_data:
118+
raise HTTPException(status_code=404, detail="User data not found")
119+
120+
# Get Availability templates for current user (only active ones)
121+
availability_templates = [
122+
AvailabilityTemplateResponse(
123+
day_of_week=template.day_of_week,
124+
start_time=template.start_time.isoformat(),
125+
end_time=template.end_time.isoformat(),
126+
)
127+
for template in current_user.availability_templates
128+
if template.is_active
129+
]
130+
131+
# Build response with all fields and resolved relationships
132+
response = UserDataResponse(
133+
# Personal Information
134+
first_name=user_data.first_name,
135+
last_name=user_data.last_name,
136+
email=user_data.email,
137+
phone=user_data.phone,
138+
date_of_birth=user_data.date_of_birth.isoformat() if user_data.date_of_birth else None,
139+
city=user_data.city,
140+
province=user_data.province,
141+
postal_code=user_data.postal_code,
142+
# Demographics
143+
gender_identity=user_data.gender_identity,
144+
pronouns=user_data.pronouns,
145+
ethnic_group=user_data.ethnic_group,
146+
marital_status=user_data.marital_status,
147+
has_kids=user_data.has_kids,
148+
other_ethnic_group=user_data.other_ethnic_group,
149+
gender_identity_custom=user_data.gender_identity_custom,
150+
# Cancer Experience
151+
diagnosis=user_data.diagnosis,
152+
date_of_diagnosis=user_data.date_of_diagnosis.isoformat() if user_data.date_of_diagnosis else None,
153+
treatments=[treatment.name for treatment in user_data.treatments],
154+
experiences=[experience.name for experience in user_data.experiences],
155+
# Loved One Information
156+
loved_one_gender_identity=user_data.loved_one_gender_identity,
157+
loved_one_age=user_data.loved_one_age,
158+
loved_one_diagnosis=user_data.loved_one_diagnosis,
159+
loved_one_date_of_diagnosis=(
160+
user_data.loved_one_date_of_diagnosis.isoformat() if user_data.loved_one_date_of_diagnosis else None
161+
),
162+
loved_one_treatments=[treatment.name for treatment in user_data.loved_one_treatments],
163+
loved_one_experiences=[experience.name for experience in user_data.loved_one_experiences],
164+
# Flow Control
165+
has_blood_cancer=user_data.has_blood_cancer,
166+
caring_for_someone=user_data.caring_for_someone,
167+
# Availability
168+
availability=availability_templates,
169+
)
170+
171+
return response
172+
173+
except HTTPException:
174+
raise
175+
except Exception as e:
176+
raise HTTPException(status_code=500, detail=str(e))
177+
178+
179+
@router.put("/me", response_model=UserDataResponse)
180+
async def update_my_user_data(
181+
update_data: dict,
182+
request: Request,
183+
db: Session = Depends(get_db),
184+
authorized: bool = has_roles([UserRole.PARTICIPANT, UserRole.VOLUNTEER, UserRole.ADMIN]),
185+
):
186+
"""
187+
Update the current user's UserData.
188+
189+
Accepts a partial update - only provided fields will be updated.
190+
Handles both simple fields and many-to-many relationships (treatments, experiences).
191+
"""
192+
try:
193+
# Get current user from auth middleware
194+
current_user_auth_id = request.state.user_id
195+
current_user = db.query(User).filter(User.auth_id == current_user_auth_id).first()
196+
197+
if not current_user:
198+
raise HTTPException(status_code=401, detail="User not found")
199+
200+
# Get UserData for current user
201+
user_data = db.query(UserData).filter(UserData.user_id == current_user.id).first()
202+
203+
if not user_data:
204+
raise HTTPException(status_code=404, detail="User data not found")
205+
206+
# Update simple fields
207+
simple_fields = [
208+
"first_name",
209+
"last_name",
210+
"email",
211+
"phone",
212+
"city",
213+
"province",
214+
"postal_code",
215+
"gender_identity",
216+
"marital_status",
217+
"has_kids",
218+
"other_ethnic_group",
219+
"gender_identity_custom",
220+
"diagnosis",
221+
"loved_one_gender_identity",
222+
"loved_one_age",
223+
"loved_one_diagnosis",
224+
"has_blood_cancer",
225+
"caring_for_someone",
226+
]
227+
228+
for field in simple_fields:
229+
if field in update_data:
230+
setattr(user_data, field, update_data[field])
231+
232+
# Update array fields
233+
array_fields = ["pronouns", "ethnic_group"]
234+
for field in array_fields:
235+
if field in update_data:
236+
setattr(user_data, field, update_data[field])
237+
238+
# Update date fields
239+
if "date_of_birth" in update_data and update_data["date_of_birth"]:
240+
try:
241+
user_data.date_of_birth = dt.fromisoformat(update_data["date_of_birth"]).date()
242+
except (ValueError, AttributeError):
243+
# Try parsing DD/MM/YYYY format
244+
try:
245+
user_data.date_of_birth = dt.strptime(update_data["date_of_birth"], "%d/%m/%Y").date()
246+
except ValueError:
247+
pass
248+
249+
if "date_of_diagnosis" in update_data and update_data["date_of_diagnosis"]:
250+
try:
251+
user_data.date_of_diagnosis = dt.fromisoformat(update_data["date_of_diagnosis"]).date()
252+
except (ValueError, AttributeError):
253+
try:
254+
user_data.date_of_diagnosis = dt.strptime(update_data["date_of_diagnosis"], "%d/%m/%Y").date()
255+
except ValueError:
256+
pass
257+
258+
if "loved_one_date_of_diagnosis" in update_data and update_data["loved_one_date_of_diagnosis"]:
259+
try:
260+
user_data.loved_one_date_of_diagnosis = dt.fromisoformat(
261+
update_data["loved_one_date_of_diagnosis"]
262+
).date()
263+
except (ValueError, AttributeError):
264+
try:
265+
user_data.loved_one_date_of_diagnosis = dt.strptime(
266+
update_data["loved_one_date_of_diagnosis"], "%d/%m/%Y"
267+
).date()
268+
except ValueError:
269+
pass
270+
271+
# Update treatments (many-to-many)
272+
if "treatments" in update_data:
273+
user_data.treatments.clear()
274+
for treatment_name in update_data["treatments"]:
275+
treatment = db.query(Treatment).filter(Treatment.name == treatment_name).first()
276+
if treatment:
277+
user_data.treatments.append(treatment)
278+
279+
# Update experiences (many-to-many)
280+
if "experiences" in update_data:
281+
user_data.experiences.clear()
282+
for experience_name in update_data["experiences"]:
283+
experience = db.query(Experience).filter(Experience.name == experience_name).first()
284+
if experience:
285+
user_data.experiences.append(experience)
286+
287+
# Update loved one treatments
288+
if "loved_one_treatments" in update_data:
289+
user_data.loved_one_treatments.clear()
290+
for treatment_name in update_data["loved_one_treatments"]:
291+
treatment = db.query(Treatment).filter(Treatment.name == treatment_name).first()
292+
if treatment:
293+
user_data.loved_one_treatments.append(treatment)
294+
295+
# Update loved one experiences
296+
if "loved_one_experiences" in update_data:
297+
user_data.loved_one_experiences.clear()
298+
for experience_name in update_data["loved_one_experiences"]:
299+
experience = db.query(Experience).filter(Experience.name == experience_name).first()
300+
if experience:
301+
user_data.loved_one_experiences.append(experience)
302+
303+
db.commit()
304+
db.refresh(user_data)
305+
306+
# Return updated data using the same logic as GET
307+
availability_templates = [
308+
AvailabilityTemplateResponse(
309+
day_of_week=template.day_of_week,
310+
start_time=template.start_time.isoformat(),
311+
end_time=template.end_time.isoformat(),
312+
)
313+
for template in current_user.availability_templates
314+
if template.is_active
315+
]
316+
317+
response = UserDataResponse(
318+
first_name=user_data.first_name,
319+
last_name=user_data.last_name,
320+
email=user_data.email,
321+
phone=user_data.phone,
322+
date_of_birth=user_data.date_of_birth.isoformat() if user_data.date_of_birth else None,
323+
city=user_data.city,
324+
province=user_data.province,
325+
postal_code=user_data.postal_code,
326+
gender_identity=user_data.gender_identity,
327+
pronouns=user_data.pronouns,
328+
ethnic_group=user_data.ethnic_group,
329+
marital_status=user_data.marital_status,
330+
has_kids=user_data.has_kids,
331+
other_ethnic_group=user_data.other_ethnic_group,
332+
gender_identity_custom=user_data.gender_identity_custom,
333+
diagnosis=user_data.diagnosis,
334+
date_of_diagnosis=user_data.date_of_diagnosis.isoformat() if user_data.date_of_diagnosis else None,
335+
treatments=[treatment.name for treatment in user_data.treatments],
336+
experiences=[experience.name for experience in user_data.experiences],
337+
loved_one_gender_identity=user_data.loved_one_gender_identity,
338+
loved_one_age=user_data.loved_one_age,
339+
loved_one_diagnosis=user_data.loved_one_diagnosis,
340+
loved_one_date_of_diagnosis=(
341+
user_data.loved_one_date_of_diagnosis.isoformat() if user_data.loved_one_date_of_diagnosis else None
342+
),
343+
loved_one_treatments=[treatment.name for treatment in user_data.loved_one_treatments],
344+
loved_one_experiences=[experience.name for experience in user_data.loved_one_experiences],
345+
has_blood_cancer=user_data.has_blood_cancer,
346+
caring_for_someone=user_data.caring_for_someone,
347+
availability=availability_templates,
348+
)
349+
350+
return response
351+
352+
except HTTPException:
353+
raise
354+
except Exception as e:
355+
db.rollback()
356+
raise HTTPException(status_code=500, detail=str(e))

0 commit comments

Comments
 (0)