From 102e4342d6c53ce2926f72392d3c006105c5da37 Mon Sep 17 00:00:00 2001 From: Ethan Chen Date: Mon, 22 Sep 2025 19:22:55 -0400 Subject: [PATCH 01/13] modify experience model, add foundations for /intake/experiences endpoint --- backend/app/models/Experience.py | 3 ++- backend/app/routes/intake.py | 36 +++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/backend/app/models/Experience.py b/backend/app/models/Experience.py index 77ab3a55..5e89d9a7 100644 --- a/backend/app/models/Experience.py +++ b/backend/app/models/Experience.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String +from sqlalchemy import Column, Enum, Integer, String from sqlalchemy.orm import relationship from .Base import Base @@ -8,6 +8,7 @@ class Experience(Base): __tablename__ = "experiences" id = Column(Integer, primary_key=True) name = Column(String, unique=True, nullable=False) # 'PTSD', 'Relapse', etc. + scope = Column(Enum("patient", "caregiver", "both", "none", name="scope"), nullable=False) # Back reference for many-to-many relationship users = relationship("UserData", secondary="user_experiences", back_populates="experiences") diff --git a/backend/app/routes/intake.py b/backend/app/routes/intake.py index dc7a4491..2acb7e3c 100644 --- a/backend/app/routes/intake.py +++ b/backend/app/routes/intake.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import Session from app.middleware.auth import has_roles -from app.models import Form, FormSubmission, User +from app.models import Form, FormSubmission, User, Experience, Treatment from app.schemas.user import UserRole from app.services.implementations.intake_form_processor import IntakeFormProcessor from app.utilities.db_utils import get_db @@ -49,6 +49,19 @@ class FormSubmissionListResponse(BaseModel): total: int +class ExperienceResponse(BaseModel): + id: int + name: str + + +class ExperienceOptionsResponse(BaseModel): + experiences: List[ExperienceResponse] + + +# class TreatmentOptionsResponse(BaseModel): +# treatments: List[Treatment] + + # ===== Custom Auth Dependencies ===== @@ -339,6 +352,27 @@ async def delete_form_submission( raise HTTPException(status_code=500, detail=str(e)) +@router.get("/experiences", response_model=ExperienceOptionsResponse) +async def get_ranking_options( + request: Request, + target: str = Query(..., pattern="^(patient|caregiver)$"), + db: Session = Depends(get_db), + authorized: bool = True, + # authorized: bool = has_roles([UserRole.PARTICIPANT, UserRole.ADMIN]), +) -> ExperienceOptionsResponse: + try: + # Query DB Experience Table + experiences = db.query(Experience).filter(Experience.scope == target).all() + # service = RankingService(db) + # user_auth_id = request.state.user_id + # options = service.get_options(user_auth_id=user_auth_id, target=target) + return ExperienceOptionsResponse(experiences) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + # ===== Additional Utility Endpoints ===== From 157f9511c334fe5de2be0de520e199207af496d5 Mon Sep 17 00:00:00 2001 From: Ethan Chen Date: Mon, 22 Sep 2025 21:13:35 -0400 Subject: [PATCH 02/13] Fix and complete API endpoint /intake/options Co-authored-by: Ryan Gunawan Co-authored-by: Teresa Yu --- backend/app/routes/intake.py | 35 +++++++++------ backend/app/seeds/experiences.py | 24 +++++------ ...80_add_scope_column_to_experience_table.py | 43 +++++++++++++++++++ backend/pyproject.toml | 2 +- 4 files changed, 77 insertions(+), 27 deletions(-) create mode 100644 backend/migrations/versions/95467f4c5c80_add_scope_column_to_experience_table.py diff --git a/backend/app/routes/intake.py b/backend/app/routes/intake.py index 2acb7e3c..0da67cc8 100644 --- a/backend/app/routes/intake.py +++ b/backend/app/routes/intake.py @@ -1,9 +1,10 @@ from datetime import datetime -from typing import List, Optional +from typing import List, Optional, Literal from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Request from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import or_ from sqlalchemy.orm import Session from app.middleware.auth import has_roles @@ -12,6 +13,7 @@ from app.services.implementations.intake_form_processor import IntakeFormProcessor from app.utilities.db_utils import get_db + # ===== Schemas ===== @@ -52,14 +54,19 @@ class FormSubmissionListResponse(BaseModel): class ExperienceResponse(BaseModel): id: int name: str + scope: Literal["patient", "caregiver", "both", "none"] + model_config = ConfigDict(from_attributes=True) -class ExperienceOptionsResponse(BaseModel): - experiences: List[ExperienceResponse] +class TreatmentResponse(BaseModel): + id: int + name: str + model_config = ConfigDict(from_attributes=True) -# class TreatmentOptionsResponse(BaseModel): -# treatments: List[Treatment] +class OptionsResponse(BaseModel): + experiences: List[ExperienceResponse] + treatments: List[TreatmentResponse] # ===== Custom Auth Dependencies ===== @@ -352,21 +359,21 @@ async def delete_form_submission( raise HTTPException(status_code=500, detail=str(e)) -@router.get("/experiences", response_model=ExperienceOptionsResponse) +@router.get( + "/options", + response_model=OptionsResponse, +) async def get_ranking_options( request: Request, target: str = Query(..., pattern="^(patient|caregiver)$"), db: Session = Depends(get_db), - authorized: bool = True, - # authorized: bool = has_roles([UserRole.PARTICIPANT, UserRole.ADMIN]), -) -> ExperienceOptionsResponse: + authorized: bool = has_roles([UserRole.PARTICIPANT, UserRole.ADMIN]), +): try: # Query DB Experience Table - experiences = db.query(Experience).filter(Experience.scope == target).all() - # service = RankingService(db) - # user_auth_id = request.state.user_id - # options = service.get_options(user_auth_id=user_auth_id, target=target) - return ExperienceOptionsResponse(experiences) + experiences = db.query(Experience).filter(or_(Experience.scope == target, Experience.scope == "both")).all() + treatments = db.query(Treatment).all() + return OptionsResponse.model_validate({"experiences": experiences, "treatments": treatments}) except HTTPException: raise except Exception as e: diff --git a/backend/app/seeds/experiences.py b/backend/app/seeds/experiences.py index fff1e318..5f770219 100644 --- a/backend/app/seeds/experiences.py +++ b/backend/app/seeds/experiences.py @@ -9,18 +9,18 @@ def seed_experiences(session: Session) -> None: """Seed the experiences table with cancer-related experiences.""" experiences_data = [ - {"id": 1, "name": "Brain Fog"}, - {"id": 2, "name": "Communication Challenges"}, - {"id": 3, "name": "Compassion Fatigue"}, - {"id": 4, "name": "Feeling Overwhelmed"}, - {"id": 5, "name": "Fatigue"}, - {"id": 6, "name": "Fertility Issues"}, - {"id": 7, "name": "Graft vs Host"}, - {"id": 8, "name": "Returning to work or school after/during treatment"}, - {"id": 9, "name": "Speaking to your family or friends about the diagnosis"}, - {"id": 10, "name": "Relapse"}, - {"id": 11, "name": "Anxiety / Depression"}, - {"id": 12, "name": "PTSD"}, + {"id": 1, "name": "Brain Fog", "scope": "both"}, + {"id": 2, "name": "Communication Challenges", "scope": "caregiver"}, + {"id": 3, "name": "Compassion Fatigue", "scope": "none"}, + {"id": 4, "name": "Feeling Overwhelmed", "scope": "both"}, + {"id": 5, "name": "Fatigue", "scope": "both"}, + {"id": 6, "name": "Fertility Issues", "scope": "patient"}, + {"id": 7, "name": "Graft vs Host", "scope": "patient"}, + {"id": 8, "name": "Returning to work or school after/during treatment", "scope": "patient"}, + {"id": 9, "name": "Speaking to your family or friends about the diagnosis", "scope": "both"}, + {"id": 10, "name": "Relapse", "scope": "patient"}, + {"id": 11, "name": "Anxiety / Depression", "scope": "both"}, + {"id": 12, "name": "PTSD", "scope": "both"}, ] for experience_data in experiences_data: diff --git a/backend/migrations/versions/95467f4c5c80_add_scope_column_to_experience_table.py b/backend/migrations/versions/95467f4c5c80_add_scope_column_to_experience_table.py new file mode 100644 index 00000000..ad623a6d --- /dev/null +++ b/backend/migrations/versions/95467f4c5c80_add_scope_column_to_experience_table.py @@ -0,0 +1,43 @@ +"""add scope column to experience table + +Revision ID: 95467f4c5c80 +Revises: 905b6788b114 +Create Date: 2025-09-22 19:41:17.291555 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = "95467f4c5c80" +down_revision: Union[str, None] = "905b6788b114" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + scope_enum = postgresql.ENUM("patient", "caregiver", "both", "none", name="scope") + scope_enum.create(op.get_bind(), checkfirst=True) + + op.add_column( + "experiences", + sa.Column( + "scope", + scope_enum, + nullable=False, + server_default=sa.text("'none'::scope"), # temporary default for NOT NULL + ), + ) + + op.alter_column("experiences", "scope", server_default=None) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("experiences", "scope") + # ### end Alembic commands ### diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 8b198a8f..1818e0d6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -29,7 +29,7 @@ license = {text = "MIT"} distribution = false [tool.pdm.scripts] -dev = "fastapi dev app/server.py" +dev = "fastapi dev app/server.py --port 8080" precommit = "pre-commit run" precommit-install = "pre-commit install" dc-down = "docker-compose down -v" From d145f0dae306cfdb2f5bfd4053263727e0e368ee Mon Sep 17 00:00:00 2001 From: Ethan Chen Date: Thu, 25 Sep 2025 19:43:27 -0400 Subject: [PATCH 03/13] Update frontend to render intake options from endpoint instead of having hardcoded options Co-authored-by: Ryan Gunawan --- backend/app/routes/intake.py | 7 +- backend/app/seeds/experiences.py | 19 ++-- backend/pdm.lock | 24 ++++- backend/pyproject.toml | 1 + .../intake/demographic-cancer-form.tsx | 99 ++++++++++++------- .../src/components/intake/loved-one-form.tsx | 65 +++++++++++- frontend/src/pages/participant/intake.tsx | 14 +-- frontend/src/pages/volunteer/intake.tsx | 16 +-- frontend/src/types/intakeTypes.ts | 15 +++ 9 files changed, 192 insertions(+), 68 deletions(-) create mode 100644 frontend/src/types/intakeTypes.ts diff --git a/backend/app/routes/intake.py b/backend/app/routes/intake.py index 0da67cc8..0c804e86 100644 --- a/backend/app/routes/intake.py +++ b/backend/app/routes/intake.py @@ -365,13 +365,16 @@ async def delete_form_submission( ) async def get_ranking_options( request: Request, - target: str = Query(..., pattern="^(patient|caregiver)$"), + target: str = Query(..., pattern="^(patient|caregiver|both)$"), db: Session = Depends(get_db), authorized: bool = has_roles([UserRole.PARTICIPANT, UserRole.ADMIN]), ): try: # Query DB Experience Table - experiences = db.query(Experience).filter(or_(Experience.scope == target, Experience.scope == "both")).all() + if target == "both": + experiences = db.query(Experience).all() + else: + experiences = db.query(Experience).filter(or_(Experience.scope == target, Experience.scope == "both")).all() treatments = db.query(Treatment).all() return OptionsResponse.model_validate({"experiences": experiences, "treatments": treatments}) except HTTPException: diff --git a/backend/app/seeds/experiences.py b/backend/app/seeds/experiences.py index 5f770219..cb6b7c8b 100644 --- a/backend/app/seeds/experiences.py +++ b/backend/app/seeds/experiences.py @@ -11,16 +11,15 @@ def seed_experiences(session: Session) -> None: experiences_data = [ {"id": 1, "name": "Brain Fog", "scope": "both"}, {"id": 2, "name": "Communication Challenges", "scope": "caregiver"}, - {"id": 3, "name": "Compassion Fatigue", "scope": "none"}, - {"id": 4, "name": "Feeling Overwhelmed", "scope": "both"}, - {"id": 5, "name": "Fatigue", "scope": "both"}, - {"id": 6, "name": "Fertility Issues", "scope": "patient"}, - {"id": 7, "name": "Graft vs Host", "scope": "patient"}, - {"id": 8, "name": "Returning to work or school after/during treatment", "scope": "patient"}, - {"id": 9, "name": "Speaking to your family or friends about the diagnosis", "scope": "both"}, - {"id": 10, "name": "Relapse", "scope": "patient"}, - {"id": 11, "name": "Anxiety / Depression", "scope": "both"}, - {"id": 12, "name": "PTSD", "scope": "both"}, + {"id": 3, "name": "Feeling Overwhelmed", "scope": "both"}, + {"id": 4, "name": "Fatigue", "scope": "both"}, + {"id": 5, "name": "Fertility Issues", "scope": "patient"}, + {"id": 6, "name": "Graft vs Host", "scope": "patient"}, + {"id": 7, "name": "Returning to work or school after/during treatment", "scope": "patient"}, + {"id": 8, "name": "Speaking to your family or friends about the diagnosis", "scope": "both"}, + {"id": 9, "name": "Relapse", "scope": "patient"}, + {"id": 10, "name": "Anxiety / Depression", "scope": "both"}, + {"id": 11, "name": "PTSD", "scope": "both"}, ] for experience_data in experiences_data: diff --git a/backend/pdm.lock b/backend/pdm.lock index 2e403191..2bf1bfda 100644 --- a/backend/pdm.lock +++ b/backend/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "lint", "test"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:847e479aa02d5a9569f0b7dc0f6458685d7627180b11b79efc24e4df29fd3f9b" +content_hash = "sha256:0a1abed287908a98f22cfa7d57b89264608f5726444a7973504d19b099329afc" [[metadata.targets]] requires_python = "==3.12.*" @@ -1112,6 +1112,28 @@ files = [ {file = "psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11"}, ] +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +requires_python = ">=3.8" +summary = "psycopg2 - Python-PostgreSQL Database Adapter" +groups = ["default"] +files = [ + {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, +] + [[package]] name = "pyasn1" version = "0.6.1" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1818e0d6..f5da457a 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "psycopg2>=2.9.9", "boto3>=1.35.71", "pytest-asyncio>=0.25.3", + "psycopg2-binary>=2.9.10", ] requires-python = "==3.12.*" readme = "README.md" diff --git a/frontend/src/components/intake/demographic-cancer-form.tsx b/frontend/src/components/intake/demographic-cancer-form.tsx index 2abb67f8..2e0d4d9a 100644 --- a/frontend/src/components/intake/demographic-cancer-form.tsx +++ b/frontend/src/components/intake/demographic-cancer-form.tsx @@ -5,6 +5,8 @@ import { FormField } from '@/components/ui/form-field'; import { InputGroup } from '@/components/ui/input-group'; import { CheckboxGroup } from '@/components/ui/checkbox-group'; import { COLORS, VALIDATION } from '@/constants/form'; +import baseAPIClient from '@/APIClients/baseAPIClient'; +import { IntakeExperience, IntakeTreatment } from '@/types/intakeTypes'; // Reusable Select component to replace inline styling const StyledSelect: React.FC<{ @@ -65,38 +67,6 @@ const DEFAULT_VALUES: DemographicCancerFormData = { otherExperience: '', }; -const TREATMENT_OPTIONS = [ - 'Unknown', - 'Watch and Wait / Active Surveillance', - 'Chemotherapy', - 'Immunotherapy', - 'Oral Chemotherapy', - 'Radiation', - 'Maintenance Chemotherapy', - 'Palliative Care', - 'Transfusions', - 'Autologous Stem Cell Transplant', - 'Allogeneic Stem Cell Transplant', - 'Haplo Stem Cell Transplant', - 'CAR-T', - 'BTK Inhibitors', -]; - -const EXPERIENCE_OPTIONS = [ - 'Brain Fog', - 'Caregiver Fatigue', - 'Communication Challenges', - 'Feeling Overwhelmed', - 'Fatigue', - 'Fertility Issues', - 'Graft vs Host', - 'Returning to work or school after/during treatment', - 'Speaking to your family or friends about the diagnosis', - 'Relapse', - 'Anxiety / Depression', - 'PTSD', -]; - const DIAGNOSIS_OPTIONS = [ 'Acute Myeloid Leukaemia', 'Acute Lymphoblastic Leukaemia', @@ -114,6 +84,8 @@ const DIAGNOSIS_OPTIONS = [ interface DemographicCancerFormProps { formType?: 'participant' | 'volunteer'; onNext: (data: DemographicCancerFormData) => void; + hasBloodCancer?: 'yes' | 'no' | ''; + caringForSomeone?: 'yes' | 'no' | ''; } // Updated options to match Figma design - moved Self-describe to bottom @@ -296,21 +268,72 @@ const MultiSelectDropdown: React.FC<{ ); }; -export function DemographicCancerForm({ formType, onNext }: DemographicCancerFormProps) { + +export function DemographicCancerForm({ formType, onNext, hasBloodCancer, caringForSomeone }: DemographicCancerFormProps) { const { control, handleSubmit, - formState: { errors, isSubmitting }, + formState, watch, setValue, } = useForm({ defaultValues: DEFAULT_VALUES, }); + const { errors, isSubmitting } = formState; // Local state for custom values const [genderIdentityCustom, setGenderIdentityCustom] = useState(''); const [pronounsCustom, setPronounsCustom] = useState(''); const [ethnicGroupCustom, setEthnicGroupCustom] = useState(''); + const [treatmentOptions, setTreatmentOptions] = useState([]); + const [experienceOptions, setExperienceOptions] = useState([]); + + useEffect(() => { + let cancelled = false; + + const run = async () => { + const hasBloodCancerBool = hasBloodCancer === 'yes'; + const caringForSomeoneBool = caringForSomeone === 'yes'; + + let target = ''; + if (hasBloodCancerBool && caringForSomeoneBool) { + target = 'both'; + } else if (hasBloodCancerBool) { + target = 'patient'; + } else if (caringForSomeoneBool) { + target = 'caregiver'; + } else { + // This form should only render if at least one of these answers is "yes". + console.error( + 'Invalid intake flow state: neither hasBloodCancer nor caringForSomeone is "yes". ' + + `Received hasBloodCancer="${hasBloodCancer}", caringForSomeone="${caringForSomeone}". ` + + 'This Demographic Cancer form expects at least one to be "yes".' + ); + alert( + 'We hit an unexpected state.\n\n' + + 'This step is only shown if you have blood cancer or are caring for someone with blood cancer. ' + + 'Please go back to the previous step and select "Yes" for one of those questions, or navigate to the basic demographics form.' + ); + return; + } + + const options = await getOptions(target); + console.log(options); + + setTreatmentOptions( + options.treatments.map((treatment: IntakeTreatment) => treatment.name) + ); + + setExperienceOptions( + options.experiences.map((experience: IntakeExperience) => experience.name) + ); + } + + run(); + return () => { + cancelled = true; + } + }, [hasBloodCancer, caringForSomeone]); const otherTreatment = watch('otherTreatment') || ''; const otherExperience = watch('otherExperience') || ''; @@ -318,6 +341,10 @@ export function DemographicCancerForm({ formType, onNext }: DemographicCancerFor const pronouns = watch('pronouns') || []; const ethnicGroup = watch('ethnicGroup') || []; + const getOptions = async (target: string) => { + const options = await baseAPIClient.get(`/intake/options?target=${target}`); + return options.data; + } const onSubmit = async (data: DemographicCancerFormData) => { try { // Merge custom values into the arrays @@ -710,7 +737,7 @@ export function DemographicCancerForm({ formType, onNext }: DemographicCancerFor }} render={({ field }) => ( ( void; + hasBloodCancer?: 'yes' | 'no' | ''; + caringForSomeone?: 'yes' | 'no' | ''; } -export function LovedOneForm({ formType = 'participant', onSubmit }: LovedOneFormProps) { +export function LovedOneForm({ formType = 'participant', onSubmit, hasBloodCancer, caringForSomeone }: LovedOneFormProps) { const { control, handleSubmit, @@ -123,11 +127,64 @@ export function LovedOneForm({ formType = 'participant', onSubmit }: LovedOneFor // Local state for custom values const [genderIdentityCustom, setGenderIdentityCustom] = useState(''); + const [treatmentOptions, setTreatmentOptions] = useState([]); + const [experienceOptions, setExperienceOptions] = useState([]); + + useEffect(() => { + let cancelled = false; + + const run = async () => { + const hasBloodCancerBool = hasBloodCancer === 'yes'; + const caringForSomeoneBool = caringForSomeone === 'yes'; + + let target = ''; + if (hasBloodCancerBool && caringForSomeoneBool) { + target = 'both'; + } else if (hasBloodCancerBool) { + target = 'patient'; + } else if (caringForSomeoneBool) { + target = 'caregiver'; + } else { + // This form should only render if at least one of these answers is "yes". + console.error( + 'Invalid intake flow state: neither hasBloodCancer nor caringForSomeone is "yes". ' + + `Received hasBloodCancer="${hasBloodCancer}", caringForSomeone="${caringForSomeone}". ` + + 'This Demographic Cancer form expects at least one to be "yes".' + ); + alert( + 'We hit an unexpected state.\n\n' + + 'This step is only shown if you have blood cancer or are caring for someone with blood cancer. ' + + 'Please go back to the previous step and select "Yes" for one of those questions, or navigate to the basic demographics form.' + ); + return; + } + + const options = await getOptions(target); + + setTreatmentOptions( + options.treatments.map((treatment: IntakeTreatment) => treatment.name) + ); + + setExperienceOptions( + options.experiences.map((experience: IntakeExperience) => experience.name) + ); + } + + run(); + return () => { + cancelled = true; + } + }, [hasBloodCancer, caringForSomeone]); const otherTreatment = watch('otherTreatment') || ''; const otherExperience = watch('otherExperience') || ''; const genderIdentity = watch('genderIdentity') || ''; + const getOptions = async (target: string) => { + const options = await baseAPIClient.get(`/intake/options?target=${target}`); + return options.data; + } + const onFormSubmit = async (data: LovedOneFormData) => { try { // Merge custom values into the data @@ -393,7 +450,7 @@ export function LovedOneForm({ formType = 'participant', onSubmit }: LovedOneFor }} render={({ field }) => ( ( + )} {currentStepType === 'demographics-caregiver' && ( - + )} {currentStepType === 'loved-one' && ( diff --git a/frontend/src/pages/volunteer/intake.tsx b/frontend/src/pages/volunteer/intake.tsx index d5750624..212aab94 100644 --- a/frontend/src/pages/volunteer/intake.tsx +++ b/frontend/src/pages/volunteer/intake.tsx @@ -148,11 +148,11 @@ export default function VolunteerIntakePage() { }), ...(prev.hasBloodCancer === 'no' && prev.caringForSomeone === 'yes' && { - caregiverExperience: { - experiences: data.experiences, - otherExperience: data.otherExperience, - }, - }), + caregiverExperience: { + experiences: data.experiences, + otherExperience: data.otherExperience, + }, + }), } as IntakeFormData; void advanceAfterUpdate(updated); @@ -228,15 +228,15 @@ export default function VolunteerIntakePage() { )} {currentStepType === 'demographics-cancer' && ( - + )} {currentStepType === 'demographics-caregiver' && ( - + )} {currentStepType === 'loved-one' && ( - + )} {currentStepType === 'demographics-basic' && ( diff --git a/frontend/src/types/intakeTypes.ts b/frontend/src/types/intakeTypes.ts new file mode 100644 index 00000000..f1f45f8e --- /dev/null +++ b/frontend/src/types/intakeTypes.ts @@ -0,0 +1,15 @@ +export interface IntakeExperience { + id: number; + name: string; + scope: string; +} + +export interface IntakeTreatment { + id: number; + name: string; +} + +export interface IntakeOptionsResponse { + experiences: IntakeExperience[]; + treatments: IntakeTreatment[]; +} From 4c47859f1f23601ab716c50d74fa5bbf7ab3afca Mon Sep 17 00:00:00 2001 From: Ethan Chen Date: Thu, 25 Sep 2025 19:55:49 -0400 Subject: [PATCH 04/13] fix intake options authorization permissions, guarantee intake options sorted by id ascending, pass complete props to LovedOneForm --- backend/app/routes/intake.py | 15 ++++++++------- backend/app/seeds/experiences.py | 5 ++++- frontend/src/pages/participant/intake.tsx | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/backend/app/routes/intake.py b/backend/app/routes/intake.py index 0c804e86..85ad238a 100644 --- a/backend/app/routes/intake.py +++ b/backend/app/routes/intake.py @@ -363,19 +363,20 @@ async def delete_form_submission( "/options", response_model=OptionsResponse, ) -async def get_ranking_options( +async def get_intake_options( request: Request, target: str = Query(..., pattern="^(patient|caregiver|both)$"), db: Session = Depends(get_db), - authorized: bool = has_roles([UserRole.PARTICIPANT, UserRole.ADMIN]), + authorized: bool = has_roles([UserRole.PARTICIPANT, UserRole.VOLUNTEER, UserRole.ADMIN]), ): try: # Query DB Experience Table - if target == "both": - experiences = db.query(Experience).all() - else: - experiences = db.query(Experience).filter(or_(Experience.scope == target, Experience.scope == "both")).all() - treatments = db.query(Treatment).all() + experiences_query = db.query(Experience) + if target != "both": + experiences_query = experiences_query.filter(or_(Experience.scope == target, Experience.scope == "both")) + experiences = experiences_query.order_by(Experience.id.asc()).all() + + treatments = db.query(Treatment).order_by(Treatment.id.asc()).all() return OptionsResponse.model_validate({"experiences": experiences, "treatments": treatments}) except HTTPException: raise diff --git a/backend/app/seeds/experiences.py b/backend/app/seeds/experiences.py index cb6b7c8b..8c697308 100644 --- a/backend/app/seeds/experiences.py +++ b/backend/app/seeds/experiences.py @@ -18,8 +18,11 @@ def seed_experiences(session: Session) -> None: {"id": 7, "name": "Returning to work or school after/during treatment", "scope": "patient"}, {"id": 8, "name": "Speaking to your family or friends about the diagnosis", "scope": "both"}, {"id": 9, "name": "Relapse", "scope": "patient"}, - {"id": 10, "name": "Anxiety / Depression", "scope": "both"}, + {"id": 10, "name": "Anxiety", "scope": "both"}, {"id": 11, "name": "PTSD", "scope": "both"}, + {"id": 12, "name": "Caregiver Fatigue", "scope": "caregiver"}, + {"id": 13, "name": "Managing practical challenges", "scope": "caregiver"}, + {"id": 14, "name": "Depression", "scope": "both"}, ] for experience_data in experiences_data: diff --git a/frontend/src/pages/participant/intake.tsx b/frontend/src/pages/participant/intake.tsx index ce7b0f4f..96bd58a4 100644 --- a/frontend/src/pages/participant/intake.tsx +++ b/frontend/src/pages/participant/intake.tsx @@ -227,7 +227,7 @@ export default function ParticipantIntakePage() { )} {currentStepType === 'loved-one' && ( - + )} {currentStepType === 'demographics-basic' && ( From 6eab43c8ba8dc2403ad3d67e1d4089f2d0587989 Mon Sep 17 00:00:00 2001 From: Ethan Chen Date: Thu, 25 Sep 2025 20:13:29 -0400 Subject: [PATCH 05/13] fix hydration error with styledselect --- .../intake/demographic-cancer-form.tsx | 83 +++++-------- .../src/components/intake/loved-one-form.tsx | 115 +++++------------- 2 files changed, 58 insertions(+), 140 deletions(-) diff --git a/frontend/src/components/intake/demographic-cancer-form.tsx b/frontend/src/components/intake/demographic-cancer-form.tsx index 2e0d4d9a..a0e1f5b3 100644 --- a/frontend/src/components/intake/demographic-cancer-form.tsx +++ b/frontend/src/components/intake/demographic-cancer-form.tsx @@ -9,35 +9,38 @@ import baseAPIClient from '@/APIClients/baseAPIClient'; import { IntakeExperience, IntakeTreatment } from '@/types/intakeTypes'; // Reusable Select component to replace inline styling -const StyledSelect: React.FC<{ +type StyledSelectProps = React.SelectHTMLAttributes & { children: React.ReactNode; - value?: string; - onChange?: (e: React.ChangeEvent) => void; error?: boolean; -}> = ({ children, value, onChange, error, ...props }) => ( - +}; + +const StyledSelect = React.forwardRef( + ({ children, error, style, ...props }, ref) => ( + + ) ); +StyledSelect.displayName = 'StyledSelect'; interface DemographicCancerFormData { genderIdentity: string; @@ -48,9 +51,7 @@ interface DemographicCancerFormData { diagnosis: string; dateOfDiagnosis: string; treatments: string[]; - otherTreatment: string; experiences: string[]; - otherExperience: string; } const DEFAULT_VALUES: DemographicCancerFormData = { @@ -62,9 +63,7 @@ const DEFAULT_VALUES: DemographicCancerFormData = { diagnosis: '', dateOfDiagnosis: '', treatments: [], - otherTreatment: '', experiences: [], - otherExperience: '', }; const DIAGNOSIS_OPTIONS = [ @@ -335,8 +334,6 @@ export function DemographicCancerForm({ formType, onNext, hasBloodCancer, caring } }, [hasBloodCancer, caringForSomeone]); - const otherTreatment = watch('otherTreatment') || ''; - const otherExperience = watch('otherExperience') || ''; const genderIdentity = watch('genderIdentity') || ''; const pronouns = watch('pronouns') || []; const ethnicGroup = watch('ethnicGroup') || []; @@ -727,23 +724,12 @@ export function DemographicCancerForm({ formType, onNext, hasBloodCancer, caring { - if (value && value.includes('Other') && !otherTreatment.trim()) { - return 'Please specify the other treatment'; - } - return true; - }, - }} render={({ field }) => ( setValue('otherTreatment', value)} /> )} /> @@ -777,23 +763,12 @@ export function DemographicCancerForm({ formType, onNext, hasBloodCancer, caring { - if (value && value.includes('Other') && !otherExperience.trim()) { - return 'Please specify the other experience'; - } - return true; - }, - }} render={({ field }) => ( setValue('otherExperience', value)} /> )} /> diff --git a/frontend/src/components/intake/loved-one-form.tsx b/frontend/src/components/intake/loved-one-form.tsx index 17549907..7792d3e9 100644 --- a/frontend/src/components/intake/loved-one-form.tsx +++ b/frontend/src/components/intake/loved-one-form.tsx @@ -9,35 +9,38 @@ import { IntakeExperience, IntakeTreatment } from '@/types/intakeTypes'; import baseAPIClient from '@/APIClients/baseAPIClient'; // Reusable Select component to replace inline styling -const StyledSelect: React.FC<{ +type StyledSelectProps = React.SelectHTMLAttributes & { children: React.ReactNode; - value?: string; - onChange?: (e: React.ChangeEvent) => void; error?: boolean; -}> = ({ children, value, onChange, error, ...props }) => ( - +}; + +const StyledSelect = React.forwardRef( + ({ children, error, style, ...props }, ref) => ( + + ) ); +StyledSelect.displayName = 'StyledSelect'; interface LovedOneFormData { genderIdentity: string; @@ -45,9 +48,7 @@ interface LovedOneFormData { diagnosis: string; dateOfDiagnosis: string; treatments: string[]; - otherTreatment: string; experiences: string[]; - otherExperience: string; } const DEFAULT_VALUES: LovedOneFormData = { @@ -56,43 +57,9 @@ const DEFAULT_VALUES: LovedOneFormData = { diagnosis: '', dateOfDiagnosis: '', treatments: [], - otherTreatment: '', experiences: [], - otherExperience: '', }; -const TREATMENT_OPTIONS = [ - 'Unknown', - 'Watch and Wait / Active Surveillance', - 'Chemotherapy', - 'Immunotherapy', - 'Oral Chemotherapy', - 'Radiation', - 'Maintenance Chemotherapy', - 'Palliative Care', - 'Transfusions', - 'Autologous Stem Cell Transplant', - 'Allogeneic Stem Cell Transplant', - 'Haplo Stem Cell Transplant', - 'CAR-T', - 'BTK Inhibitors', -]; - -const EXPERIENCE_OPTIONS = [ - 'Brain Fog', - 'Caregiver Fatigue', - 'Communication Challenges', - 'Feeling Overwhelmed', - 'Fatigue', - 'Fertility Issues', - 'Graft vs Host', - 'Returning to work or school after/during treatment', - 'Speaking to your family or friends about the diagnosis', - 'Relapse', - 'Anxiety / Depression', - 'PTSD', -]; - const DIAGNOSIS_OPTIONS = [ 'Acute Myeloid Leukaemia', 'Acute Lymphoblastic Leukaemia', @@ -176,8 +143,6 @@ export function LovedOneForm({ formType = 'participant', onSubmit, hasBloodCance } }, [hasBloodCancer, caringForSomeone]); - const otherTreatment = watch('otherTreatment') || ''; - const otherExperience = watch('otherExperience') || ''; const genderIdentity = watch('genderIdentity') || ''; const getOptions = async (target: string) => { @@ -440,23 +405,12 @@ export function LovedOneForm({ formType = 'participant', onSubmit, hasBloodCance { - if (value && value.includes('Other') && !otherTreatment.trim()) { - return 'Please specify the other treatment'; - } - return true; - }, - }} render={({ field }) => ( setValue('otherTreatment', value)} /> )} /> @@ -490,23 +444,12 @@ export function LovedOneForm({ formType = 'participant', onSubmit, hasBloodCance { - if (value && value.includes('Other') && !otherExperience.trim()) { - return 'Please specify the other experience'; - } - return true; - }, - }} render={({ field }) => ( setValue('otherExperience', value)} /> )} /> From 9643b712329e7baa722c29e178561de15a632ac4 Mon Sep 17 00:00:00 2001 From: Ethan Chen Date: Thu, 25 Sep 2025 20:18:54 -0400 Subject: [PATCH 06/13] remote "other" fields from frontend intake.tsx components --- frontend/src/components/ui/checkbox-group.tsx | 36 ------------------- frontend/src/pages/participant/intake.tsx | 9 ----- frontend/src/pages/volunteer/intake.tsx | 9 ----- 3 files changed, 54 deletions(-) diff --git a/frontend/src/components/ui/checkbox-group.tsx b/frontend/src/components/ui/checkbox-group.tsx index 8766d465..b75e8b43 100644 --- a/frontend/src/components/ui/checkbox-group.tsx +++ b/frontend/src/components/ui/checkbox-group.tsx @@ -8,9 +8,6 @@ export interface CheckboxGroupProps { selectedValues: string[]; onValueChange: (values: string[]) => void; maxSelections: number; - showOther?: boolean; - otherValue?: string; - onOtherChange?: (value: string) => void; } export const CheckboxGroup: React.FC = ({ @@ -18,9 +15,6 @@ export const CheckboxGroup: React.FC = ({ selectedValues, onValueChange, maxSelections, - showOther = false, - otherValue = '', - onOtherChange, }) => { const handleCheckboxChange = (option: string) => { if (selectedValues.includes(option)) { @@ -45,36 +39,6 @@ export const CheckboxGroup: React.FC = ({ ))} - - {showOther && ( - - handleCheckboxChange('other')} - disabled={!selectedValues.includes('other') && selectedValues.length >= maxSelections} - colorScheme="teal" - > - - Other: - - - onOtherChange?.(e.target.value)} - placeholder="" - fontFamily="system-ui, -apple-system, sans-serif" - fontSize="14px" - color={COLORS.veniceBlue} - borderColor="#d1d5db" - borderRadius="6px" - h="32px" - maxW="200px" - _placeholder={{ color: '#9ca3af' }} - _focus={{ borderColor: COLORS.teal, boxShadow: `0 0 0 3px ${COLORS.teal}20` }} - disabled={!selectedValues.includes('other')} - /> - - )} ); }; diff --git a/frontend/src/pages/participant/intake.tsx b/frontend/src/pages/participant/intake.tsx index 96bd58a4..c379ab3e 100644 --- a/frontend/src/pages/participant/intake.tsx +++ b/frontend/src/pages/participant/intake.tsx @@ -28,9 +28,7 @@ interface DemographicCancerFormData { diagnosis: string; dateOfDiagnosis: string; treatments: string[]; - otherTreatment: string; experiences: string[]; - otherExperience: string; } interface LovedOneFormData { @@ -40,9 +38,7 @@ interface LovedOneFormData { diagnosis: string; dateOfDiagnosis: string; treatments: string[]; - otherTreatment: string; experiences: string[]; - otherExperience: string; } interface BasicDemographicsFormData { @@ -133,15 +129,12 @@ export default function ParticipantIntakePage() { dateOfDiagnosis: data.dateOfDiagnosis, treatments: data.treatments, experiences: data.experiences, - otherTreatment: data.otherTreatment, - otherExperience: data.otherExperience, }, }), ...(prev.hasBloodCancer === 'no' && prev.caringForSomeone === 'yes' && { caregiverExperience: { experiences: data.experiences, - otherExperience: data.otherExperience, }, }), } as IntakeFormData; @@ -166,8 +159,6 @@ export default function ParticipantIntakePage() { dateOfDiagnosis: data.dateOfDiagnosis, treatments: data.treatments, experiences: data.experiences, - otherTreatment: data.otherTreatment, - otherExperience: data.otherExperience, }, }, }; diff --git a/frontend/src/pages/volunteer/intake.tsx b/frontend/src/pages/volunteer/intake.tsx index 212aab94..d0b9678d 100644 --- a/frontend/src/pages/volunteer/intake.tsx +++ b/frontend/src/pages/volunteer/intake.tsx @@ -28,9 +28,7 @@ interface DemographicCancerFormData { diagnosis: string; dateOfDiagnosis: string; treatments: string[]; - otherTreatment: string; experiences: string[]; - otherExperience: string; } interface LovedOneFormData { @@ -40,9 +38,7 @@ interface LovedOneFormData { diagnosis: string; dateOfDiagnosis: string; treatments: string[]; - otherTreatment: string; experiences: string[]; - otherExperience: string; } interface BasicDemographicsFormData { @@ -142,15 +138,12 @@ export default function VolunteerIntakePage() { dateOfDiagnosis: data.dateOfDiagnosis, treatments: data.treatments, experiences: data.experiences, - otherTreatment: data.otherTreatment, - otherExperience: data.otherExperience, }, }), ...(prev.hasBloodCancer === 'no' && prev.caringForSomeone === 'yes' && { caregiverExperience: { experiences: data.experiences, - otherExperience: data.otherExperience, }, }), } as IntakeFormData; @@ -175,8 +168,6 @@ export default function VolunteerIntakePage() { dateOfDiagnosis: data.dateOfDiagnosis, treatments: data.treatments, experiences: data.experiences, - otherTreatment: data.otherTreatment, - otherExperience: data.otherExperience, }, }, }; From b11416f2a8cb3c828bd14330b07839b3cdd0b081 Mon Sep 17 00:00:00 2001 From: Ethan Chen Date: Thu, 25 Sep 2025 20:24:01 -0400 Subject: [PATCH 07/13] remove processing of other_treatment and other_experiences fields in intake_form_processor, delete related table columns --- backend/app/models/UserData.py | 4 --- .../implementations/intake_form_processor.py | 6 ---- ...bd691_remove_other_treatment_and_other_.py | 35 +++++++++++++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 backend/migrations/versions/a59aeb0bd691_remove_other_treatment_and_other_.py diff --git a/backend/app/models/UserData.py b/backend/app/models/UserData.py index f4c26121..93c45f64 100644 --- a/backend/app/models/UserData.py +++ b/backend/app/models/UserData.py @@ -65,8 +65,6 @@ class UserData(Base): date_of_diagnosis = Column(Date, nullable=True) # "Other" text fields for custom entries - other_treatment = Column(Text, nullable=True) - other_experience = Column(Text, nullable=True) other_ethnic_group = Column(Text, nullable=True) gender_identity_custom = Column(Text, nullable=True) @@ -81,8 +79,6 @@ class UserData(Base): # Loved One Cancer Experience loved_one_diagnosis = Column(String(100), nullable=True) loved_one_date_of_diagnosis = Column(Date, nullable=True) - loved_one_other_treatment = Column(Text, nullable=True) - loved_one_other_experience = Column(Text, nullable=True) # Many-to-many relationships treatments = relationship("Treatment", secondary=user_treatments, back_populates="users") diff --git a/backend/app/services/implementations/intake_form_processor.py b/backend/app/services/implementations/intake_form_processor.py index 9f78759e..eb40b639 100644 --- a/backend/app/services/implementations/intake_form_processor.py +++ b/backend/app/services/implementations/intake_form_processor.py @@ -175,8 +175,6 @@ def _process_demographics(self, user_data: UserData, demographics: Dict[str, Any def _process_cancer_experience(self, user_data: UserData, cancer_experience: Dict[str, Any]): """Process cancer experience information.""" user_data.diagnosis = self._trim_text(cancer_experience.get("diagnosis")) - user_data.other_treatment = self._trim_text(cancer_experience.get("other_treatment")) - user_data.other_experience = self._trim_text(cancer_experience.get("other_experience")) # Parse diagnosis date with strict validation if "date_of_diagnosis" in cancer_experience: @@ -327,10 +325,6 @@ def _process_loved_one_cancer_experience(self, user_data: UserData, cancer_exp: f"Invalid date format for loved one dateOfDiagnosis: {cancer_exp.get('date_of_diagnosis')}" ) - # Handle "Other" treatment and experience text for loved one with trimming - user_data.loved_one_other_treatment = self._trim_text(cancer_exp.get("other_treatment")) - user_data.loved_one_other_experience = self._trim_text(cancer_exp.get("other_experience")) - def _process_loved_one_treatments(self, user_data: UserData, cancer_exp: Dict[str, Any]): """Process loved one treatments - map frontend names to database records.""" treatment_names = cancer_exp.get("treatments", []) diff --git a/backend/migrations/versions/a59aeb0bd691_remove_other_treatment_and_other_.py b/backend/migrations/versions/a59aeb0bd691_remove_other_treatment_and_other_.py new file mode 100644 index 00000000..4e788c64 --- /dev/null +++ b/backend/migrations/versions/a59aeb0bd691_remove_other_treatment_and_other_.py @@ -0,0 +1,35 @@ +"""remove other_treatment and other_experience columns from UserData table + +Revision ID: a59aeb0bd691 +Revises: 95467f4c5c80 +Create Date: 2025-09-25 20:22:55.535261 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = 'a59aeb0bd691' +down_revision: Union[str, None] = '95467f4c5c80' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user_data', 'other_experience') + op.drop_column('user_data', 'loved_one_other_experience') + op.drop_column('user_data', 'other_treatment') + op.drop_column('user_data', 'loved_one_other_treatment') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user_data', sa.Column('loved_one_other_treatment', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('user_data', sa.Column('other_treatment', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('user_data', sa.Column('loved_one_other_experience', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('user_data', sa.Column('other_experience', sa.TEXT(), autoincrement=False, nullable=True)) + # ### end Alembic commands ### From fcbb389367a1505b1fcfafb806d822616bca5f2e Mon Sep 17 00:00:00 2001 From: Ethan Chen Date: Sun, 28 Sep 2025 14:18:49 -0400 Subject: [PATCH 08/13] remove testing for custom/other experiences and treatments in test_intake_form_processor.py --- .../implementations/intake_form_processor.py | 38 --- backend/docs/intake_api.md | 4 - backend/test_intake.db | Bin 126976 -> 126976 bytes backend/tests/unit/test_intake_api.py | 214 +++++++++++++++ .../tests/unit/test_intake_form_processor.py | 253 +++++------------- frontend/src/constants/form.ts | 8 - 6 files changed, 282 insertions(+), 235 deletions(-) diff --git a/backend/app/services/implementations/intake_form_processor.py b/backend/app/services/implementations/intake_form_processor.py index eb40b639..13fb57fe 100644 --- a/backend/app/services/implementations/intake_form_processor.py +++ b/backend/app/services/implementations/intake_form_processor.py @@ -211,13 +211,6 @@ def _process_treatments(self, user_data: UserData, cancer_exp: Dict[str, Any]): if treatment: user_data.treatments.append(treatment) - else: - # Create new treatment for custom entry - logger.info(f"Creating new treatment: {treatment_name}") - new_treatment = Treatment(name=treatment_name) - self.db.add(new_treatment) - self.db.flush() # Get the ID - user_data.treatments.append(new_treatment) def _process_experiences(self, user_data: UserData, cancer_exp: Dict[str, Any]): """ @@ -240,13 +233,6 @@ def _process_experiences(self, user_data: UserData, cancer_exp: Dict[str, Any]): if experience: user_data.experiences.append(experience) - else: - # Create new experience for custom entry - logger.info(f"Creating new experience: {experience_name}") - new_experience = Experience(name=experience_name) - self.db.add(new_experience) - self.db.flush() # Get the ID - user_data.experiences.append(new_experience) def _process_caregiver_experience(self, user_data: UserData, caregiver_exp: Dict[str, Any]): """ @@ -256,9 +242,6 @@ def _process_caregiver_experience(self, user_data: UserData, caregiver_exp: Dict if not caregiver_exp: return - # Handle "Other" caregiver experience text - user_data.other_experience = caregiver_exp.get("other_experience") - # Process caregiver experiences - map to same experiences table experience_names = caregiver_exp.get("experiences", []) if not experience_names: @@ -278,13 +261,6 @@ def _process_caregiver_experience(self, user_data: UserData, caregiver_exp: Dict # Only add if not already present if experience not in user_data.experiences: user_data.experiences.append(experience) - else: - # Create new experience for custom entry - logger.info(f"Creating new caregiver experience: {experience_name}") - new_experience = Experience(name=experience_name) - self.db.add(new_experience) - self.db.flush() # Get the ID - user_data.experiences.append(new_experience) def _process_loved_one_data(self, user_data: UserData, loved_one_data: Dict[str, Any]): """Process loved one data including demographics and cancer experience.""" @@ -343,13 +319,6 @@ def _process_loved_one_treatments(self, user_data: UserData, cancer_exp: Dict[st if treatment: user_data.loved_one_treatments.append(treatment) - else: - # Create new treatment for custom entry - logger.info(f"Creating new loved one treatment: {treatment_name}") - new_treatment = Treatment(name=treatment_name) - self.db.add(new_treatment) - self.db.flush() # Get the ID - user_data.loved_one_treatments.append(new_treatment) def _process_loved_one_experiences(self, user_data: UserData, cancer_exp: Dict[str, Any]): """Process loved one experiences - map frontend names to database records.""" @@ -369,13 +338,6 @@ def _process_loved_one_experiences(self, user_data: UserData, cancer_exp: Dict[s if experience: user_data.loved_one_experiences.append(experience) - else: - # Create new experience for custom entry - logger.info(f"Creating new loved one experience: {experience_name}") - new_experience = Experience(name=experience_name) - self.db.add(new_experience) - self.db.flush() # Get the ID - user_data.loved_one_experiences.append(new_experience) def process_ranking_form(self, user_id: str, ranking_data: Dict[str, Any]): """ diff --git a/backend/docs/intake_api.md b/backend/docs/intake_api.md index 6d8d007b..b7fd40e2 100644 --- a/backend/docs/intake_api.md +++ b/backend/docs/intake_api.md @@ -44,8 +44,6 @@ Create a new form submission and process it into structured data. "dateOfDiagnosis": "DD/MM/YYYY (optional)", "treatments": ["array of treatment names (optional)"], "experiences": ["array of experience names (optional)"], - "otherTreatment": "string (optional)", - "otherExperience": "string (optional)" }, "lovedOne": { "demographics": { @@ -57,8 +55,6 @@ Create a new form submission and process it into structured data. "dateOfDiagnosis": "DD/MM/YYYY (optional)", "treatments": ["array of treatment names (optional)"], "experiences": ["array of experience names (optional)"], - "otherTreatment": "string (optional)", - "otherExperience": "string (optional)" } } } diff --git a/backend/test_intake.db b/backend/test_intake.db index ae147b15955646242b66e5c331bf5c058ee4887d..d603d9b10c7baa5f4bdf380e7f03bca82b432243 100644 GIT binary patch delta 239 zcmZp8z~1nHeS(xw+9d`C26-Ts0%Ac1hR~jgI!0_jQN6Oxi7Bf5@0n2GWWGAP#hql?7&0#y!_R_xh2m*K@K+CN< zBUfM96v_7<9^d=CA9x@ja`V}$;p*~^Ww$j~E(`Anq9}Y7p}J5(z}jss}0T7{0Ae;Ri#o>p|jsUbyABT1%~4jUcMji)!jb z+*XYS;R|Qk-_o{Bt7|&)Cz~7VtcdQeY1GxGZFM_-U|X{d`U2!t+^*vn%gbX|bJRVf z5rlg$-7vJKX*otXqF1QCPLQH6WVySfw0{-6!hvWxb|6R;pY_3ilK);%Rkdtx|n1 zKT)2`Ge(1Tp{@0mXIqLqLpLhsih^+Nmv1~0$VPuDO-_o3q?O1>@cEqJQI4xe5dMEw z4e?)xjQ9RfRZIS;%oZoo(gP`}6=^z0U0`<^K@k7?bxB#yPMRduBuNr~T<|9ehh=hs z17Sf)6GQy?=UO&)BH&UCj(slLwpuzFrE*+utd%-nc4;hYX!uhZZ_ld{9VB&t{YQiqma2b;s2ks%x}$GU_7%w~ubV zmyu>?#a9+9bR;_R^cGix!46qfG=8~#xG`~xW?6$;`{XBn8+oZj7a4#scl#1YleY}cq89nExOsrIc_*L3v+ z$oj73=q4LK1HG4aE%y4}dLT)=PM2+D5R%m>-M!aV))e`n@~E=4T9ap%x2j|^si`z{ zvsPJq%80uix-@vBO}M6uvF9C**B)!yRpOROKi_~RAhoIko_9vv{+Em$|Ahm<92!bWVK^h zX6$6;9&|#>{QIxlSC%dgJM{ABAhqN_yma_}jvZMY^%hNjg#X|oS4BVAt7hDO&}ns> zu?rZ|OLhO4h%?}NMr5^2vm>kJAEh#E1>akIgc?8e@H|)6P{gp(x@7ibL&u-GXitik zC|5ij5i`>Cw0O7~OxfPF7#_3dxH5!eh7=;)IgYU=qQ~aspdUXFfB*y_009U<00Izz z00bZa0SNp*7NE!fsr+w+{2%lGK<@AZ0SG_<0uX=z1Rwwb2tWV=5P-m2BruXn36j=m z8|I~RDPh9DG-*ss2~(k~n{fXBEoxL06#@`|00bZa0SG_<0uX=z1R(G~6rjidV*Zx` z`Hvq6KmY;|fB*y_009U<00Izz00d5pz?zs{8y~-uogT@SwNhjI{z6$_xIe$BEi8RB zzf*7AZ!CSZuvDkNYvkjMw#bLxxa9u3`b(|dY3XyFV-yb-R!QlVUq76G_19IpUUM*=?9*d(ChzS z_f|vi`~Q9;>^V^7Aek9J0tOLs4Ro&EL5&qqGV%%;Ce|M0@K^M5%1D{)Tv51}b6l&8|t<4I94 z%!dBbdC@XlUDdj-#h$CKt!r*uH(f_91&{NG`9H}>lau11;cDA0J%|Y&<(8{TrKZTW z%EzmU9E8g=xvbHUSJrFFW2GvuZ`9=Vt<}{zIhQrHwl05KsV;x7!bd!Tum+c53@hHEN7&N3GrtySS9=j z|G@}X1wYuU!PeHwL$3HN0jb(R#ExM*t{Sf0%>8**iI$cSLCUJD){eSu*zT_UkU-Kp z>ut?w^@V0i3%yJ_yO!y@&3J)z!#xO6&U-0sYu_;ILE?K}xaGK7ORZatAga`hYU-q4 z*s9SWeIT4=e@pW9lvdYt#qVUDK$mP21{r{J=K(@=E#w|>R4?(+(urCmbI@pR1%>^UdqqP z6ABV)&4CsQA_ER@5Y3dlh7YCjI#!UjPZy*~LX4(};fx!pk2b2x%Hwr9dS|>bQk1L8 zBc-aWFDsie4RhGI&J~LjY3YG9Y#2N00^1-{y#4y5Y_vP+qrbS7kt9j{als$y4$I`i zzz8P=Ve`{52QfDR#Exbz#oz$uqHU|Clc5$nIg&Yfm&V1-k6NcgMz-FNKi=3_RVr*C zYIWDx*ZU#d%fvNOe>!C)WOGxv*}Q^r%|^re&F0)>zK06)Wijkmiu3ihIM)16XUL(2 z&J`aN($cfpq`u;Gn@!zu$pCYWwoZ0mk$~GrH{Z)hv$NtWixoN&9eH|-t4Ji0cA2*3 z)>-?+?bv7u=v-0XOKM%NN&0!s$=rqmtjkNT< zoTO`O<{q8hI<~%}+jK@H!ZCq2kM3U2NaeEld8*en2DgjZs=#T-B%tEnR5G}`ebLp( zD&`=Gnr0U}!nii@a-Wlf+y?x;G#o#hR{TXu7(bLzSiTo*_(SRQ52TD#D2P9uWnf-& zdUw}37{93#M^s0%U88PvG}EOV1C!dm)#{qA&UWyFvGrZc(M>jf26`{;TI}_`oqCdX zoh~5BAkY_aa74^V)6?SNW-w)Y z(_(ncp5w|8ju}#jaOXJ2nyB~vf4>v*-{k)}|GT&JZ4VS10uX=z1Rwwb2tWV=5P$## zAaGU%M#*;pPCnGh`|S>WPl$a9;3M+AfX%L+BVPhosF*Je-8~o``7hz(NLk4LU4C`+ z52L>t{Y6g9ev$pNY-Xf9^7ol1>CZ0w`S~Yj6%qA@00fRHaFi`(q)J76b&I{L$=);% zt^f$m{j)RaYHZ8|_l{?B4)py`7RxTt@lI6H_gaHj37t}ieLLUVk~CPST zRW?uvS+UB8o{taKITV{%)gaBu*Okkgt85VC*H(YOcXCV&Y8#NS$_7ub4%Rh@OsuM( zT(J^9b#d%-I&>p)PUqfqT3VS( zy80mW`f4fksrrN#1x+Y4o_e2)J8 zKaT(3fE7t0009U<00Izz00bZa0SG_<0;fvg_{aakJEyAPC=3K3009U<00Izz00bZa z0SG_<0w*j$|NbAx|0j%!>>vOE2tWV=5P$##AOHafKmY=#On_ehkK_MS)+i_r1Rwwb z2tWV=5P$##AOHafK;VQ0aQuJ5pvVpa5P$##AOHafKmY;|fB*y_aC!ykCjd;4+YNFX zC%3EQM#J7Ew=B8QFUif2TbkT1kXxSI=${VIFVMYAZYgrZ4+J0p0SG_<0uX=z1Rwwb z2tWV=r&Ivv|4(VdpjZ%q00bZa0SG_<0uX=z1Rwx`6BNMb|D7NvvVs5vAOHafKmY;| zfB*y_009V`QURR*Kcx+WVnF}`5P$##AOHafKmY;|fB*zePypxuPY@GXK>z{}fB*y_ t009U<00Izz00d5{0M7rP(uP5?AOHafKmY;|fB*y_009U<00Ji{@ZS List[str]: + if isinstance(condition, BooleanClauseList): + values: List[str] = [] + for clause in condition.clauses: + values.extend(_extract_filter_values(clause)) + return values + if isinstance(condition, BinaryExpression): + right = getattr(condition, "right", None) + value = getattr(right, "value", None) if right is not None else None + return [value] if value is not None else [] + return [] + + +class FakeQuery: + def __init__(self, data: List): + self._data = list(data) + + def _clone(self, data: List): + return self.__class__(data) + + def order_by(self, clause): + element = getattr(clause, "element", None) + key_name = getattr(element, "key", None) if element is not None else None + if not key_name: + key_name = getattr(clause, "key", None) or "id" + sorted_data = sorted(self._data, key=lambda item: getattr(item, key_name)) + return self._clone(sorted_data) + + def all(self): + return list(self._data) + + +class FakeExperienceQuery(FakeQuery): + def filter(self, condition): + scopes = {value for value in _extract_filter_values(condition) if value is not None} + if not scopes: + return self._clone(self._data) + filtered = [item for item in self._data if item.scope in scopes] + return self._clone(filtered) + + +class FakeTreatmentQuery(FakeQuery): + pass + + +class FakeSession: + def __init__( + self, + experiences: List[FakeExperienceRecord], + treatments: List[FakeTreatmentRecord], + query_exception: Exception | None = None, + ): + self._experiences = experiences + self._treatments = treatments + self._query_exception = query_exception + + def query(self, model): + if self._query_exception: + raise self._query_exception + if model is Experience: + return FakeExperienceQuery(self._experiences) + if model is Treatment: + return FakeTreatmentQuery(self._treatments) + raise AssertionError(f"Unexpected model queried: {model}") + + def close(self): + """Mimic SQLAlchemy session close.""" + + +@contextmanager +def override_dependencies(session: FakeSession, authorized: bool = True): + def _override_db(): + yield session + + class DummyAuthService: + def is_authorized_by_role(self, token, roles): + return authorized + + app.dependency_overrides[get_db] = _override_db + app.dependency_overrides[get_auth_service] = lambda: DummyAuthService() + + try: + yield + finally: + app.dependency_overrides.pop(get_db, None) + app.dependency_overrides.pop(get_auth_service, None) + if hasattr(session, "close"): + session.close() + + +class TestGetIntakeOptions: + auth_header = {"Authorization": "Bearer test-token"} + + def test_patient_target_filters_and_sorts_experiences(self, client): + experiences = [ + FakeExperienceRecord(id=2, name="Caregiver Only", scope="caregiver"), + FakeExperienceRecord(id=3, name="Both Eligible", scope="both"), + FakeExperienceRecord(id=1, name="Patient Only", scope="patient"), + ] + treatments = [ + FakeTreatmentRecord(id=2, name="Radiation"), + FakeTreatmentRecord(id=1, name="Chemotherapy"), + ] + session = FakeSession(experiences, treatments) + + with override_dependencies(session): + response = client.get("/intake/options", params={"target": "patient"}, headers=self.auth_header) + + assert response.status_code == 200 + body = response.json() + experience_scopes = [exp["scope"] for exp in body["experiences"]] + experience_ids = [exp["id"] for exp in body["experiences"]] + assert experience_scopes == ["patient", "both"], "Only patient & both scopes should be returned" + assert experience_ids == [1, 3], "Experiences should be ordered by id ascending" + treatment_ids = [treatment["id"] for treatment in body["treatments"]] + assert treatment_ids == [1, 2], "Treatments should be ordered by id ascending" + + def test_caregiver_target_includes_both_scope(self, client): + experiences = [ + FakeExperienceRecord(id=1, name="Patient Experience", scope="patient"), + FakeExperienceRecord(id=2, name="Caregiver Experience", scope="caregiver"), + FakeExperienceRecord(id=3, name="Shared Experience", scope="both"), + ] + treatments = [FakeTreatmentRecord(id=1, name="Chemotherapy")] + session = FakeSession(experiences, treatments) + + with override_dependencies(session): + response = client.get("/intake/options", params={"target": "caregiver"}, headers=self.auth_header) + + assert response.status_code == 200 + body = response.json() + returned_scopes = {exp["scope"] for exp in body["experiences"]} + assert returned_scopes == {"caregiver", "both"} + assert all(exp["id"] in {2, 3} for exp in body["experiences"]) + + def test_both_target_returns_all_experiences(self, client): + experiences = [ + FakeExperienceRecord(id=1, name="Patient Experience", scope="patient"), + FakeExperienceRecord(id=2, name="Caregiver Experience", scope="caregiver"), + FakeExperienceRecord(id=3, name="Shared Experience", scope="both"), + ] + treatments = [FakeTreatmentRecord(id=1, name="Chemotherapy")] + session = FakeSession(experiences, treatments) + + with override_dependencies(session): + response = client.get("/intake/options", params={"target": "both"}, headers=self.auth_header) + + assert response.status_code == 200 + body = response.json() + assert [exp["id"] for exp in body["experiences"]] == [1, 2, 3] + assert [exp["scope"] for exp in body["experiences"]] == ["patient", "caregiver", "both"] + + def test_missing_authorization_header_returns_403(self, client): + response = client.get("/intake/options", params={"target": "patient"}) + assert response.status_code == 401 + + def test_invalid_target_returns_422(self, client): + session = FakeSession([], []) + + with override_dependencies(session): + response = client.get( + "/intake/options", + params={"target": "invalid"}, + headers=self.auth_header, + ) + assert response.status_code == 422 + + def test_database_error_returns_500(self, client): + session = FakeSession([], [], query_exception=RuntimeError("database down")) + + with override_dependencies(session): + response = client.get("/intake/options", params={"target": "patient"}, headers=self.auth_header) + + assert response.status_code == 500 + assert response.json()["detail"] == "database down" diff --git a/backend/tests/unit/test_intake_form_processor.py b/backend/tests/unit/test_intake_form_processor.py index 5603e8d9..eda98104 100644 --- a/backend/tests/unit/test_intake_form_processor.py +++ b/backend/tests/unit/test_intake_form_processor.py @@ -62,18 +62,40 @@ def db_session(): # Create test treatments (predefined) treatments = [ - Treatment(id=1, name="Chemotherapy"), - Treatment(id=2, name="Surgery"), - Treatment(id=3, name="Radiation Therapy"), + Treatment(id=1, name="Unknown"), + Treatment(id=2, name="Watch and Wait / Active Surveillance"), + Treatment(id=3, name="Chemotherapy"), + Treatment(id=4, name="Immunotherapy"), + Treatment(id=5, name="Oral Chemotherapy"), + Treatment(id=6, name="Radiation"), + Treatment(id=7, name="Maintenance Chemotherapy"), + Treatment(id=8, name="Palliative Care"), + Treatment(id=9, name="Transfusions"), + Treatment(id=10, name="Autologous Stem Cell Transplant"), + Treatment(id=11, name="Allogeneic Stem Cell Transplant"), + Treatment(id=12, name="Haplo Stem Cell Transplant"), + Treatment(id=13, name="CAR-T"), + Treatment(id=14, name="BTK Inhibitors"), ] for treatment in treatments: session.add(treatment) # Create test experiences (predefined) experiences = [ - Experience(id=1, name="Anxiety"), - Experience(id=2, name="Fatigue"), - Experience(id=3, name="Depression"), + Experience(id=1, name="Brain Fog", scope="both"), + Experience(id=2, name="Communication Challenges", scope="caregiver"), + Experience(id=3, name="Feeling Overwhelmed", scope="both"), + Experience(id=4, name="Fatigue", scope="both"), + Experience(id=5, name="Fertility Issues", scope="patient"), + Experience(id=6, name="Graft vs Host", scope="patient"), + Experience(id=7, name="Returning to work or school after/during treatment", scope="patient"), + Experience(id=8, name="Speaking to your family or friends about the diagnosis", scope="both"), + Experience(id=9, name="Relapse", scope="patient"), + Experience(id=10, name="Anxiety", scope="both"), + Experience(id=11, name="PTSD", scope="both"), + Experience(id=12, name="Caregiver Fatigue", scope="caregiver"), + Experience(id=13, name="Managing practical challenges", scope="caregiver"), + Experience(id=14, name="Depression", scope="both"), ] for experience in experiences: session.add(experience) @@ -142,10 +164,8 @@ def test_participant_with_cancer_only(db_session, test_user): "cancer_experience": { "diagnosis": "Leukemia", "date_of_diagnosis": "01/01/2023", - "treatments": ["Chemotherapy", "Surgery"], + "treatments": ["Chemotherapy", "Transfusions"], "experiences": ["Anxiety", "Fatigue"], - "other_treatment": "Some custom treatment details", - "other_experience": "Custom experience notes", }, } @@ -171,8 +191,6 @@ def test_participant_with_cancer_only(db_session, test_user): # Assert - Cancer Experience assert user_data.diagnosis == "Leukemia" assert user_data.date_of_diagnosis == date(2023, 1, 1) - assert user_data.other_treatment == "Some custom treatment details" - assert user_data.other_experience == "Custom experience notes" # Assert - Flow Control assert user_data.has_blood_cancer == "yes" @@ -181,7 +199,7 @@ def test_participant_with_cancer_only(db_session, test_user): # Assert - Treatments (many-to-many) treatment_names = [t.name for t in user_data.treatments] assert "Chemotherapy" in treatment_names - assert "Surgery" in treatment_names + assert "Transfusions" in treatment_names assert len(user_data.treatments) == 2 # Assert - Experiences (many-to-many) @@ -203,80 +221,6 @@ def test_participant_with_cancer_only(db_session, test_user): raise -def test_custom_treatments_and_experiences(db_session, test_user): - """Test that custom treatments and experiences are created in the database""" - try: - # Arrange - processor = IntakeFormProcessor(db_session) - form_data = { - "form_type": "participant", - "has_blood_cancer": "yes", - "caring_for_someone": "no", - "personal_info": { - "first_name": "Jane", - "last_name": "Smith", - "date_of_birth": "20/12/1990", - "phone_number": "555-987-6543", - "city": "Vancouver", - "province": "British Columbia", - "postal_code": "V6B 1A1", - }, - "demographics": { - "gender_identity": "Female", - "pronouns": ["she", "her"], - "ethnic_group": ["Asian"], - "marital_status": "Single", - "has_kids": "no", - }, - "cancer_experience": { - "diagnosis": "Lymphoma", - "date_of_diagnosis": "15/06/2022", - "treatments": ["Custom Treatment X", "Experimental Therapy Y"], # New treatments - "experiences": ["Custom Symptom A", "Unique Experience B"], # New experiences - "other_treatment": "Details about experimental treatment", - "other_experience": "Unique side effects experienced", - }, - } - - # Verify custom treatments/experiences don't exist yet - assert db_session.query(Treatment).filter(Treatment.name == "Custom Treatment X").first() is None - assert db_session.query(Experience).filter(Experience.name == "Custom Symptom A").first() is None - - # Act - user_data = processor.process_form_submission(str(test_user.id), form_data) - - # Assert - New treatments were created - custom_treatment_x = db_session.query(Treatment).filter(Treatment.name == "Custom Treatment X").first() - custom_treatment_y = db_session.query(Treatment).filter(Treatment.name == "Experimental Therapy Y").first() - assert custom_treatment_x is not None - assert custom_treatment_y is not None - - # Assert - New experiences were created - custom_symptom_a = db_session.query(Experience).filter(Experience.name == "Custom Symptom A").first() - unique_experience_b = db_session.query(Experience).filter(Experience.name == "Unique Experience B").first() - assert custom_symptom_a is not None - assert unique_experience_b is not None - - # Assert - User is linked to custom treatments and experiences - user_treatment_names = [t.name for t in user_data.treatments] - user_experience_names = [e.name for e in user_data.experiences] - - assert "Custom Treatment X" in user_treatment_names - assert "Experimental Therapy Y" in user_treatment_names - assert "Custom Symptom A" in user_experience_names - assert "Unique Experience B" in user_experience_names - - # Assert - Custom text fields are stored - assert user_data.other_treatment == "Details about experimental treatment" - assert user_data.other_experience == "Unique side effects experienced" - - db_session.commit() - - except Exception: - db_session.rollback() - raise - - def test_volunteer_caregiver_experience_processing(db_session, test_user): """Test processing volunteer caregiver experience (separate from cancer experience)""" try: @@ -303,18 +247,15 @@ def test_volunteer_caregiver_experience_processing(db_session, test_user): "has_kids": "yes", }, "caregiver_experience": { - "experiences": ["Financial Stress", "Relationship Changes"], - "other_experience": "Dealing with healthcare system complexity", + "experiences": ["Anxiety", "Depression"], }, "loved_one": { "demographics": {"gender_identity": "Male", "age": "45-54"}, "cancer_experience": { "diagnosis": "Brain Cancer", "date_of_diagnosis": "10/05/2020", - "treatments": ["Surgery", "Radiation Therapy"], - "experiences": ["Depression", "Cognitive Changes"], - "other_treatment": "Specialized brain surgery", - "other_experience": "Memory issues post-surgery", + "treatments": ["Transfusions", "Radiation"], + "experiences": ["Depression", "Fatigue"], }, }, } @@ -338,9 +279,8 @@ def test_volunteer_caregiver_experience_processing(db_session, test_user): # Assert - Caregiver Experience (mapped to user experiences) experience_names = [e.name for e in user_data.experiences] - assert "Financial Stress" in experience_names - assert "Relationship Changes" in experience_names - assert user_data.other_experience == "Dealing with healthcare system complexity" + assert "Anxiety" in experience_names + assert "Depression" in experience_names # Assert - No personal cancer experience assert user_data.diagnosis is None @@ -357,10 +297,10 @@ def test_volunteer_caregiver_experience_processing(db_session, test_user): loved_one_treatment_names = [t.name for t in user_data.loved_one_treatments] loved_one_experience_names = [e.name for e in user_data.loved_one_experiences] - assert "Surgery" in loved_one_treatment_names - assert "Radiation Therapy" in loved_one_treatment_names + assert "Transfusions" in loved_one_treatment_names + assert "Radiation" in loved_one_treatment_names assert "Depression" in loved_one_experience_names - assert "Cognitive Changes" in loved_one_experience_names + assert "Fatigue" in loved_one_experience_names db_session.commit() @@ -399,20 +339,16 @@ def test_form_submission_json_structure(db_session, test_user): "cancer_experience": { "diagnosis": "Ovarian Cancer", "date_of_diagnosis": "03/07/2022", - "treatments": ["Chemotherapy", "Custom Treatment Protocol"], - "experiences": ["Anxiety", "Custom Side Effect"], - "other_treatment": "Experimental immunotherapy trial", - "other_experience": "Severe neuropathy affecting daily activities", + "treatments": ["Chemotherapy", "CAR-T"], + "experiences": ["Anxiety", "Fertility Issues"], }, "loved_one": { "demographics": {"gender_identity": "Female", "age": "65+"}, "cancer_experience": { "diagnosis": "Lung Cancer", "date_of_diagnosis": "15/01/2021", - "treatments": ["Radiation Therapy", "Palliative Care"], - "experiences": ["Sleep Problems", "Loss of Appetite"], - "other_treatment": "Comfort care measures", - "other_experience": "End-of-life care planning", + "treatments": ["Radiation", "Palliative Care"], + "experiences": ["Brain Fog", "Feeling Overwhelmed"], }, }, } @@ -427,32 +363,16 @@ def test_form_submission_json_structure(db_session, test_user): assert "Other" in user_data.ethnic_group and "Asian" in user_data.ethnic_group assert user_data.other_ethnic_group == "Mixed heritage - Filipino and Indigenous" - # Assert - Custom Treatments Created - custom_treatment = db_session.query(Treatment).filter(Treatment.name == "Custom Treatment Protocol").first() - assert custom_treatment is not None - assert custom_treatment in user_data.treatments - - # Assert - Custom Experiences Created - custom_experience = db_session.query(Experience).filter(Experience.name == "Custom Side Effect").first() - assert custom_experience is not None - assert custom_experience in user_data.experiences - - # Assert - "Other" Text Fields - assert user_data.other_treatment == "Experimental immunotherapy trial" - assert user_data.other_experience == "Severe neuropathy affecting daily activities" - # Assert - Loved One Complex Data assert user_data.loved_one_gender_identity == "Female" assert user_data.loved_one_age == "65+" assert user_data.loved_one_diagnosis == "Lung Cancer" - assert user_data.loved_one_other_treatment == "Comfort care measures" - assert user_data.loved_one_other_experience == "End-of-life care planning" # Assert - Both User and Loved One Have Relationships - assert len(user_data.treatments) >= 2 # Chemo + Custom - assert len(user_data.experiences) >= 2 # Anxiety + Custom + assert len(user_data.treatments) >= 2 # Chemo + CAR-T + assert len(user_data.experiences) >= 2 # Anxiety + Fertility assert len(user_data.loved_one_treatments) >= 2 # Radiation + Palliative - assert len(user_data.loved_one_experiences) >= 2 # Sleep + Appetite + assert len(user_data.loved_one_experiences) >= 2 # Brain Fog + Feeling Overwhelmed db_session.commit() @@ -506,7 +426,6 @@ def test_empty_and_minimal_data_handling(db_session, test_user): # Assert - Optional sections remain None/empty assert user_data.diagnosis is None - assert user_data.other_treatment is None assert len(user_data.treatments) == 0 assert len(user_data.experiences) == 0 assert user_data.loved_one_gender_identity is None @@ -548,10 +467,8 @@ def test_participant_caregiver_without_cancer(db_session, test_user): "cancer_experience": { "diagnosis": "Prostate Cancer", "date_of_diagnosis": "20/03/2021", - "treatments": ["Surgery", "Hormone Therapy"], - "experiences": ["Anxiety", "Relationship Changes"], - "other_treatment": "Robotic surgery", - "other_experience": "Intimacy concerns", + "treatments": ["Transfusions", "Immunotherapy"], + "experiences": ["Anxiety", "Communication Challenges"], }, }, } @@ -582,16 +499,14 @@ def test_participant_caregiver_without_cancer(db_session, test_user): assert user_data.loved_one_age == "55-64" assert user_data.loved_one_diagnosis == "Prostate Cancer" assert user_data.loved_one_date_of_diagnosis == date(2021, 3, 20) - assert user_data.loved_one_other_treatment == "Robotic surgery" - assert user_data.loved_one_other_experience == "Intimacy concerns" # Assert - Loved One Relationships loved_one_treatment_names = [t.name for t in user_data.loved_one_treatments] loved_one_experience_names = [e.name for e in user_data.loved_one_experiences] - assert "Surgery" in loved_one_treatment_names - assert "Hormone Therapy" in loved_one_treatment_names + assert "Transfusions" in loved_one_treatment_names + assert "Immunotherapy" in loved_one_treatment_names assert "Anxiety" in loved_one_experience_names - assert "Relationship Changes" in loved_one_experience_names + assert "Communication Challenges" in loved_one_experience_names db_session.commit() @@ -629,20 +544,16 @@ def test_participant_cancer_patient_and_caregiver(db_session, test_user): "cancer_experience": { "diagnosis": "Lymphoma", "date_of_diagnosis": "15/08/2022", - "treatments": ["Chemotherapy", "Radiation Therapy"], + "treatments": ["Chemotherapy", "Radiation"], "experiences": ["Fatigue", "Depression"], - "other_treatment": "Targeted therapy", - "other_experience": "Cognitive fog", }, "loved_one": { "demographics": {"gender_identity": "Female", "age": "35-44"}, "cancer_experience": { "diagnosis": "Breast Cancer", "date_of_diagnosis": "10/01/2023", - "treatments": ["Surgery", "Chemotherapy"], - "experiences": ["Hair Loss", "Body Image Issues"], - "other_treatment": "Reconstruction surgery", - "other_experience": "Fertility concerns", + "treatments": ["Transfusions", "Chemotherapy"], + "experiences": ["Graft vs Host", "Feeling Overwhelmed"], }, }, } @@ -657,28 +568,26 @@ def test_participant_cancer_patient_and_caregiver(db_session, test_user): # Assert - Own Cancer Experience assert user_data.diagnosis == "Lymphoma" assert user_data.date_of_diagnosis == date(2022, 8, 15) - assert user_data.other_treatment == "Targeted therapy" - assert user_data.other_experience == "Cognitive fog" # Assert - Own Treatments/Experiences treatment_names = [t.name for t in user_data.treatments] experience_names = [e.name for e in user_data.experiences] assert "Chemotherapy" in treatment_names - assert "Radiation Therapy" in treatment_names + assert "Radiation" in treatment_names assert "Fatigue" in experience_names assert "Depression" in experience_names # Assert - Loved One Data assert user_data.loved_one_diagnosis == "Breast Cancer" assert user_data.loved_one_date_of_diagnosis == date(2023, 1, 10) - assert user_data.loved_one_other_treatment == "Reconstruction surgery" - assert user_data.loved_one_other_experience == "Fertility concerns" # Assert - Loved One Relationships loved_one_treatment_names = [t.name for t in user_data.loved_one_treatments] loved_one_experience_names = [e.name for e in user_data.loved_one_experiences] - assert "Surgery" in loved_one_treatment_names - assert "Hair Loss" in loved_one_experience_names + assert "Transfusions" in loved_one_treatment_names + assert "Chemotherapy" in loved_one_treatment_names + assert "Graft vs Host" in loved_one_experience_names + assert "Feeling Overwhelmed" in loved_one_experience_names # Assert - Custom demographics assert "Other" in user_data.ethnic_group @@ -742,8 +651,6 @@ def test_participant_no_cancer_experience(db_session, test_user): # Assert - No cancer-related data assert user_data.diagnosis is None assert user_data.date_of_diagnosis is None - assert user_data.other_treatment is None - assert user_data.other_experience is None assert len(user_data.treatments) == 0 assert len(user_data.experiences) == 0 @@ -788,10 +695,8 @@ def test_volunteer_cancer_patient_only(db_session, test_user): "cancer_experience": { "diagnosis": "Myeloma", "date_of_diagnosis": "12/05/2019", - "treatments": ["Chemotherapy", "Stem Cell Transplant"], - "experiences": ["Depression", "Survivorship Concerns"], - "other_treatment": "Maintenance therapy", - "other_experience": "Long-term survivor guilt", + "treatments": ["Chemotherapy", "Autologous Stem Cell Transplant"], + "experiences": ["Depression", "Anxiety"], }, } @@ -809,16 +714,14 @@ def test_volunteer_cancer_patient_only(db_session, test_user): # Assert - Cancer Experience assert user_data.diagnosis == "Myeloma" assert user_data.date_of_diagnosis == date(2019, 5, 12) - assert user_data.other_treatment == "Maintenance therapy" - assert user_data.other_experience == "Long-term survivor guilt" # Assert - Treatments/Experiences treatment_names = [t.name for t in user_data.treatments] experience_names = [e.name for e in user_data.experiences] assert "Chemotherapy" in treatment_names - assert "Stem Cell Transplant" in treatment_names + assert "Autologous Stem Cell Transplant" in treatment_names assert "Depression" in experience_names - assert "Survivorship Concerns" in experience_names + assert "Anxiety" in experience_names # Assert - No loved one data (not a caregiver) assert user_data.loved_one_gender_identity is None @@ -861,20 +764,16 @@ def test_volunteer_cancer_patient_and_caregiver(db_session, test_user): "cancer_experience": { "diagnosis": "Breast Cancer", "date_of_diagnosis": "08/11/2015", - "treatments": ["Surgery", "Chemotherapy", "Radiation Therapy"], - "experiences": ["Hair Loss", "Survivorship Concerns"], - "other_treatment": "Hormone blocking therapy", - "other_experience": "10-year survivor perspective", + "treatments": ["Transfusions", "Chemotherapy", "Radiation"], + "experiences": ["Graft vs Host", "Anxiety"], }, "loved_one": { "demographics": {"gender_identity": "Male", "age": "65+"}, "cancer_experience": { "diagnosis": "Pancreatic Cancer", "date_of_diagnosis": "25/09/2023", - "treatments": ["Surgery", "Palliative Care"], - "experiences": ["Loss of Appetite", "Fatigue"], - "other_treatment": "Whipple procedure", - "other_experience": "End-of-life discussions", + "treatments": ["Transfusions", "Palliative Care"], + "experiences": ["Brain Fog", "Fatigue"], }, }, } @@ -889,21 +788,17 @@ def test_volunteer_cancer_patient_and_caregiver(db_session, test_user): # Assert - Own Cancer Experience (10-year survivor) assert user_data.diagnosis == "Breast Cancer" assert user_data.date_of_diagnosis == date(2015, 11, 8) - assert user_data.other_treatment == "Hormone blocking therapy" - assert user_data.other_experience == "10-year survivor perspective" # Assert - Own Treatments (comprehensive) treatment_names = [t.name for t in user_data.treatments] - assert "Surgery" in treatment_names + assert "Transfusions" in treatment_names assert "Chemotherapy" in treatment_names - assert "Radiation Therapy" in treatment_names + assert "Radiation" in treatment_names assert len(user_data.treatments) == 3 # Assert - Loved One Data (current patient) assert user_data.loved_one_diagnosis == "Pancreatic Cancer" assert user_data.loved_one_date_of_diagnosis == date(2023, 9, 25) - assert user_data.loved_one_other_treatment == "Whipple procedure" - assert user_data.loved_one_other_experience == "End-of-life discussions" # Assert - Both user and loved one have data assert len(user_data.treatments) >= 3 @@ -968,8 +863,6 @@ def test_volunteer_no_cancer_experience(db_session, test_user): # Assert - No cancer-related data assert user_data.diagnosis is None assert user_data.date_of_diagnosis is None - assert user_data.other_treatment is None - assert user_data.other_experience is None assert len(user_data.treatments) == 0 assert len(user_data.experiences) == 0 @@ -1177,8 +1070,6 @@ def test_text_trimming_and_normalization(db_session, test_user): "date_of_diagnosis": "01/01/2020", "treatments": [" Surgery ", " Chemotherapy "], "experiences": [" Fatigue "], - "other_treatment": " Custom treatment ", - "other_experience": " Custom experience ", }, } @@ -1194,8 +1085,6 @@ def test_text_trimming_and_normalization(db_session, test_user): assert user_data.gender_identity == "Male" assert user_data.marital_status == "Single" assert user_data.diagnosis == "Leukemia" - assert user_data.other_treatment == "Custom treatment" - assert user_data.other_experience == "Custom experience" db_session.commit() @@ -1228,7 +1117,6 @@ def test_sql_injection_prevention(db_session, test_user): "date_of_diagnosis": "01/01/2020", "treatments": ["Surgery"], "experiences": ["Fatigue"], - "other_treatment": "'; INSERT INTO admin_users VALUES (1); --", }, } @@ -1238,7 +1126,6 @@ def test_sql_injection_prevention(db_session, test_user): # Verify the malicious strings are stored as literal text, not executed assert user_data.first_name == "'; DROP TABLE users; --" assert "DELETE FROM user_data" in user_data.last_name - assert "INSERT INTO admin_users VALUES" in user_data.other_treatment # Verify no actual SQL injection occurred by checking database integrity user_count = db_session.query(User).count() @@ -1279,8 +1166,6 @@ def test_unicode_and_special_characters(db_session, test_user): "date_of_diagnosis": "01/01/2020", "treatments": ["Chimiothérapie"], "experiences": ["Fatigue"], - "other_treatment": "Traitement spécialisé avec émojis 💊🏥", - "other_experience": "Expérience émotionnelle complexe 😔➡️😊", }, } @@ -1294,8 +1179,6 @@ def test_unicode_and_special_characters(db_session, test_user): assert "中国人" in user_data.other_ethnic_group assert "हिन्दी" in user_data.other_ethnic_group assert "🌍" in user_data.other_ethnic_group - assert "💊🏥" in user_data.other_treatment - assert "😔➡️😊" in user_data.other_experience db_session.commit() diff --git a/frontend/src/constants/form.ts b/frontend/src/constants/form.ts index ebfdbdb3..5c1ca94b 100644 --- a/frontend/src/constants/form.ts +++ b/frontend/src/constants/form.ts @@ -66,14 +66,11 @@ export interface IntakeFormData { dateOfDiagnosis: string; treatments: string[]; experiences: string[]; - otherTreatment?: string; - otherExperience?: string; }; // User's Caregiver Experience (if applicable) caregiverExperience?: { experiences: string[]; - otherExperience?: string; }; // Loved One's Information (if applicable) @@ -88,8 +85,6 @@ export interface IntakeFormData { dateOfDiagnosis: string; treatments: string[]; experiences: string[]; - otherTreatment?: string; - otherExperience?: string; }; }; } @@ -123,13 +118,10 @@ export interface CancerExperienceData { dateOfDiagnosis: string; treatments: string[]; experiences: string[]; - otherTreatment?: string; - otherExperience?: string; } export interface CaregiverExperienceData { experiences: string[]; - otherExperience?: string; } export interface LovedOneData { From a3597cfc87943e0c8b757af4486bcbb5bee32106 Mon Sep 17 00:00:00 2001 From: Ethan Chen Date: Mon, 29 Sep 2025 19:27:26 -0400 Subject: [PATCH 09/13] Experiences seed: merge "anxiety" and "depression" into one row --- backend/app/seeds/experiences.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/app/seeds/experiences.py b/backend/app/seeds/experiences.py index 8c697308..110fe4c7 100644 --- a/backend/app/seeds/experiences.py +++ b/backend/app/seeds/experiences.py @@ -18,11 +18,10 @@ def seed_experiences(session: Session) -> None: {"id": 7, "name": "Returning to work or school after/during treatment", "scope": "patient"}, {"id": 8, "name": "Speaking to your family or friends about the diagnosis", "scope": "both"}, {"id": 9, "name": "Relapse", "scope": "patient"}, - {"id": 10, "name": "Anxiety", "scope": "both"}, + {"id": 10, "name": "Anxiety / Depression", "scope": "both"}, {"id": 11, "name": "PTSD", "scope": "both"}, {"id": 12, "name": "Caregiver Fatigue", "scope": "caregiver"}, {"id": 13, "name": "Managing practical challenges", "scope": "caregiver"}, - {"id": 14, "name": "Depression", "scope": "both"}, ] for experience_data in experiences_data: From 124d5f8e3b36b41aa7f5901569a822a284523a7e Mon Sep 17 00:00:00 2001 From: Ethan Chen Date: Mon, 29 Sep 2025 20:07:06 -0400 Subject: [PATCH 10/13] set loved one form to always fetch patient experience options, run prettier Co-authored-by: Ryan Gunawan Co-authored-by: Teresa Yu --- .../intake/demographic-cancer-form.tsx | 41 ++++++---------- .../src/components/intake/loved-one-form.tsx | 49 +++---------------- frontend/src/pages/participant/intake.tsx | 24 ++++++--- frontend/src/pages/volunteer/intake.tsx | 24 ++++++--- 4 files changed, 58 insertions(+), 80 deletions(-) diff --git a/frontend/src/components/intake/demographic-cancer-form.tsx b/frontend/src/components/intake/demographic-cancer-form.tsx index a0e1f5b3..57480c80 100644 --- a/frontend/src/components/intake/demographic-cancer-form.tsx +++ b/frontend/src/components/intake/demographic-cancer-form.tsx @@ -38,7 +38,7 @@ const StyledSelect = React.forwardRef( > {children} - ) + ), ); StyledSelect.displayName = 'StyledSelect'; @@ -267,15 +267,13 @@ const MultiSelectDropdown: React.FC<{ ); }; - -export function DemographicCancerForm({ formType, onNext, hasBloodCancer, caringForSomeone }: DemographicCancerFormProps) { - const { - control, - handleSubmit, - formState, - watch, - setValue, - } = useForm({ +export function DemographicCancerForm({ + formType, + onNext, + hasBloodCancer, + caringForSomeone, +}: DemographicCancerFormProps) { + const { control, handleSubmit, formState, watch } = useForm({ defaultValues: DEFAULT_VALUES, }); const { errors, isSubmitting } = formState; @@ -288,8 +286,6 @@ export function DemographicCancerForm({ formType, onNext, hasBloodCancer, caring const [experienceOptions, setExperienceOptions] = useState([]); useEffect(() => { - let cancelled = false; - const run = async () => { const hasBloodCancerBool = hasBloodCancer === 'yes'; const caringForSomeoneBool = caringForSomeone === 'yes'; @@ -305,13 +301,13 @@ export function DemographicCancerForm({ formType, onNext, hasBloodCancer, caring // This form should only render if at least one of these answers is "yes". console.error( 'Invalid intake flow state: neither hasBloodCancer nor caringForSomeone is "yes". ' + - `Received hasBloodCancer="${hasBloodCancer}", caringForSomeone="${caringForSomeone}". ` + - 'This Demographic Cancer form expects at least one to be "yes".' + `Received hasBloodCancer="${hasBloodCancer}", caringForSomeone="${caringForSomeone}". ` + + 'This Demographic Cancer form expects at least one to be "yes".', ); alert( 'We hit an unexpected state.\n\n' + - 'This step is only shown if you have blood cancer or are caring for someone with blood cancer. ' + - 'Please go back to the previous step and select "Yes" for one of those questions, or navigate to the basic demographics form.' + 'This step is only shown if you have blood cancer or are caring for someone with blood cancer. ' + + 'Please go back to the previous step and select "Yes" for one of those questions, or navigate to the basic demographics form.', ); return; } @@ -319,19 +315,14 @@ export function DemographicCancerForm({ formType, onNext, hasBloodCancer, caring const options = await getOptions(target); console.log(options); - setTreatmentOptions( - options.treatments.map((treatment: IntakeTreatment) => treatment.name) - ); + setTreatmentOptions(options.treatments.map((treatment: IntakeTreatment) => treatment.name)); setExperienceOptions( - options.experiences.map((experience: IntakeExperience) => experience.name) + options.experiences.map((experience: IntakeExperience) => experience.name), ); - } + }; run(); - return () => { - cancelled = true; - } }, [hasBloodCancer, caringForSomeone]); const genderIdentity = watch('genderIdentity') || ''; @@ -341,7 +332,7 @@ export function DemographicCancerForm({ formType, onNext, hasBloodCancer, caring const getOptions = async (target: string) => { const options = await baseAPIClient.get(`/intake/options?target=${target}`); return options.data; - } + }; const onSubmit = async (data: DemographicCancerFormData) => { try { // Merge custom values into the arrays diff --git a/frontend/src/components/intake/loved-one-form.tsx b/frontend/src/components/intake/loved-one-form.tsx index 7792d3e9..a47559c9 100644 --- a/frontend/src/components/intake/loved-one-form.tsx +++ b/frontend/src/components/intake/loved-one-form.tsx @@ -38,7 +38,7 @@ const StyledSelect = React.forwardRef( > {children} - ) + ), ); StyledSelect.displayName = 'StyledSelect'; @@ -77,17 +77,14 @@ const DIAGNOSIS_OPTIONS = [ interface LovedOneFormProps { formType?: 'participant' | 'volunteer'; onSubmit: (data: LovedOneFormData) => void; - hasBloodCancer?: 'yes' | 'no' | ''; - caringForSomeone?: 'yes' | 'no' | ''; } -export function LovedOneForm({ formType = 'participant', onSubmit, hasBloodCancer, caringForSomeone }: LovedOneFormProps) { +export function LovedOneForm({ formType = 'participant', onSubmit }: LovedOneFormProps) { const { control, handleSubmit, formState: { errors, isSubmitting }, watch, - setValue, } = useForm({ defaultValues: DEFAULT_VALUES, }); @@ -98,57 +95,27 @@ export function LovedOneForm({ formType = 'participant', onSubmit, hasBloodCance const [experienceOptions, setExperienceOptions] = useState([]); useEffect(() => { - let cancelled = false; - const run = async () => { - const hasBloodCancerBool = hasBloodCancer === 'yes'; - const caringForSomeoneBool = caringForSomeone === 'yes'; - - let target = ''; - if (hasBloodCancerBool && caringForSomeoneBool) { - target = 'both'; - } else if (hasBloodCancerBool) { - target = 'patient'; - } else if (caringForSomeoneBool) { - target = 'caregiver'; - } else { - // This form should only render if at least one of these answers is "yes". - console.error( - 'Invalid intake flow state: neither hasBloodCancer nor caringForSomeone is "yes". ' + - `Received hasBloodCancer="${hasBloodCancer}", caringForSomeone="${caringForSomeone}". ` + - 'This Demographic Cancer form expects at least one to be "yes".' - ); - alert( - 'We hit an unexpected state.\n\n' + - 'This step is only shown if you have blood cancer or are caring for someone with blood cancer. ' + - 'Please go back to the previous step and select "Yes" for one of those questions, or navigate to the basic demographics form.' - ); - return; - } + const target = 'patient'; const options = await getOptions(target); - setTreatmentOptions( - options.treatments.map((treatment: IntakeTreatment) => treatment.name) - ); + setTreatmentOptions(options.treatments.map((treatment: IntakeTreatment) => treatment.name)); setExperienceOptions( - options.experiences.map((experience: IntakeExperience) => experience.name) + options.experiences.map((experience: IntakeExperience) => experience.name), ); - } + }; run(); - return () => { - cancelled = true; - } - }, [hasBloodCancer, caringForSomeone]); + }, []); const genderIdentity = watch('genderIdentity') || ''; const getOptions = async (target: string) => { const options = await baseAPIClient.get(`/intake/options?target=${target}`); return options.data; - } + }; const onFormSubmit = async (data: LovedOneFormData) => { try { diff --git a/frontend/src/pages/participant/intake.tsx b/frontend/src/pages/participant/intake.tsx index c379ab3e..8c87b23f 100644 --- a/frontend/src/pages/participant/intake.tsx +++ b/frontend/src/pages/participant/intake.tsx @@ -133,10 +133,10 @@ export default function ParticipantIntakePage() { }), ...(prev.hasBloodCancer === 'no' && prev.caringForSomeone === 'yes' && { - caregiverExperience: { - experiences: data.experiences, - }, - }), + caregiverExperience: { + experiences: data.experiences, + }, + }), } as IntakeFormData; void advanceAfterUpdate(updated); @@ -210,15 +210,25 @@ export default function ParticipantIntakePage() { )} {currentStepType === 'demographics-cancer' && ( - + )} {currentStepType === 'demographics-caregiver' && ( - + )} {currentStepType === 'loved-one' && ( - + )} {currentStepType === 'demographics-basic' && ( diff --git a/frontend/src/pages/volunteer/intake.tsx b/frontend/src/pages/volunteer/intake.tsx index d0b9678d..1d8ddbf0 100644 --- a/frontend/src/pages/volunteer/intake.tsx +++ b/frontend/src/pages/volunteer/intake.tsx @@ -142,10 +142,10 @@ export default function VolunteerIntakePage() { }), ...(prev.hasBloodCancer === 'no' && prev.caringForSomeone === 'yes' && { - caregiverExperience: { - experiences: data.experiences, - }, - }), + caregiverExperience: { + experiences: data.experiences, + }, + }), } as IntakeFormData; void advanceAfterUpdate(updated); @@ -219,15 +219,25 @@ export default function VolunteerIntakePage() { )} {currentStepType === 'demographics-cancer' && ( - + )} {currentStepType === 'demographics-caregiver' && ( - + )} {currentStepType === 'loved-one' && ( - + )} {currentStepType === 'demographics-basic' && ( From ea7bc14c37637ffffd091e9b0b3d6a8e8e1e72fb Mon Sep 17 00:00:00 2001 From: IceRat1 <44247012+IceRat1@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:20:21 -0400 Subject: [PATCH 11/13] formatting --- backend/app/routes/intake.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/app/routes/intake.py b/backend/app/routes/intake.py index 85ad238a..a62775f2 100644 --- a/backend/app/routes/intake.py +++ b/backend/app/routes/intake.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List, Optional, Literal +from typing import List, Literal, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Request @@ -8,12 +8,11 @@ from sqlalchemy.orm import Session from app.middleware.auth import has_roles -from app.models import Form, FormSubmission, User, Experience, Treatment +from app.models import Experience, Form, FormSubmission, Treatment, User from app.schemas.user import UserRole from app.services.implementations.intake_form_processor import IntakeFormProcessor from app.utilities.db_utils import get_db - # ===== Schemas ===== From 02504df1d924cda7e82ae9191ffdec4c2102d735 Mon Sep 17 00:00:00 2001 From: IceRat1 <44247012+IceRat1@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:21:53 -0400 Subject: [PATCH 12/13] formatting again --- ...bd691_remove_other_treatment_and_other_.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/backend/migrations/versions/a59aeb0bd691_remove_other_treatment_and_other_.py b/backend/migrations/versions/a59aeb0bd691_remove_other_treatment_and_other_.py index 4e788c64..7f361da7 100644 --- a/backend/migrations/versions/a59aeb0bd691_remove_other_treatment_and_other_.py +++ b/backend/migrations/versions/a59aeb0bd691_remove_other_treatment_and_other_.py @@ -5,31 +5,32 @@ Create Date: 2025-09-25 20:22:55.535261 """ + from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision: str = 'a59aeb0bd691' -down_revision: Union[str, None] = '95467f4c5c80' +revision: str = "a59aeb0bd691" +down_revision: Union[str, None] = "95467f4c5c80" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('user_data', 'other_experience') - op.drop_column('user_data', 'loved_one_other_experience') - op.drop_column('user_data', 'other_treatment') - op.drop_column('user_data', 'loved_one_other_treatment') + op.drop_column("user_data", "other_experience") + op.drop_column("user_data", "loved_one_other_experience") + op.drop_column("user_data", "other_treatment") + op.drop_column("user_data", "loved_one_other_treatment") # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.add_column('user_data', sa.Column('loved_one_other_treatment', sa.TEXT(), autoincrement=False, nullable=True)) - op.add_column('user_data', sa.Column('other_treatment', sa.TEXT(), autoincrement=False, nullable=True)) - op.add_column('user_data', sa.Column('loved_one_other_experience', sa.TEXT(), autoincrement=False, nullable=True)) - op.add_column('user_data', sa.Column('other_experience', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column("user_data", sa.Column("loved_one_other_treatment", sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column("user_data", sa.Column("other_treatment", sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column("user_data", sa.Column("loved_one_other_experience", sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column("user_data", sa.Column("other_experience", sa.TEXT(), autoincrement=False, nullable=True)) # ### end Alembic commands ### From c313f1c411ac53286acfc546b10dbba79c5521ba Mon Sep 17 00:00:00 2001 From: Ethan Chen Date: Mon, 29 Sep 2025 21:04:52 -0400 Subject: [PATCH 13/13] adjust test_options_caregiver_without_cancer to use valid experience in loved_experiences --- backend/tests/unit/test_ranking_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/unit/test_ranking_service.py b/backend/tests/unit/test_ranking_service.py index 7f4983ef..56529055 100644 --- a/backend/tests/unit/test_ranking_service.py +++ b/backend/tests/unit/test_ranking_service.py @@ -180,7 +180,7 @@ async def test_options_caregiver_without_cancer(db_session: Session): self_experiences=["Caregiver Fatigue"], loved_one_diagnosis="CLL", loved_treatments=["Immunotherapy"], - loved_experiences=["Anxiety"], + loved_experiences=["Anxiety / Depression"], ) service = RankingService(db_session)