Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/app/models/Experience.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
4 changes: 0 additions & 4 deletions backend/app/models/UserData.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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")
Expand Down
48 changes: 46 additions & 2 deletions backend/app/routes/intake.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from datetime import datetime
from typing import List, Optional
from typing import List, Literal, Optional
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
from app.models import Form, FormSubmission, User
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
Expand Down Expand Up @@ -49,6 +50,24 @@ class FormSubmissionListResponse(BaseModel):
total: int


class ExperienceResponse(BaseModel):
id: int
name: str
scope: Literal["patient", "caregiver", "both", "none"]
model_config = ConfigDict(from_attributes=True)


class TreatmentResponse(BaseModel):
id: int
name: str
model_config = ConfigDict(from_attributes=True)


class OptionsResponse(BaseModel):
experiences: List[ExperienceResponse]
treatments: List[TreatmentResponse]


# ===== Custom Auth Dependencies =====


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


@router.get(
"/options",
response_model=OptionsResponse,
)
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.VOLUNTEER, UserRole.ADMIN]),
):
try:
# Query DB Experience Table
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
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


# ===== Additional Utility Endpoints =====


Expand Down
25 changes: 13 additions & 12 deletions backend/app/seeds/experiences.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ 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": "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"},
{"id": 12, "name": "Caregiver Fatigue", "scope": "caregiver"},
{"id": 13, "name": "Managing practical challenges", "scope": "caregiver"},
]

for experience_data in experiences_data:
Expand Down
44 changes: 0 additions & 44 deletions backend/app/services/implementations/intake_form_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -213,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]):
"""
Expand All @@ -242,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]):
"""
Expand All @@ -258,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:
Expand All @@ -280,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."""
Expand Down Expand Up @@ -327,10 +301,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", [])
Expand All @@ -349,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."""
Expand All @@ -375,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]):
"""
Expand Down
4 changes: 0 additions & 4 deletions backend/docs/intake_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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)"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""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 ###
24 changes: 23 additions & 1 deletion backend/pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,7 +30,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"
Expand Down
Binary file modified backend/test_intake.db
Binary file not shown.
Loading