Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/run-tests-v1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ jobs:
install: false
start: npm run dev
working-directory: ./frontend
wait-on: "http://localhost:4040/"
wait-on: "http://localhost:4041/"
6 changes: 3 additions & 3 deletions .github/workflows/run-tests-v2.yml.old
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
image: ghcr.io/hackforla/homeuniteus/app:latest-test
options: --entrypoint /bin/bash --no-healthcheck
env:
CYPRESS_BASE_URL: http://frontend:4040
CYPRESS_BASE_URL: http://frontend:4041
CYPRESS_USE_MOCK: true
steps:
- name: Run Tests With Backend Mocking
Expand All @@ -102,7 +102,7 @@ jobs:
image: ghcr.io/hackforla/homeuniteus/app:latest-test
options: --entrypoint /bin/bash --no-healthcheck
env:
CYPRESS_BASE_URL: http://frontend:4040
CYPRESS_BASE_URL: http://frontend:4041
CYPRESS_USE_MOCK: false
CYPRESS_REAL_EMAIL: [email protected]
CYPRESS_REAL_PASSWORD: Test!123
Expand All @@ -127,4 +127,4 @@ jobs:
# install: false
# start: npm run dev
# working-directory: ./app
# wait-on: "http://[::1]:4040/"
# wait-on: "http://[::1]:4041/"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Building with Docker is the simplest option, and debugging applications within t

1. Build and run all containers by running the `docker compose up -d --build` shell command from the root directory:
2. Verify there are no build errors. If there are build errors, reach out to the development team.
3. Open `http://localhost:4040` in any browser to use Home Unite Us.
3. Open `http://localhost:4041` in any browser to use Home Unite Us.

* `pgAdmin4` is available at http://localhost:5050/browser/ to query the database.
* `moto` server is available at http://localhost:5000/moto-api/ to view mocked AWS data.
Expand Down
4 changes: 2 additions & 2 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
COGNITO_CLIENT_ID=testing
COGNITO_CLIENT_SECRET=testing
COGNITO_REGION=us-east-1
COGNITO_REDIRECT_URI=http://localhost:4040/signin
COGNITO_REDIRECT_URI=http://localhost:4041/signin
COGNITO_USER_POOL_ID=testing
COGNITO_ACCESS_ID=testing
COGNITO_ACCESS_KEY=testing
COGNITO_ENDPOINT_URL=http://127.0.0.1:5000
ROOT_URL=http://localhost:4040
ROOT_URL=http://localhost:4041
DATABASE_URL=postgresql+psycopg2://postgres:[email protected]:5432/huu
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ RUN chmod +x /code/startup_scripts/entrypoint.sh
WORKDIR /code
ENTRYPOINT ["/code/startup_scripts/entrypoint.sh"]
CMD []
EXPOSE 8000
EXPOSE 8080
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""make contact_info fields nullable

Revision ID: 0dd803590552
Revises: 24116d95dda6
Create Date: 2025-07-05 10:43:46.785939

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '0dd803590552'
down_revision = '24116d95dda6'
branch_labels = None
depends_on = None


def upgrade():
op.alter_column('contact_info', 'preferred_method',
existing_type=sa.String(length=50),
nullable=True)
op.alter_column('contact_info', 'phone_number',
existing_type=sa.String(length=20),
nullable=True)

def downgrade():
op.alter_column('contact_info', 'preferred_method',
existing_type=sa.String(length=50),
nullable=False)
op.alter_column('contact_info', 'phone_number',
existing_type=sa.String(length=20),
nullable=False)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Add user_id to contact_info

Revision ID: 24116d95dda6
Revises: 009fe89fbb99
Create Date: 2025-07-04 16:11:29.182946

