Skip to content

Commit

Permalink
Merge pull request #768 from hackforla/refactor/backend-data-flow
Browse files Browse the repository at this point in the history
use db data for coordinator dashboard, initial admin view
  • Loading branch information
tylerthome authored Sep 6, 2024
2 parents 04ae4f5 + 0ac5f74 commit e238d7c
Show file tree
Hide file tree
Showing 25 changed files with 995 additions and 51 deletions.
44 changes: 44 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# using a generic base image since we want to install python, nodejs, etc
FROM mcr.microsoft.com/vscode/devcontainers/base:dev-bullseye

# use latest available system package listings and installations
RUN sudo apt-get update -y && sudo apt-get upgrade -y

# we need `curl` to download things, and `build-essential` to
# install python and node from source
RUN sudo apt-get install -y \
curl \
build-essential \
libsqlite3-dev \
libpq-dev

# these packages currently using 3.9 as latest available,
# ideally these would be included in the `apt-get` command:
# - python3
# - python3-dev

# keep dependency installation resources separate from source
WORKDIR /opt

# download and install python from source
# as of this writing, the latest package supported by apt is Python 3.9, and
# this is the simplest way to get Python 3.12+ installed for dev
RUN curl -LO https://www.python.org/ftp/python/3.12.5/Python-3.12.5.tgz \
&& tar xzf Python-3.12.5.tgz \
&& cd Python-3.12.5 \
&& ./configure --enable-loadable-sqlite-extensions \
&& make \
&& sudo make install \
&& cd ..

