Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scorecard basics - assign ladder and track progress [gh-22] #23

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.env
.coverage
# dependencies
**/node_modules
**/.pnp
Expand Down
13 changes: 12 additions & 1 deletion backend/postgres/docker-entrypoint-initdb.d/daos.sql
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,15 @@ CREATE TRIGGER update_user_task_updated_on
ON
buckets
FOR EACH ROW
EXECUTE PROCEDURE update_edit_date();
EXECUTE PROCEDURE update_edit_date();

--- Create read model for user scorecard
CREATE TABLE users
(
username varchar(255) PRIMARY KEY,
first_name varchar(255),
last_name varchar(255),
email varchar(255),
manager_identifier varchar(255),
activity integer DEFAULT 0
);
13 changes: 13 additions & 0 deletions backend/src/cpf/adapters/inbound/rest_api/rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from cpf.core.ports.provided.services import (
ManageService,
QueryService,
ScorecardManageService,
UserManagementService,
)
from cpf.core.ports.required.dtos import UserDTO
Expand All @@ -24,6 +25,7 @@
library_manage_service: ManageService | None = None
library_query_service: QueryService | None = None
user_management_service: UserManagementService | None = None
scorecard_service: ScorecardManageService | None = None


def set_library_manage_service(service: ManageService):
Expand Down Expand Up @@ -59,6 +61,17 @@ def get_user_management_service() -> UserManagementService:
return user_management_service


def set_scorecard_manage_service(service: ScorecardManageService) -> None:
global scorecard_service
scorecard_service = service


def get_scorecard_manage_service() -> ScorecardManageService:
if not scorecard_service:
raise RuntimeError("Scorecard service not set")
return scorecard_service


class FastAPIAuth:

def __call__(self, request: Request) -> UserDTO:
Expand Down
53 changes: 50 additions & 3 deletions backend/src/cpf/adapters/inbound/rest_api/users/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
from fastapi import APIRouter, Depends

from cpf.adapters.inbound.rest_api.permissions import check_permissions
from cpf.core.ports.provided.services import UserManagementService
from cpf.core.ports.provided.services import (
ScorecardManageService,
UserManagementService,
)
from cpf.core.ports.required.dtos import UserDTO
from cpf.core.ports.required.readmodels import UserReadModel

from ..rest_api import auth, get_user_management_service
from .models.requests import PutUser
from ..rest_api import auth, get_scorecard_manage_service, get_user_management_service
from .models.requests import PutUser, UserProgress
from .models.responses import UserResponse

router = APIRouter(prefix=f"{os.getenv('BASE_URL')}/users")
Expand All @@ -30,3 +34,46 @@ def create_new_user(
first_name=new_user.first_name,
last_name=new_user.last_name,
)


@router.get(path="", response_model_exclude_none=True)
@check_permissions(permission_classes=[])
def get_users(
user: Annotated[UserDTO, Depends(auth)], service: UserManagementService = Depends(get_user_management_service)
) -> list[UserResponse]:
users: list[UserReadModel] = service.get_users(manager_identifier=user.username)
return [
UserResponse(
first_name=user.first_name,
last_name=user.last_name,
email=user.email,
username=user.username,
)
for user in users
]


@router.get(path="/{username}", response_model_exclude_none=True)
def get_user_scorecard(username: str, service: ScorecardManageService = Depends(get_scorecard_manage_service)):
user_scorecard = service.get_user_scorecard(username=username)
return user_scorecard


@router.put(path="/{username}/ladder/{ladder_slug}", response_model_exclude_none=True)
def set_user_ladder(
username: str, ladder_slug: str, service: ScorecardManageService = Depends(get_scorecard_manage_service)
):
user_scorecard = service.set_ladder(username=username, ladder_slug=ladder_slug)
return user_scorecard


@router.put(path="/{username}/progress", response_model_exclude_none=True)
def update_user_progress(
username: str, request: UserProgress, service: ScorecardManageService = Depends(get_scorecard_manage_service)
):
user_scorecard = service.update_user_progress(
username=username,
bucket_slug=request.bucket_slug,
atomic_skills=request.atomic_skills,
)
return user_scorecard
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ class PutUser(BaseModel):
first_name: str = Field(..., description="New user first name")
last_name: str = Field(..., description="New user last name")
email: EmailStr = Field(..., description="New user email")


class UserProgress(BaseModel):
atomic_skills: list[str] = Field(..., description="Archived skills")
bucket_slug: str = Field(..., description="Bucket slug")
97 changes: 93 additions & 4 deletions backend/src/cpf/adapters/outbound/postgres/daos.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import json
import os
from contextlib import contextmanager
from typing import Generator
from typing import Any, Generator

from psycopg2.extensions import connection as Connection
from psycopg2.extensions import cursor as Cursor
from psycopg2.pool import SimpleConnectionPool