"""
from alembic import op
import sqlalchemy as sa

revision = '24116d95dda6'
down_revision = '009fe89fbb99'
branch_labels = None
depends_on = None

def upgrade():
op.add_column('contact_info', sa.Column('user_id', sa.Integer(), nullable=True))

op.create_foreign_key(
'fk_contact_info_user_id',
'contact_info',
'user',
['user_id'],
['id'],
ondelete='CASCADE'
)

def downgrade():
op.drop_constraint('fk_contact_info_user_id', 'contact_info', type_='foreignkey')
op.drop_column('contact_info', 'user_id')
29 changes: 29 additions & 0 deletions backend/alembic/versions/5e9719d2f076_create_contact_info_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""create contact_info table

Revision ID: 009fe89fbb99
Revises: a1a53aaf81d3
Create Date: 2025-06-20 00:54:00.883606

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '009fe89fbb99'
down_revision = 'a1a53aaf81d3'
branch_labels = None
depends_on = None

def upgrade():
op.create_table(
'contact_info',
sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
sa.Column('preferred_method', sa.String(length=50), nullable=False),
sa.Column('phone_number', sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
)


def downgrade():
op.drop_table('contact_info')
6 changes: 3 additions & 3 deletions backend/alembic/versions/a1a53aaf81d3_initial_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ def upgrade() -> None:
sa.Column('title', sa.String(), nullable=False),
sa.Column('description', sa.String(), nullable=False),
sa.Column('created_at',
sa.DateTime(timezone=True),
server_default=sa.sql.func.utcnow(),
nullable=False), sa.PrimaryKeyConstraint('form_id'))
sa.DateTime(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False)),
op.create_table('housing_orgs',
sa.Column('housing_org_id', sa.Integer(), nullable=False),
sa.Column('org_name', sa.String(), nullable=False),
Expand Down
26 changes: 21 additions & 5 deletions backend/app/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker, DeclarativeBase
from sqlalchemy.types import JSON
from typing import Any
from typing import Any, Generator
from fastapi import Depends
import logging

from app.core.config import get_settings

logger = logging.getLogger(__name__)

_db_engine = None
Expand All @@ -14,6 +17,7 @@
class Base(DeclarativeBase):
type_annotation_map = {dict[str, Any]: JSON}


def init_db(engine):
if engine is None:
logger.error("Database engine initialization failed: engine is None")
Expand All @@ -23,7 +27,6 @@ def init_db(engine):
Base.metadata.create_all(bind=engine, checkfirst=True)
with engine.connect() as connection:
connection.execute(text("SELECT 1"))

except Exception as e:
logger.error("Database initialization failed", extra={
"error": str(e),
Expand All @@ -32,7 +35,7 @@ def init_db(engine):
raise


def db_engine(settings):
def db_engine(settings) -> "Engine":
global _db_engine
if _db_engine is None:
try:
Expand All @@ -46,7 +49,7 @@ def db_engine(settings):
return _db_engine


def db_session_factory(engine):
def db_session_factory(engine) -> sessionmaker:
global _DbSessionFactory
if _DbSessionFactory is None:
try:
Expand All @@ -61,4 +64,17 @@ def db_session_factory(engine):
"error_type": type(e).__name__
})
raise
return _DbSessionFactory
return _DbSessionFactory


def get_db(settings = Depends(get_settings)) -> Generator:
"""
FastAPI dependency that provides a SQLAlchemy Session.
"""
engine = db_engine(settings)
SessionLocal = db_session_factory(engine)
db = SessionLocal()
try:
yield db
finally:
db.close()
32 changes: 32 additions & 0 deletions backend/app/modules/host_dashboard/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from sqlalchemy.orm import Session
from . import models, schemas

def create_contact_info(db: Session, contact_info: schemas.ContactInfoCreate):
existing_entry = (
db.query(models.ContactInfo)
.filter(models.ContactInfo.user_id == contact_info.user_id)
.first()
)

# Update existing entry
if existing_entry:
existing_entry.preferred_method = contact_info.preferred_method
existing_entry.phone_number = contact_info.phone_number
db.commit()
db.refresh(existing_entry)
return existing_entry

# Create new entry
db_entry = models.ContactInfo(
preferred_method=contact_info.preferred_method,
phone_number=contact_info.phone_number,
user_id=contact_info.user_id
)
db.add(db_entry)
db.commit()
db.refresh(db_entry)
return db_entry

# Retrieve ContactInfo by its primary key (id).
def get_contact_info(db: Session, contact_id: int):
return db.query(models.ContactInfo).filter(models.ContactInfo.id == contact_id).first()
11 changes: 11 additions & 0 deletions backend/app/modules/host_dashboard/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from sqlalchemy import Column, Integer, String, DateTime, func, ForeignKey
from app.core.db import Base

class ContactInfo(Base):
__tablename__ = "contact_info"

id = Column(Integer, primary_key=True, index=True, autoincrement=True)
preferred_method = Column(String(50), nullable=True)
phone_number = Column(String(20), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE'), nullable=True)
50 changes: 50 additions & 0 deletions backend/app/modules/host_dashboard/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.modules.deps import DbSessionDep
from app.core.db import get_db
from . import schemas, crud, models

router = APIRouter()

@router.post("/contact-info")
def submit_contact_info(
contact_info: schemas.ContactInfoCreate,
db: DbSessionDep
):
return crud.create_contact_info(db, contact_info)

@router.get("/contact-info/{contact_id}", response_model=schemas.ContactInfo)
def get_contact_info(contact_id: int, db: DbSessionDep):
contact = crud.get_contact_info(db, contact_id)
if not contact:
raise HTTPException(status_code=404, detail="Contact info not found")
return contact


def determine_status(record: object, required_fields: list[str]) -> str:
if not record:
return "incomplete"

values = [getattr(record, field) for field in required_fields]
filled = [v is not None and v != "" for v in values]

if all(filled):
return "complete"
elif any(filled):
return "partial"
return "incomplete"

@router.get("/completion-status/{user_id}")
def get_completion_status(user_id: int, db: Session = Depends(get_db)):
status = {}

contact_info = db.query(models.ContactInfo).filter(models.ContactInfo.user_id == user_id).first()

contact_required_fields = ["preferred_method", "phone_number"]

if contact_info and contact_info.preferred_method == "Email":
contact_required_fields = ["preferred_method"]

status["Contact Information"] = determine_status(contact_info, contact_required_fields)

return status
18 changes: 18 additions & 0 deletions backend/app/modules/host_dashboard/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional

class ContactInfoCreate(BaseModel):
preferred_method: Optional[str] = Field(None, max_length=50)
phone_number: Optional[str] = Field(None, min_length=10, max_length=20)
user_id: int

class ContactInfo(BaseModel):
id: int
preferred_method: Optional[str]
phone_number: Optional[str]
created_at: datetime
user_id: int

class Config:
orm_mode = True
6 changes: 6 additions & 0 deletions backend/app/modules/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from app.modules.tenant_housing_orgs import controller as housing_org
from app.modules.workflow.dashboards.coordinator import coordinator_dashboard

from app.modules.host_dashboard import routes as host_dashboard_routes

api_router = APIRouter()

api_router.include_router(auth_controller.router,
Expand All @@ -23,3 +25,7 @@
prefix="/housing-orgs",
tags=["tenant_housing_orgs"])
api_router.include_router(coordinator_dashboard.router, tags=["coordinator"])

api_router.include_router(host_dashboard_routes.router,
prefix="/host-dashboard",
tags=["host-dashboard"])
8 changes: 6 additions & 2 deletions backend/startup_scripts/create_groups_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,17 @@ def create_group(groups, group, user_pool_id):
create_user(cognito_client, user_pool_id, email, group)
print(email + '/Test123! created.')

sql = 'INSERT INTO public.user (email, "firstName", "lastName", "roleId") VALUES (%s, %s, %s, %s) ON CONFLICT(email) DO NOTHING'
create_table = 'CREATE TABLE IF NOT EXISTS public.user (id SERIAL NOT NULL, email varchar(256), "firstName" varchar(256), "lastName" varchar(256), "roleId" integer, PRIMARY KEY (id));'

sql = 'INSERT INTO public.user (email, "firstName", "lastName", "roleId") VALUES (%s, %s, %s, %s)'
url = urlparse(os.environ['DATABASE_URL'])
with psycopg2.connect(database=url.path[1:],
user=url.username,
password=url.password,
host=url.hostname,
port=url.port) as db_conn:
with db_conn.cursor() as cur:
cur.executemany(sql, rows)
cur.execute(create_table)
db_conn.commit()
cur.executemany(sql, rows)
db_conn.commit()
Loading
Loading