Skip to content

Commit e6830d8

Browse files
chene0ryan-n-gunawanyut-code
authored
intake form experiences update (#58)
## Notion ticket link <!-- Please replace with your ticket's URL --> [Intake Form Experiences Update](https://www.notion.so/uwblueprintexecs/Intake-Form-Experiences-Update-27210f3fb1dc8070927ddee67e07d232?v=27210f3fb1dc8133a6e1000cf4eabc81&source=copy_link) <!-- Give a quick summary of the implementation details, provide design justifications if necessary --> ## Implementation description * Add a scope enum column to experiences (patient, caregiver, both, none), wiping existing entries for consistency. Update the seeder to include experiences with scopes; treatments seeder stays as-is. Create a new API endpoint to return filtered experiences (by target) and all treatments. Update frontend forms to fetch options from the API instead of hardcoded constants. Remove all "Other" treatment/experience fields in forms, interfaces, and backend processing. Update or add tests to cover API changes and remove “other” field logic. <!-- 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 --------- Co-authored-by: Ryan Gunawan <[email protected]> Co-authored-by: Teresa Yu <[email protected]> Co-authored-by: IceRat1 <[email protected]>
1 parent 4e3fdbe commit e6830d8

File tree

21 files changed

+633
-507
lines changed

21 files changed

+633
-507
lines changed

backend/app/models/Experience.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from sqlalchemy import Column, Integer, String
1+
from sqlalchemy import Column, Enum, Integer, String
22
from sqlalchemy.orm import relationship
33

44
from .Base import Base
@@ -8,6 +8,7 @@ class Experience(Base):
88
__tablename__ = "experiences"
99
id = Column(Integer, primary_key=True)
1010
name = Column(String, unique=True, nullable=False) # 'PTSD', 'Relapse', etc.
11+
scope = Column(Enum("patient", "caregiver", "both", "none", name="scope"), nullable=False)
1112

1213
# Back reference for many-to-many relationship
1314
users = relationship("UserData", secondary="user_experiences", back_populates="experiences")

backend/app/models/UserData.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ class UserData(Base):
6565
date_of_diagnosis = Column(Date, nullable=True)
6666

6767
# "Other" text fields for custom entries
68-
other_treatment = Column(Text, nullable=True)
69-
other_experience = Column(Text, nullable=True)
7068
other_ethnic_group = Column(Text, nullable=True)
7169
gender_identity_custom = Column(Text, nullable=True)
7270

@@ -81,8 +79,6 @@ class UserData(Base):
8179
# Loved One Cancer Experience
8280
loved_one_diagnosis = Column(String(100), nullable=True)
8381
loved_one_date_of_diagnosis = Column(Date, nullable=True)
84-
loved_one_other_treatment = Column(Text, nullable=True)
85-
loved_one_other_experience = Column(Text, nullable=True)
8682

8783
# Many-to-many relationships
8884
treatments = relationship("Treatment", secondary=user_treatments, back_populates="users")

backend/app/routes/intake.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from datetime import datetime
2-
from typing import List, Optional
2+
from typing import List, Literal, Optional
33
from uuid import UUID
44

55
from fastapi import APIRouter, Depends, HTTPException, Query, Request
66
from pydantic import BaseModel, ConfigDict, Field
7+
from sqlalchemy import or_
78
from sqlalchemy.orm import Session
89

910
from app.middleware.auth import has_roles
10-
from app.models import Form, FormSubmission, User
11+
from app.models import Experience, Form, FormSubmission, Treatment, User
1112
from app.schemas.user import UserRole
1213
from app.services.implementations.intake_form_processor import IntakeFormProcessor
1314
from app.utilities.db_utils import get_db
@@ -49,6 +50,24 @@ class FormSubmissionListResponse(BaseModel):
4950
total: int
5051

5152

53+
class ExperienceResponse(BaseModel):
54+
id: int
55+
name: str
56+
scope: Literal["patient", "caregiver", "both", "none"]
57+
model_config = ConfigDict(from_attributes=True)
58+
59+
60+
class TreatmentResponse(BaseModel):
61+
id: int
62+
name: str
63+
model_config = ConfigDict(from_attributes=True)
64+
65+
66+
class OptionsResponse(BaseModel):
67+
experiences: List[ExperienceResponse]
68+
treatments: List[TreatmentResponse]
69+
70+
5271
# ===== Custom Auth Dependencies =====
5372

5473

@@ -339,6 +358,31 @@ async def delete_form_submission(
339358
raise HTTPException(status_code=500, detail=str(e))
340359

341360

361+
@router.get(
362+
"/options",
363+
response_model=OptionsResponse,
364+
)
365+
async def get_intake_options(
366+
request: Request,
367+
target: str = Query(..., pattern="^(patient|caregiver|both)$"),
368+
db: Session = Depends(get_db),
369+
authorized: bool = has_roles([UserRole.PARTICIPANT, UserRole.VOLUNTEER, UserRole.ADMIN]),
370+
):
371+
try:
372+
# Query DB Experience Table
373+
experiences_query = db.query(Experience)
374+
if target != "both":
375+
experiences_query = experiences_query.filter(or_(Experience.scope == target, Experience.scope == "both"))
376+
experiences = experiences_query.order_by(Experience.id.asc()).all()
377+
378+
treatments = db.query(Treatment).order_by(Treatment.id.asc()).all()
379+
return OptionsResponse.model_validate({"experiences": experiences, "treatments": treatments})
380+
except HTTPException:
381+
raise
382+
except Exception as e:
383+
raise HTTPException(status_code=500, detail=str(e))
384+
385+
342386
# ===== Additional Utility Endpoints =====
343387

344388

backend/app/seeds/experiences.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,19 @@ def seed_experiences(session: Session) -> None:
99
"""Seed the experiences table with cancer-related experiences."""
1010

1111
experiences_data = [
12-
{"id": 1, "name": "Brain Fog"},
13-
{"id": 2, "name": "Communication Challenges"},
14-
{"id": 3, "name": "Compassion Fatigue"},
15-
{"id": 4, "name": "Feeling Overwhelmed"},
16-
{"id": 5, "name": "Fatigue"},
17-
{"id": 6, "name": "Fertility Issues"},
18-
{"id": 7, "name": "Graft vs Host"},
19-
{"id": 8, "name": "Returning to work or school after/during treatment"},
20-
{"id": 9, "name": "Speaking to your family or friends about the diagnosis"},
21-
{"id": 10, "name": "Relapse"},
22-
{"id": 11, "name": "Anxiety / Depression"},
23-
{"id": 12, "name": "PTSD"},
12+
{"id": 1, "name": "Brain Fog", "scope": "both"},
13+
{"id": 2, "name": "Communication Challenges", "scope": "caregiver"},
14+
{"id": 3, "name": "Feeling Overwhelmed", "scope": "both"},
15+
{"id": 4, "name": "Fatigue", "scope": "both"},
16+
{"id": 5, "name": "Fertility Issues", "scope": "patient"},
17+
{"id": 6, "name": "Graft vs Host", "scope": "patient"},
18+
{"id": 7, "name": "Returning to work or school after/during treatment", "scope": "patient"},
19+
{"id": 8, "name": "Speaking to your family or friends about the diagnosis", "scope": "both"},
20+
{"id": 9, "name": "Relapse", "scope": "patient"},
21+
{"id": 10, "name": "Anxiety / Depression", "scope": "both"},
22+
{"id": 11, "name": "PTSD", "scope": "both"},
23+
{"id": 12, "name": "Caregiver Fatigue", "scope": "caregiver"},
24+
{"id": 13, "name": "Managing practical challenges", "scope": "caregiver"},
2425
]
2526

2627
for experience_data in experiences_data:

backend/app/services/implementations/intake_form_processor.py

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,6 @@ def _process_demographics(self, user_data: UserData, demographics: Dict[str, Any
175175
def _process_cancer_experience(self, user_data: UserData, cancer_experience: Dict[str, Any]):
176176
"""Process cancer experience information."""
177177
user_data.diagnosis = self._trim_text(cancer_experience.get("diagnosis"))
178-
user_data.other_treatment = self._trim_text(cancer_experience.get("other_treatment"))
179-
user_data.other_experience = self._trim_text(cancer_experience.get("other_experience"))
180178

181179
# Parse diagnosis date with strict validation
182180
if "date_of_diagnosis" in cancer_experience:
@@ -213,13 +211,6 @@ def _process_treatments(self, user_data: UserData, cancer_exp: Dict[str, Any]):
213211

214212
if treatment:
215213
user_data.treatments.append(treatment)
216-
else:
217-
# Create new treatment for custom entry
218-
logger.info(f"Creating new treatment: {treatment_name}")
219-
new_treatment = Treatment(name=treatment_name)
220-
self.db.add(new_treatment)
221-
self.db.flush() # Get the ID
222-
user_data.treatments.append(new_treatment)
223214

224215
def _process_experiences(self, user_data: UserData, cancer_exp: Dict[str, Any]):
225216
"""
@@ -242,13 +233,6 @@ def _process_experiences(self, user_data: UserData, cancer_exp: Dict[str, Any]):
242233

243234
if experience:
244235
user_data.experiences.append(experience)
245-
else:
246-
# Create new experience for custom entry
247-
logger.info(f"Creating new experience: {experience_name}")
248-
new_experience = Experience(name=experience_name)
249-
self.db.add(new_experience)
250-
self.db.flush() # Get the ID
251-
user_data.experiences.append(new_experience)
252236

253237
def _process_caregiver_experience(self, user_data: UserData, caregiver_exp: Dict[str, Any]):
254238
"""
@@ -258,9 +242,6 @@ def _process_caregiver_experience(self, user_data: UserData, caregiver_exp: Dict
258242
if not caregiver_exp:
259243
return
260244

261-
# Handle "Other" caregiver experience text
262-
user_data.other_experience = caregiver_exp.get("other_experience")
263-
264245
# Process caregiver experiences - map to same experiences table
265246
experience_names = caregiver_exp.get("experiences", [])
266247
if not experience_names:
@@ -280,13 +261,6 @@ def _process_caregiver_experience(self, user_data: UserData, caregiver_exp: Dict
280261
# Only add if not already present
281262
if experience not in user_data.experiences:
282263
user_data.experiences.append(experience)
283-
else:
284-
# Create new experience for custom entry
285-
logger.info(f"Creating new caregiver experience: {experience_name}")
286-
new_experience = Experience(name=experience_name)
287-
self.db.add(new_experience)
288-
self.db.flush() # Get the ID
289-
user_data.experiences.append(new_experience)
290264

291265
def _process_loved_one_data(self, user_data: UserData, loved_one_data: Dict[str, Any]):
292266
"""Process loved one data including demographics and cancer experience."""
@@ -327,10 +301,6 @@ def _process_loved_one_cancer_experience(self, user_data: UserData, cancer_exp:
327301
f"Invalid date format for loved one dateOfDiagnosis: {cancer_exp.get('date_of_diagnosis')}"
328302
)
329303

330-
# Handle "Other" treatment and experience text for loved one with trimming
331-
user_data.loved_one_other_treatment = self._trim_text(cancer_exp.get("other_treatment"))
332-
user_data.loved_one_other_experience = self._trim_text(cancer_exp.get("other_experience"))
333-
334304
def _process_loved_one_treatments(self, user_data: UserData, cancer_exp: Dict[str, Any]):
335305
"""Process loved one treatments - map frontend names to database records."""
336306
treatment_names = cancer_exp.get("treatments", [])
@@ -349,13 +319,6 @@ def _process_loved_one_treatments(self, user_data: UserData, cancer_exp: Dict[st
349319

350320
if treatment:
351321
user_data.loved_one_treatments.append(treatment)
352-
else:
353-
# Create new treatment for custom entry
354-
logger.info(f"Creating new loved one treatment: {treatment_name}")
355-
new_treatment = Treatment(name=treatment_name)
356-
self.db.add(new_treatment)
357-
self.db.flush() # Get the ID
358-
user_data.loved_one_treatments.append(new_treatment)
359322

360323
def _process_loved_one_experiences(self, user_data: UserData, cancer_exp: Dict[str, Any]):
361324
"""Process loved one experiences - map frontend names to database records."""
@@ -375,13 +338,6 @@ def _process_loved_one_experiences(self, user_data: UserData, cancer_exp: Dict[s
375338

376339
if experience:
377340
user_data.loved_one_experiences.append(experience)
378-
else:
379-
# Create new experience for custom entry
380-
logger.info(f"Creating new loved one experience: {experience_name}")
381-
new_experience = Experience(name=experience_name)
382-
self.db.add(new_experience)
383-
self.db.flush() # Get the ID
384-
user_data.loved_one_experiences.append(new_experience)
385341

386342
def process_ranking_form(self, user_id: str, ranking_data: Dict[str, Any]):
387343
"""

backend/docs/intake_api.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ Create a new form submission and process it into structured data.
4444
"dateOfDiagnosis": "DD/MM/YYYY (optional)",
4545
"treatments": ["array of treatment names (optional)"],
4646
"experiences": ["array of experience names (optional)"],
47-
"otherTreatment": "string (optional)",
48-
"otherExperience": "string (optional)"
4947
},
5048
"lovedOne": {
5149
"demographics": {
@@ -57,8 +55,6 @@ Create a new form submission and process it into structured data.
5755
"dateOfDiagnosis": "DD/MM/YYYY (optional)",
5856
"treatments": ["array of treatment names (optional)"],
5957
"experiences": ["array of experience names (optional)"],
60-
"otherTreatment": "string (optional)",
61-
"otherExperience": "string (optional)"
6258
}
6359
}
6460
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""add scope column to experience table
2+
3+
Revision ID: 95467f4c5c80
4+
Revises: 905b6788b114
5+
Create Date: 2025-09-22 19:41:17.291555
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
import sqlalchemy as sa
12+
from alembic import op
13+
from sqlalchemy.dialects import postgresql
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = "95467f4c5c80"
17+
down_revision: Union[str, None] = "905b6788b114"
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
scope_enum = postgresql.ENUM("patient", "caregiver", "both", "none", name="scope")
24+
scope_enum.create(op.get_bind(), checkfirst=True)
25+
26+
op.add_column(
27+
"experiences",
28+
sa.Column(
29+
"scope",
30+
scope_enum,
31+
nullable=False,
32+
server_default=sa.text("'none'::scope"), # temporary default for NOT NULL
33+
),
34+
)
35+
36+
op.alter_column("experiences", "scope", server_default=None)
37+
# ### end Alembic commands ###
38+
39+
40+
def downgrade() -> None:
41+
# ### commands auto generated by Alembic - please adjust! ###
42+
op.drop_column("experiences", "scope")
43+
# ### end Alembic commands ###
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""remove other_treatment and other_experience columns from UserData table
2+
3+
Revision ID: a59aeb0bd691
4+
Revises: 95467f4c5c80
5+
Create Date: 2025-09-25 20:22:55.535261
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
import sqlalchemy as sa
12+
from alembic import op
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = "a59aeb0bd691"
16+
down_revision: Union[str, None] = "95467f4c5c80"
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
# ### commands auto generated by Alembic - please adjust! ###
23+
op.drop_column("user_data", "other_experience")
24+
op.drop_column("user_data", "loved_one_other_experience")
25+
op.drop_column("user_data", "other_treatment")
26+
op.drop_column("user_data", "loved_one_other_treatment")
27+
# ### end Alembic commands ###
28+
29+
30+
def downgrade() -> None:
31+
# ### commands auto generated by Alembic - please adjust! ###
32+
op.add_column("user_data", sa.Column("loved_one_other_treatment", sa.TEXT(), autoincrement=False, nullable=True))
33+
op.add_column("user_data", sa.Column("other_treatment", sa.TEXT(), autoincrement=False, nullable=True))
34+
op.add_column("user_data", sa.Column("loved_one_other_experience", sa.TEXT(), autoincrement=False, nullable=True))
35+
op.add_column("user_data", sa.Column("other_experience", sa.TEXT(), autoincrement=False, nullable=True))
36+
# ### end Alembic commands ###

backend/pdm.lock

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies = [
1919
"psycopg2>=2.9.9",
2020
"boto3>=1.35.71",
2121
"pytest-asyncio>=0.25.3",
22+
"psycopg2-binary>=2.9.10",
2223
]
2324
requires-python = "==3.12.*"
2425
readme = "README.md"
@@ -29,7 +30,7 @@ license = {text = "MIT"}
2930
distribution = false
3031

3132
[tool.pdm.scripts]
32-
dev = "fastapi dev app/server.py"
33+
dev = "fastapi dev app/server.py --port 8080"
3334
precommit = "pre-commit run"
3435
precommit-install = "pre-commit install"
3536
dc-down = "docker-compose down -v"

0 commit comments

Comments
 (0)