from cpf.core.domain.aggregates.users.aggregate import User
from cpf.core.domain.enums import BucketType
from cpf.core.ports.required.daos import BucketReadModelDao as BaseBucketReadModelDao
from cpf.core.ports.required.daos import LadderReadModelDao as BaseLadderReadModelDao
from cpf.core.ports.required.readmodels import BucketReadModel, LadderReadModel
from cpf.core.ports.required.daos import ScorecardDao
from cpf.core.ports.required.readmodels import (
BucketReadModel,
LadderReadModel,
UserReadModel,
)


class BucketReadModelDao(BaseBucketReadModelDao):
Expand Down Expand Up @@ -165,11 +171,13 @@ def all(self) -> list[LadderReadModel]:
ladders.append(LadderReadModel(slug=ladder_slug, ladder_name=ladder_data["ladder_name"]))
return ladders

def get(self, uuid: str) -> LadderReadModel:
def get(self, slug: str) -> LadderReadModel | None:
with self._get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM ladders WHERE ladder_slug = %s", (uuid,))
cursor.execute("SELECT * FROM ladders WHERE ladder_slug = %s", (slug,))
result = cursor.fetchone()
if not result:
return None
ladder_slug, ladder_data, _ = result

return LadderReadModel(
Expand All @@ -186,6 +194,83 @@ def get(self, uuid: str) -> LadderReadModel:
)


class UserScorecardDao(ScorecardDao):

def __init__(self, connection_pool: SimpleConnectionPool):
self._pool = connection_pool

@contextmanager
def _get_connection(self) -> Generator[Connection, None, None]:
conn = self._pool.getconn()
try:
yield conn
finally:
self._pool.putconn(conn)

@staticmethod
def _execute_create(
cursor: Cursor,
username: str,
first_name: str,
last_name: str,
email: str,
manager_identifier: str,
) -> None:
cursor.execute(
"INSERT INTO users VALUES (%s, %s, %s, %s, %s)",
(username, first_name, last_name, email, manager_identifier),
)

@staticmethod
def _execute_update(cursor: Cursor, username: str, manager_identifier: str, activity: int) -> None:
cursor.execute(
"UPDATE users SET manager_identifier = %s, activity = %s WHERE username = %s",
(manager_identifier, activity, username),
)

@staticmethod
def _get_record_from_db(cursor: Cursor, username) -> tuple[Any] | None:
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
result = cursor.fetchone()
if not result:
return None
return result

def save(self, aggregate: User) -> None:
with self._get_connection() as conn:
with conn.cursor() as cursor:
if self._get_record_from_db(cursor, aggregate.aggregate_id):
self._execute_update(
cursor=cursor,
username=aggregate.aggregate_id,
manager_identifier="admin",
activity=0,
)
else:
self._execute_create(
cursor=cursor,
username=aggregate.aggregate_id,
first_name=aggregate.first_name,
last_name=aggregate.last_name,
email=aggregate.email,
manager_identifier="admin",
)
conn.commit()

def all_users(self) -> list[UserReadModel]:
with self._get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM users")
results = cursor.fetchall()
read_models: list[UserReadModel] = []
for result in results:
username, first_name, last_name, email, manager_identifier, activity = result
read_models.append(
UserReadModel(username=username, first_name=first_name, last_name=last_name, email=email)
)
return read_models


connection_pool = SimpleConnectionPool(
minconn=os.getenv("POSTGRES_MIN_CONNECTIONS", 1),
maxconn=os.getenv("POSTGRES_MAX_CONNECTIONS", 10),
Expand All @@ -203,3 +288,7 @@ def ladder_dao_factory() -> BaseLadderReadModelDao:

def bucket_dao_factory() -> BaseBucketReadModelDao:
return BucketReadModelDao(connection_pool=connection_pool)


def user_dao_factory() -> UserScorecardDao:
return UserScorecardDao(connection_pool=connection_pool)
4 changes: 2 additions & 2 deletions backend/src/cpf/core/domain/aggregates/buckets/aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def set_atomic_skill(

@AggregateRoot.produces_events
def update_advancement_level(self, advancement_level: AdvancementLevel, description: str) -> AdvancementLevelUpdate:
if advancement_level == AdvancementLevel.NO_LEVELS and self.bucket_type == BucketType.HARD_SKILL:
if advancement_level == AdvancementLevel.NO_LEVEL and self.bucket_type == BucketType.HARD_SKILL:
raise ValueError("No level advancement is not set for hard skill bucket")

return AdvancementLevelUpdate(advancement_level=advancement_level.value, description=description)
Expand Down Expand Up @@ -132,7 +132,7 @@ def _handle_bucket_created(self, event: BucketCreated) -> None:
level=skill_level,
)
else:
self.levels[AdvancementLevel.NO_LEVELS] = Advancement(level=AdvancementLevel.NO_LEVELS)
self.levels[AdvancementLevel.NO_LEVEL] = Advancement(level=AdvancementLevel.NO_LEVEL)

def handle_event(self, event: DomainEvent) -> None:
raise NotImplementedError(f"Event {event.event_type()} not handled")
Expand Down
Loading