Skip to content

Commit f33269e

Browse files
authored
LLSC-65: Created form_status column on users table that keeps track o… (#59)
Added form_status column to users table to track onboarding progress. Added routing in the frontend to automatically take the user to the correct form/page based on their form_status. This way if i use logs in, and has already completed the intake form, they won't be shown the intake form again. Todo: we need methods to update statuses which will be done as part of the admin dashboard. We also need the backend for the volunteer secondary app. ## Notion ticket link <!-- Please replace with your ticket's URL --> [Form Routing](https://www.notion.so/uwblueprintexecs/Intake-Forms-Track-Completion-Status-and-Route-to-Correct-Form-based-on-this-27210f3fb1dc806d8defe3ef1b99eb68?v=27210f3fb1dc8133a6e1000cf4eabc81&source=copy_link) <!-- Give a quick summary of the implementation details, provide design justifications if necessary --> ## Implementation description * <!-- What should the reviewer do to verify your changes? Describe expected results and include screenshots when appropriate --> ## Steps to test 1. <!-- Draw attention to the substantial parts of your PR or anything you'd like a second opinion on --> ## What should reviewers focus on? * ## Checklist - [ ] My PR name is descriptive and in imperative tense - [ ] My commit messages are descriptive and in imperative tense. My commits are atomic and trivial commits are squashed or fixup'd into non-trivial commits - [ ] I have run the appropriate linter(s) - [ ] I have requested a review from the PL, as well as other devs who have background knowledge on this PR or who will be building on top of this PR
1 parent dc0387d commit f33269e

File tree

33 files changed

+1124
-346
lines changed

33 files changed

+1124
-346
lines changed

AGENTS.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
The repository separates API and UI concerns. `backend/app` houses the FastAPI stack—middlewares, models, services, routes, schemas, and utilities—with migrations in `backend/migrations`. Automated tests live in `backend/tests` (unit and functional) plus `e2e-tests` for request-driven smoke checks. The Next.js client resides in `frontend/src`, where `components`, `pages`, `contexts`, `APIClients`, and `utils` cover the feature surface.
5+
6+
## Build, Test, and Development Commands
7+
- `docker-compose up -d` boots Postgres and supporting services; pair with `docker-compose down -v` to rebuild from scratch.
8+
- `cd backend && pdm install` prepares Python deps; `pdm run dev` starts FastAPI on `http://localhost:8080`.
9+
- `cd frontend && npm install` sets up the UI; `npm run dev` runs the Next.js app on `http://localhost:3000`.
10+
- `pdm run tests` executes the backend pytest suite; `npm run lint` runs ESLint/TypeScript checks; `npm run build` verifies a production bundle.
11+
12+
## Coding Style & Naming Conventions
13+
Backend code targets Python 3.12 with Ruff enforcing 120-character lines, import sorting, and lint rules; run `pdm run ruff format` before committing. Use snake_case for files, modules, and functions, PascalCase for SQLAlchemy models and Pydantic schemas, and keep service interfaces under `services/interfaces`. Frontend TypeScript follows Prettier defaults (2-space indentation) and Next.js conventions: PascalCase React components, camelCase hooks, and colocated styles in `src/styles` when needed.
14+
15+
## Testing Guidelines
16+
Pytest expects files named `test_*.py` in `backend/tests`, with async fixtures available for FastAPI routes. Focus unit tests on service logic and place HTTP flow checks in `tests/functional`. Gather coverage with `pytest --cov=app` when shipping high-risk changes. The `e2e-tests` directory hosts request-based regression scripts; run them against a live stack (`BACKEND_URL` set) before deploying auth or entity flows. Frontend currently relies on linting and type checks—add component tests under `frontend/src/__tests__` when introducing complex interactions.
17+
18+
## Commit & Pull Request Guidelines
19+
Commits should be atomic and written in imperative mood (e.g., “Add intake confirmation flow”), mirroring existing history. Reference tickets in the body when relevant. Pull requests must summarize scope, note migrations or env updates, and include screenshots or GIFs for UI changes. Confirm linting, formatting, and tests ran successfully, and call out follow-up items so reviewers can assess risk quickly.
20+
21+
## Security & Configuration Tips
22+
Never commit secrets. Copy `.env.sample` into `backend/.env` and `frontend/.env`, sourcing values from the LLSC vault. Keep `backend/serviceAccountKey.json` local; it is already gitignored. When adding loggers, register names via `app/utilities/constants.py` to preserve structured logging across services.

CLAUDE.md

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
LLSC (Leukemia & Lymphoma Society of Canada) is a full-stack application built for matching participants with volunteers. The application uses a monorepo structure with separate frontend and backend services.
8+
9+
**Tech Stack:**
10+
- Backend: FastAPI (Python 3.12), SQLAlchemy, PostgreSQL, Alembic migrations
11+
- Frontend: Next.js, React, TypeScript, Chakra UI
12+
- Infrastructure: Docker, Docker Compose
13+
- Auth: Firebase Authentication
14+
- Email: AWS SES
15+
16+
## Development Commands
17+
18+
### Initial Setup
19+
```bash
20+
# Start Docker containers (required for database)
21+
docker-compose up -d
22+
23+
# Backend setup
24+
cd backend
25+
pdm install
26+
pdm run dev # Runs on http://localhost:8000
27+
28+
# Frontend setup
29+
cd frontend
30+
npm install
31+
npm run dev # Runs on http://localhost:3000
32+
```
33+
34+
### Backend Commands
35+
```bash
36+
cd backend
37+
38+
# Development
39+
pdm run dev # Start FastAPI dev server (port 8000)
40+
41+
# Database
42+
pdm run alembic revision --autogenerate -m "message" # Create migration
43+
pdm run alembic upgrade head # Apply migrations
44+
pdm run seed # Seed database with reference data
45+
pdm run db-reset # Reset DB: down, up, migrate, seed
46+
47+
# Testing
48+
pdm run tests # Run pytest tests
49+
50+
# Linting & Formatting
51+
pdm run ruff check . # Check linting
52+
pdm run ruff check --fix . # Fix linting issues
53+
pdm run ruff format . # Format code
54+
pdm run precommit # Run pre-commit hooks manually
55+
```
56+
57+
### Frontend Commands
58+
```bash
59+
cd frontend
60+
61+
npm run dev # Start Next.js dev server
62+
npm run build # Build for production
63+
npm run lint # Run ESLint
64+
npm run lint:fix # Fix ESLint issues
65+
npm run prettier:check # Check formatting
66+
npm run prettier:fix # Fix formatting
67+
npm run format # Run both prettier and eslint fixes
68+
```
69+
70+
### Docker Commands
71+
```bash
72+
# Database access
73+
docker exec -it llsc_db psql -U postgres -d llsc
74+
75+
# Container management
76+
docker-compose up --build # Rebuild and start
77+
docker-compose down # Stop containers
78+
docker-compose down --volumes # Stop and remove volumes
79+
docker ps # List running containers
80+
```
81+
82+
## Architecture
83+
84+
### Backend Structure
85+
86+
The backend follows a **layered service architecture**:
87+
88+
- **Models** (`app/models/`): SQLAlchemy ORM models. All models must be imported in `app/models/__init__.py` for Alembic autogeneration to work.
89+
- **Schemas** (`app/schemas/`): Pydantic schemas for request/response validation.
90+
- **Routes** (`app/routes/`): FastAPI route handlers that accept requests and return responses.
91+
- **Services** (`app/services/`): Business logic layer that routes call into.
92+
- `implementations/`: Concrete service implementations
93+
- **Interfaces** (`app/interfaces/`): Abstract base classes defining service contracts.
94+
- **Middleware** (`app/middleware/`): Request/response middleware (e.g., `AuthMiddleware`).
95+
- **Utilities** (`app/utilities/`): Shared utilities, constants, Firebase, SES initialization.
96+
- **Seeds** (`app/seeds/`): Database seeding scripts for reference data.
97+
98+
**Key Backend Files:**
99+
- `app/server.py`: FastAPI application initialization, middleware setup, route registration
100+
- `app/__init__.py`: Contains `run_migrations()` which auto-runs Alembic migrations on startup
101+
- `app/utilities/constants.py`: Contains `LOGGER_NAME()` function for creating standardized loggers
102+
103+
**Authentication Flow:**
104+
- Firebase tokens are validated via `AuthMiddleware`
105+
- Public paths are defined in `server.py` (`PUBLIC_PATHS`)
106+
- Protected routes require valid Firebase auth tokens
107+
108+
**Database Migrations:**
109+
When adding a new model, you MUST:
110+
1. Create the model file in `app/models/`
111+
2. Import it in `app/models/__init__.py` and add to `__all__`
112+
3. Run `pdm run alembic revision --autogenerate -m "description"`
113+
4. Run `pdm run alembic upgrade head`
114+
115+
### Frontend Structure
116+
117+
The frontend is a **Next.js application** using TypeScript and Chakra UI:
118+
119+
- **Pages** (`src/pages/`): Next.js file-based routing
120+
- `participant/`: Participant-specific pages (intake, ranking)
121+
- `volunteer/`: Volunteer-specific pages (intake, secondary application)
122+
- `admin/`: Admin dashboard pages
123+
- **Components** (`src/components/`): Reusable React components
124+
- `auth/`: Authentication-related components
125+
- `intake/`: Form intake components
126+
- `ranking/`: Ranking interface components
127+
- `ui/`: Base UI components
128+
- **API Clients** (`src/APIClients/`): API communication layer with backend
129+
- **Contexts** (`src/contexts/`): React Context providers for global state
130+
- **Hooks** (`src/hooks/`): Custom React hooks
131+
- **Types** (`src/types/`): TypeScript type definitions
132+
- **Utils** (`src/utils/`): Utility functions
133+
134+
**Key Frontend Files:**
135+
- `src/pages/_app.tsx`: Next.js app wrapper, providers setup
136+
- `src/pages/_document.tsx`: Custom document for Next.js
137+
138+
## Version Control
139+
140+
### Branching
141+
- Branch off `main` for all feature work
142+
- Branch naming: `<github-username>/<ticket-number>-description` (e.g., `mslwang/LLSC-42-readme-update`)
143+
- Use rebase instead of merge to integrate main: `git pull origin main --rebase`
144+
145+
### Commits
146+
- Commits should be atomic and self-contained
147+
- Use imperative tense with capitalized first word (e.g., "Add user authentication")
148+
- Squash trivial commits (typos, formatting) using `git rebase -i`
149+
150+
## Logging
151+
152+
To add a logger to any file:
153+
```python
154+
from app.utilities.constants import LOGGER_NAME
155+
import logging
156+
157+
log = logging.getLogger(LOGGER_NAME("my_service"))
158+
```
159+
160+
New logger names must be added to `alembic.ini` under the logger section.
161+
162+
## Testing
163+
164+
- Backend tests: `pdm run tests` (runs pytest in `backend/tests/`)
165+
- Test structure: `tests/unit/` and `tests/functional/`
166+
- CI runs linting, formatting checks, unit tests, and security scans on every PR
167+
168+
## Firebase Configuration
169+
170+
Backend requires:
171+
1. `serviceAccountKey.json` in `backend/` directory (obtain from Firebase Console)
172+
2. `FIREBASE_WEB_API_KEY` in `.env` file
173+
174+
## Environment Variables
175+
176+
Environment variables are stored in:
177+
- Backend: `.env` in project root (loaded by backend service)
178+
- Frontend: `frontend/.env`
179+
180+
Secrets are documented in the LLSC Notion workspace.

backend/app/models/User.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
import uuid
2+
from enum import Enum as PyEnum
23

34
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
5+
from sqlalchemy import Enum as SQLEnum
46
from sqlalchemy.dialects.postgresql import UUID
57
from sqlalchemy.orm import relationship
68

79
from .Base import Base
810
from .Match import Match
911

1012

13+
class FormStatus(str, PyEnum):
14+
INTAKE_TODO = "intake-todo"
15+
INTAKE_SUBMITTED = "intake-submitted"
16+
RANKING_TODO = "ranking-todo"
17+
RANKING_SUBMITTED = "ranking-submitted"
18+
SECONDARY_APPLICATION_TODO = "secondary-application-todo"
19+
SECONDARY_APPLICATION_SUBMITTED = "secondary-application-submitted"
20+
COMPLETED = "completed"
21+
22+
1123
class User(Base):
1224
__tablename__ = "users"
1325
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
@@ -18,6 +30,16 @@ class User(Base):
1830
auth_id = Column(String, nullable=False)
1931
approved = Column(Boolean, default=False)
2032
active = Column(Boolean, nullable=False, default=True)
33+
form_status = Column(
34+
SQLEnum(
35+
FormStatus,
36+
name="form_status_enum",
37+
create_type=False,
38+
values_callable=lambda enum_cls: [member.value for member in enum_cls],
39+
),
40+
nullable=False,
41+
default=FormStatus.INTAKE_TODO,
42+
)
2143

2244
role = relationship("Role")
2345

backend/app/models/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .SuggestedTime import suggested_times
2222
from .TimeBlock import TimeBlock
2323
from .Treatment import Treatment
24-
from .User import User
24+
from .User import FormStatus, User
2525
from .UserData import UserData
2626

2727
# Used to avoid import errors for the models
@@ -42,6 +42,7 @@
4242
"RankingPreference",
4343
"Form",
4444
"FormSubmission",
45+
"FormStatus",
4546
]
4647

4748
log = logging.getLogger(LOGGER_NAME("models"))

backend/app/routes/auth.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ async def get_current_user(
127127
role_id=user.role_id,
128128
auth_id=user.auth_id,
129129
approved=user.approved,
130+
form_status=user.form_status,
130131
)
131132
except HTTPException:
132133
raise

backend/app/schemas/user.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ def to_role_id(cls, role: "UserRole") -> int:
3535
return role_map[role]
3636

3737

38+
class FormStatus(str, Enum):
39+
INTAKE_TODO = "intake-todo"
40+
INTAKE_SUBMITTED = "intake-submitted"
41+
RANKING_TODO = "ranking-todo"
42+
RANKING_SUBMITTED = "ranking-submitted"
43+
SECONDARY_APPLICATION_TODO = "secondary-application-todo"
44+
SECONDARY_APPLICATION_SUBMITTED = "secondary-application-submitted"
45+
COMPLETED = "completed"
46+
47+
3848
class UserBase(BaseModel):
3949
"""
4050
Base schema for user model with common attributes shared across schemas.
@@ -91,6 +101,7 @@ class UserUpdateRequest(BaseModel):
91101
email: Optional[EmailStr] = None
92102
role: Optional[UserRole] = None
93103
approved: Optional[bool] = None
104+
form_status: Optional[FormStatus] = None
94105

95106

96107
class UserCreateResponse(BaseModel):
@@ -105,6 +116,7 @@ class UserCreateResponse(BaseModel):
105116
role_id: int
106117
auth_id: str
107118
approved: bool
119+
form_status: FormStatus
108120

109121
# from_attributes enables automatic mapping from SQLAlchemy model to Pydantic model
110122
model_config = ConfigDict(from_attributes=True)
@@ -123,6 +135,7 @@ class UserResponse(BaseModel):
123135
auth_id: str
124136
approved: bool
125137
role: "RoleResponse"
138+
form_status: FormStatus
126139

127140
model_config = ConfigDict(from_attributes=True)
128141

backend/app/seeds/users.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from app.models.Experience import Experience
99
from app.models.Treatment import Treatment
10-
from app.models.User import User
10+
from app.models.User import FormStatus, User
1111
from app.models.UserData import UserData
1212
from app.utilities.form_constants import ExperienceId, TreatmentId
1313

@@ -316,6 +316,7 @@ def seed_users(session: Session) -> None:
316316
auth_id=user_info["user_data"]["auth_id"],
317317
approved=True,
318318
active=True,
319+
form_status=FormStatus.INTAKE_TODO,
319320
)
320321
session.add(user)
321322
session.flush() # Get user ID

backend/app/services/implementations/intake_form_processor.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from sqlalchemy.orm import Session
77

8-
from app.models import Experience, Treatment, User, UserData
8+
from app.models import Experience, FormStatus, Treatment, User, UserData
99

1010
logger = logging.getLogger(__name__)
1111

@@ -72,6 +72,14 @@ def process_form_submission(self, user_id: str, form_data: Dict[str, Any]) -> Us
7272
if owning_user and owning_user.email:
7373
user_data.email = owning_user.email
7474

75+
# Update form status for the owning user without regressing progress
76+
owning_user = self.db.query(User).filter(User.id == user_data.user_id).first()
77+
if owning_user and owning_user.form_status in {
78+
FormStatus.INTAKE_TODO,
79+
FormStatus.INTAKE_SUBMITTED,
80+
}:
81+
owning_user.form_status = FormStatus.INTAKE_SUBMITTED
82+
7583
# Commit all changes
7684
self.db.commit()
7785
self.db.refresh(user_data)

backend/app/services/implementations/ranking_service.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from sqlalchemy.orm import Session
44

5-
from app.models import Quality, User, UserData
5+
from app.models import FormStatus, Quality, User, UserData
66
from app.models.RankingPreference import RankingPreference
77

88

@@ -205,4 +205,11 @@ def save_preferences(self, user_auth_id: str, target: str, items: List[Dict]) ->
205205
)
206206
if normalized:
207207
self.db.bulk_save_objects(normalized)
208+
209+
if user.form_status in (
210+
FormStatus.RANKING_TODO,
211+
FormStatus.RANKING_SUBMITTED,
212+
):
213+
user.form_status = FormStatus.RANKING_SUBMITTED
214+
208215
self.db.commit()

0 commit comments

Comments
 (0)