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
29 changes: 18 additions & 11 deletions api/api_models/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,28 @@ class CreateProject(ProjectBase):
project_tools: Optional[List[int]] = Field(None)


class MemberWithTeamResponse(BaseModel):
id: int
first_name: str = Field(...)
last_name: str = Field(...)
username: str = Field(...)
email: str = Field(...)
profile_pic_url: Optional[str] = None
team: Optional[str] = Field(None)

model_config = ConfigDict(from_attributes=True)


# Alias for backward compatibility
MembersResponse = MemberWithTeamResponse


class ProjectResponse(ProjectBase):
id: int
created_at: datetime
updated_at: datetime
members: Optional[list[UserResponse]] = Field(None)
manager: Optional[UserResponse] = Field(None)
members: Optional[list[MemberWithTeamResponse]] = Field(None)
stacks: Optional[list[Stacks]] = Field(None)
project_tools: Optional[list[Skills]] = Field(None)
status: ProjectStatus
Expand All @@ -44,13 +61,3 @@ class UpdateProject(BaseModel):

class ProjectMember(BaseModel):
team: ProjectTeam = Field(...)


class MembersResponse(BaseModel):
id: int
first_name: str = Field(...)
last_name: str = Field(...)
username: str = Field(...)
email: str = Field(...)
profile_pic_url: Optional[str] = None
stack: Optional[Stacks] = Field(None)
35 changes: 32 additions & 3 deletions api/api_models/technical_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,44 @@ class TechnicalTaskResponse(TechnicalTaskBase):


class TechnicalTaskSubmissionBase(BaseModel):
github_link: Text
live_demo_url: Optional[Text]
description: Optional[Text]
github_link: Text = Field(...)
live_demo_url: Optional[Text] = None
description: Optional[Text] = None


class UserMinimal(BaseModel):
id: int
first_name: str
last_name: str
username: str
profile_pic_url: Optional[str] = None

model_config = ConfigDict(from_attributes=True)


class StackMinimal(BaseModel):
id: int
name: str

model_config = ConfigDict(from_attributes=True)


class TechnicalTaskMinimal(BaseModel):
id: int
content: str
experience_level: ExperienceLevel
stack: Optional[StackMinimal] = None

model_config = ConfigDict(from_attributes=True)


class TechnicalTaskSubmissionResponse(TechnicalTaskSubmissionBase):
id: int
created_at: datetime
updated_at: datetime
task_id: int
user_id: Optional[int] = None
user: Optional[UserMinimal] = None
technical_task: Optional[TechnicalTaskMinimal] = None

model_config = ConfigDict(from_attributes=True)
3 changes: 3 additions & 0 deletions api/api_models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ class OrgChartNode(BaseModel):
role: Optional[Role] = None
stack: Optional[Stacks] = None
manager_id: Optional[int] = None
status: str # Added for frontend filtering
is_active: bool # Added for frontend filtering
subordinates: List[OrgChartNode] = []

model_config = ConfigDict(from_attributes=True)
Expand Down Expand Up @@ -278,6 +280,7 @@ class Token(BaseModel):
is_active: bool = Field(...)
user_status: str = Field(...)
refresh_token: str = Field(...)
role: Optional[Role] = Field(None)


class TokenData(BaseModel):
Expand Down
7 changes: 6 additions & 1 deletion api/routes/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ def get(project_id: int, db: Session = Depends(get_db)):

@project_router.get("/", status_code=status.HTTP_200_OK, response_model=Page[ProjectResponse])
def get_all(db: Session = Depends(get_db)):
return paginate(db, _service(db).get_all_query())
service = _service(db)
page = paginate(db, service.get_all_query())
# Enrich members with team data
for project in page.items:
service._enrich_project_members_with_team(project)
return page


@project_router.post("/{project_id}/add/{user_id}", status_code=status.HTTP_201_CREATED)
Expand Down
38 changes: 25 additions & 13 deletions db/repository/org_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ class OrgChartRepository(BaseRepository):
def get_user(self, user_id: int) -> Optional[User]:
return self.db.query(User).filter(User.id == user_id).first()