# intall nodejs from prebuilt binaries
# this approach avoids an issue with other provided installation steps
# using Node/Vercel tools and scripts, namely surrounding the execution
# of `source` and `.` commands during a Docker build
RUN curl -LO https://nodejs.org/dist/v20.17.0/node-v20.17.0-linux-x64.tar.xz \
&& tar xJf node-v20.17.0-linux-x64.tar.xz \
&& cd node-v20.17.0-linux-x64 \
&& cp -r ./bin/* /usr/local/bin/ \
&& cp -r ./lib/* /usr/local/lib/ \
&& cp -r ./share/* /usr/local/share/ \
&& cd ..
27 changes: 27 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "FullStack",
"build": {
"dockerfile": "./Dockerfile",
"context": ".."
},
// install the decalred pip and npm packages
"postCreateCommand": "bash ./scripts/install-deps-debian.bash",
"runArgs": ["--platform=linux/amd64"],
"forwardPorts": [
38429,
38428
],
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"extensions.verifySignature": false
},
"extensions": [
"ms-python.vscode-pylance",
"ms-vscode.vscode-typescript-next"
]
}
}
}

32 changes: 32 additions & 0 deletions api/.devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# using a generic base image since we want to install python, nodejs, etc
FROM mcr.microsoft.com/vscode/devcontainers/base:dev-bullseye

# use latest available system package listings and installations
RUN sudo apt-get update -y && sudo apt-get upgrade -y

# we need `curl` to download things, and `build-essential` to
# install python and node from source
RUN sudo apt-get install -y \
curl \
build-essential \
libsqlite3-dev \
libpq-dev

# these packages currently using 3.9 as latest available,
# ideally these would be included in the `apt-get` command:
# - python3
# - python3-dev

# keep dependency installation resources separate from source
WORKDIR /opt

# download and install python from source
# as of this writing, the latest package supported by apt is Python 3.9, and
# this is the simplest way to get Python 3.12+ installed for dev
RUN curl -LO https://www.python.org/ftp/python/3.12.5/Python-3.12.5.tgz \
&& tar xzf Python-3.12.5.tgz \
&& cd Python-3.12.5 \
&& ./configure --enable-loadable-sqlite-extensions \
&& make \
&& sudo make install \
&& cd ..
25 changes: 25 additions & 0 deletions api/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "Python",
"build": {
"dockerfile": "./Dockerfile",
"context": ".."
},
"postCreateCommand": "python3 -m pip install .",
//// TODO: if we can use the latest Python offered in devcontainers, this may
//// provide a better dev UX
// "image": "mcr.microsoft.com/vscode/devcontainers/python:3.9-bullseye",
"runArgs": ["--platform=linux/amd64"],
"forwardPorts": [
38429
],
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"extensions.verifySignature": false
},
"extensions": ["ms-python.vscode-pylance"]
}
}
}

17 changes: 15 additions & 2 deletions api/openapi_server/controllers/auth_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import requests
import jwt
import random
import json

from flask import (
redirect,
Expand All @@ -12,7 +13,7 @@
)
from openapi_server.exceptions import AuthError
from openapi_server.models.database import DataAccessLayer, User
from openapi_server.repositories.user_repo import UserRepository
from openapi_server.repositories.user_repo import UnmatchedCaseRepository, UserRepository
from openapi_server.models.user_roles import UserRole
from openapi_server.models.schema import user_schema
from sqlalchemy import select
Expand Down Expand Up @@ -615,7 +616,7 @@ def google():
def invite():

if connexion.request.is_json:
body = connexion.request.get_json()
body = connexion.request.get_json()

# TODO: Possibly encrypt these passwords?
numbers = '0123456789'
Expand Down Expand Up @@ -648,6 +649,7 @@ def invite():
raise AuthError({"message": msg}, 500)

try:
coordinator_email = session['username']
with DataAccessLayer.session() as db_session:
user_repo = UserRepository(db_session)
user_repo.add_user(
Expand All @@ -657,8 +659,19 @@ def invite():
middleName=body.get('middleName', ''),
lastName=body.get('lastName', '')
)
guest_id = user_repo.get_user_id(body['email'])
coordinator_id = user_repo.get_user_id(coordinator_email)
unmatched_case_repo = UnmatchedCaseRepository(db_session)
unmatched_case_repo.add_case(
guest_id=guest_id,
coordinator_id=coordinator_id
)
except Exception as error:
raise AuthError({"message": str(error)}, 400)






def confirm_invite():
Expand Down
71 changes: 71 additions & 0 deletions api/openapi_server/controllers/coordinator_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from flask import Response

from openapi_server.models.database import DataAccessLayer
from openapi_server.models.schema import users_schema
from openapi_server.models.user_roles import UserRole
from openapi_server.repositories.user_repo import UserRepository, UnmatchedCaseRepository

import json
"""
userName:
type: string
caseStatus:
type: string
coordinatorName:
type: string
userType:
type: string
lastUpdated:
type: string
notes:
type: string
"""
def get_dashboard_data() -> Response:
with DataAccessLayer.session() as session:
user_repo = UserRepository(session)
coordinator_users_by_id = {x.id: x for x in user_repo.get_users_with_role(UserRole.COORDINATOR)}
print(f'get_dashboard_data(): coordinator_users_by_id = {json.dumps({k:v.email for k,v in coordinator_users_by_id.items()})}')
case_repo = UnmatchedCaseRepository(session)

all_users = []
for guest in user_repo.get_users_with_role(UserRole.GUEST):
print(f'get_dashboard_data(): looking at guest: {guest.email} with ID "{guest.id}"')
case_status = case_repo.get_case_for_guest(int(guest.id))
print(f'get_dashboard_data(): get_case_for_guest({guest.id}) returned "{case_status}"')
coordinator = coordinator_users_by_id[case_status.coordinator_id]
all_users.append({
'id': guest.id,
'userName': f'{guest.firstName} {guest.lastName}',
'caseStatus': 'In Progress',
'userType':'GUEST',
'coordinatorName': f'{coordinator.firstName} {coordinator.lastName}',
'lastUpdated': '2024-08-25',
'Notes': 'N/A'
})

for host in user_repo.get_users_with_role(UserRole.HOST):
all_users.append({
'id': host.id,
'userName': f'{host.firstName} {host.lastName}',
'caseStatus': 'In Progress',
'userType':'HOST',
'coordinatorName': f'N/A',
'lastUpdated': '2024-08-25',
'Notes': 'N/A'
})

for coordinator in user_repo.get_users_with_role(UserRole.COORDINATOR):
all_users.append({
'id': coordinator.id,
'userName': f'{coordinator.firstName} {coordinator.lastName}',
'caseStatus': 'N/A',
'userType':'COORDINATOR',
'coordinatorName': f'N/A',
'lastUpdated': '2024-08-25',
'Notes': 'N/A'
})

return {
'dashboardItems': all_users
}, 200
55 changes: 48 additions & 7 deletions api/openapi_server/controllers/users_controller.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,63 @@

import string

from openapi_server.controllers.auth_controller import get_token_auth_header

from openapi_server.models.database import DataAccessLayer, User
from flask import session, current_app
from sqlalchemy import delete
from sqlalchemy.exc import IntegrityError
from openapi_server.exceptions import AuthError
from openapi_server.repositories.user_repo import UnmatchedCaseRepository, UserRepository

from openapi_server.models.database import Role, UnmatchedGuestCase
from openapi_server.models.user_roles import UserRole

def delete_user(user_id: int):

# get the user's username (i.e. email) from db
with DataAccessLayer.session() as db_session:
try:
user_repo = UserRepository(db_session)
user: User = user_repo.get_user_by_id(user_id)
role = db_session.query(Role).filter_by(id=user.role_id).first()

if role.name == UserRole.GUEST.value:
unmatched_cases_repo = UnmatchedCaseRepository(db_session)
unmatched_cases_repo.delete_case_for_guest(user.id)

unmatched_cases = []
if role.name == UserRole.COORDINATOR.value:
unmatched_cases = db_session.query(UnmatchedGuestCase).filter_by(coordinator_id=user.id).all()


def delete_user(user_id: string):
# get access token from header
access_token = get_token_auth_header()
if len(unmatched_cases) > 0:
user_repo = UserRepository(db_session)
guests_by_id = {x.id: x for x in user_repo.get_users_with_role(UserRole.GUEST)}
guest_emails_with_ids = [{
'id': x.guest_id,
'email': guests_by_id[x.guest_id].email,
} for x in unmatched_cases]

guest_emails_with_ids_strs = [f'{g["email"]} (#{g["id"]})' for g in guest_emails_with_ids]

return {
"message": f"Coordinator is associated with {len(unmatched_cases)} case(s). Move these Guest(s) to a different Coordinator before attempting to delete this account",
"items":guest_emails_with_ids_strs
}, 400

except AuthError as auth_error:
raise auth_error
except IntegrityError:
db_session.rollback()
raise AuthError({
"message": "An error occured while removing user to database."
}, 422)


# delete user from cognito
try:
response = current_app.boto_client.delete_user(
AccessToken=access_token
response = current_app.boto_client.admin_delete_user(
UserPoolId=current_app.config['COGNITO_USER_POOL_ID'],
Username=user.email
)
except Exception as e:
code = e.response['Error']['Code']
Expand Down
14 changes: 14 additions & 0 deletions api/openapi_server/models/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ def validate_first_name(self, key, value):
raise ValueError(f"{key} must contain at least one non-space character")
return value.strip()

class UnmatchedGuestCase(Base):
__tablename__ = "unmatched_guest_case"
id = Column(Integer, primary_key=True, index=True)
guest_id = Column(Integer, ForeignKey('user.id'), nullable=False)
coordinator_id = Column(Integer, ForeignKey('user.id'), nullable=False)
status_id = Column(Integer, ForeignKey('unmatched_guest_case_status.id'), nullable=False)
status = relationship("UnmatchedGuestCaseStatus", back_populates="cases")

class UnmatchedGuestCaseStatus(Base):
__tablename__ = "unmatched_guest_case_status"
id = Column(Integer, primary_key=True, index=True)
status_text = Column(String(255), nullable=False, unique=True)
cases = relationship("UnmatchedGuestCase", back_populates="status")

class Role(Base):
__tablename__ = "role"
id = Column(Integer, primary_key=True, index=True)
Expand Down
16 changes: 15 additions & 1 deletion api/openapi_server/models/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ class Meta:
include_relationships = True
load_instance = True

class UnmatchedCaseSchema(SQLAlchemyAutoSchema):
class Meta:
model = UnmatchedGuestCase
include_relationships = True
load_instance = True

class UnmatchedCaseStatusSchema(SQLAlchemyAutoSchema):
class Meta:
model = UnmatchedGuestCaseStatus
include_relationships = True
load_instance = True

class UserSchema(SQLAlchemyAutoSchema):
role = Nested(RoleSchema, only=['name'], required=True)
class Meta:
Expand Down Expand Up @@ -146,4 +158,6 @@ def make_response(self, data, **kwargs):
service_provider_schema = HousingProgramServiceProviderSchema()
service_provider_list_schema = HousingProgramServiceProviderSchema(many=True)
form_schema = FormSchema()
response_schema = ResponseSchema(many=True)
response_schema = ResponseSchema(many=True)
unmatched_cs_schema = UnmatchedCaseStatusSchema()
unmatched_c_schema = UnmatchedCaseSchema()
8 changes: 7 additions & 1 deletion api/openapi_server/models/user_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ class UserRole(Enum):
ADMIN = "Admin"
GUEST = "Guest"
HOST = "Host"
COORDINATOR = "Coordinator"
COORDINATOR = "Coordinator"



class UmatchedCaseStatus(Enum):
IN_PROGRESS = "In Progress"
COMPLETE = "Complete"
Loading

0 comments on commit e238d7c

Please sign in to comment.