def get_direct_subordinates(self, user_id: int) -> list[User]:
return (
self.db.query(User)
.filter(User.manager_id == user_id)
.all()
)
def get_direct_subordinates(self, user_id: int, filter_active: bool = False) -> list[User]:
"""Get direct subordinates of a user.

Args:
user_id: The manager's user ID
filter_active: If True, only return ACCEPTED + is_active users
"""
query = self.db.query(User).filter(User.manager_id == user_id)

if filter_active:
query = query.filter(User.status == "ACCEPTED", User.is_active == True)

return query.all()

def get_subtree_ids(self, root_id: int, max_depth: int = 5) -> list[dict]:
"""Fetch all descendant user IDs using a recursive CTE, up to max_depth.
Expand Down Expand Up @@ -58,13 +65,18 @@ def get_users_by_ids(self, user_ids: list[int]) -> list[User]:
return []
return self.db.query(User).filter(User.id.in_(user_ids)).all()

def get_root_users(self) -> list[User]:
"""Return users with no manager (org tree roots)."""
return (
self.db.query(User)
.filter(User.manager_id.is_(None))
.all()
)
def get_root_users(self, filter_active: bool = False) -> list[User]:
"""Return users with no manager (org tree roots).

Args:
filter_active: If True, only return ACCEPTED + is_active users
"""
query = self.db.query(User).filter(User.manager_id.is_(None))

if filter_active:
query = query.filter(User.status == "ACCEPTED", User.is_active == True)

return query.all()

def get_ancestor_ids(self, user_id: int, max_depth: int = 50) -> list[int]:
"""Walk the manager chain upward using a recursive CTE.
Expand Down
6 changes: 6 additions & 0 deletions db/repository/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ def update(self, project: Project, update_data: dict) -> Project:

def delete_with_memberships(self, project_id: int) -> None:
self.db.query(UserProject).filter(UserProject.project_id == project_id).delete()
# Delete project_stacks entries
from db.models.project_stacks import ProjectStack
self.db.query(ProjectStack).filter(ProjectStack.project_id == project_id).delete()
# Delete project_skills entries
from db.models.project_skills import ProjectSkill
self.db.query(ProjectSkill).filter(ProjectSkill.project_id == project_id).delete()
self.db.query(Project).filter(Project.id == project_id).delete()
self.db.commit()

Expand Down
24 changes: 23 additions & 1 deletion db/repository/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,18 @@ def get_by_username(self, username: str) -> Optional[User]:
return self.db.query(User).filter(User.username == username).first()

def create(self, user_data: UserSignUp) -> User:
from db.models.roles import Role
from utils.utils import RoleChoices

data = user_data.model_dump().copy()
data.pop("password_confirmation")

# Ensure role_id is set - default to USER role if not provided
if not data.get("role_id"):
user_role = self.db.query(Role).filter(Role.name == RoleChoices.USER).first()
if user_role:
data["role_id"] = user_role.id

new_user = User(**data)
return self.save(new_user)

Expand Down Expand Up @@ -56,6 +66,8 @@ def update_avatar(self, user: User, url: str) -> User:

def build_search_query(self, skill: Optional[str], stack: Optional[str],
active: Optional[bool], p: Optional[str]):
from sqlalchemy import or_

query = select(User).order_by(desc(User.created_at))
if skill:
query = query.join(users_skills.UserSkill).join(Skill).filter(
Expand All @@ -64,7 +76,17 @@ def build_search_query(self, skill: Optional[str], stack: Optional[str],
if stack:
query = query.filter(User.stack.has(name=stack.capitalize()))
if active is not None:
query = query.filter(User.is_active == active)
if active: # active=True means Directory
# Only show ACCEPTED + is_active users
query = query.filter(User.is_active.is_(True), User.status == "ACCEPTED")
else: # active=False means Applicants
# Show: (is_active=false any status) OR (is_active=true but status != ACCEPTED)
query = query.filter(
or_(
User.is_active.is_(False),
User.is_active.is_(True) & (User.status != "ACCEPTED")
)
)
if p:
p_escaped = p.replace("%", r"\%").replace("_", r"\_")
query = query.filter(
Expand Down
Loading
Loading