Skip to content

Commit

Permalink
Add create and get project allocation endpoints
Browse files Browse the repository at this point in the history
The creation is a simple method but to get the allocation ready
for the fronted, they are grouped by user and weeks instead of
being returned raw as the DB model.

This is to avoid too many calculations in the frontend to display
the data in a timeline.

In this phase of the project there are still too many business rules
missing like what to do with overlapping allocations and overbooking,
but this will be done in a later stage.
  • Loading branch information
anarute committed Jan 10, 2024
1 parent 9d2ae2c commit 26718fb
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 4 deletions.
36 changes: 35 additions & 1 deletion api/routers/v1/projects.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from datetime import date
from typing import List
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session

from models.project import Project
from schemas.project import Project as ProjectSchema
from schemas.project import Project as ProjectSchema, ProjectAllocationInDb, BaseProjectAllocation
from services.projects import ProjectService

from db.db_connection import get_db
from dependencies import get_current_user
from auth.auth_bearer import BearerToken

router = APIRouter(
Expand All @@ -28,3 +30,35 @@ async def get_projects(db: Session = Depends(get_db), offset: int = 0, limit: in
@router.get("/{project_id}", response_model=ProjectSchema)
async def get_project(project_id: int, db: Session = Depends(get_db)):
return db.query(Project).filter(Project.id == project_id).first()


@router.get("/{project_id}/allocations", dependencies=[Depends(BearerToken())])
async def get_project_allocations(
project_id: int,
start: date = date.today(),
end: date = date.today(),
current_user=Depends(get_current_user),
db: Session = Depends(get_db),
):
return ProjectService(db).get_project_allocations(project_id, start, end)


@router.post(
"/{project_id}/allocations",
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(BearerToken())],
response_model=ProjectAllocationInDb,
)
async def add_project_allocations(
project_id: int,
allocation: BaseProjectAllocation,
current_user=Depends(get_current_user),
db: Session = Depends(get_db),
):
"""
Create a project allocation. Required fields are: `projecId` that comes from the url,
`userId`, `hoursPerDay`, `startDate`, and `endDate`.
"""
# TODO we need to validate for overlapping, overbooking, etc
result = ProjectService(db).create_project_allocation(allocation, current_user.username)
return result
28 changes: 27 additions & 1 deletion api/schemas/project.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import date
from pydantic import ConfigDict, BaseModel
from pydantic.alias_generators import to_camel
from typing import Optional
from typing import Optional, Dict


class Project(BaseModel):
Expand All @@ -18,4 +18,30 @@ class Project(BaseModel):
customer_id: int
customer_name: Optional[str] = None
area_id: int

model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)


class BaseProjectAllocation(BaseModel):
user_id: int
project_id: int
start_date: Optional[date] = None
end_date: Optional[date] = None
hours_per_day: Optional[float] = None
fte: Optional[float] = None
is_tentative: Optional[bool] = None
is_billable: Optional[bool] = None
notes: Optional[str] = None

model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)


class ProjectAllocationInDb(BaseProjectAllocation):
id: int
total_hours: float
username: str


class ProjectAllocationPerUser(BaseModel):
username: str
hours: Dict
67 changes: 65 additions & 2 deletions api/services/projects.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
from typing import List

from datetime import date, datetime, timedelta
from services.main import AppService
from models.project import Project
from models.project import Project, ProjectAllocation
from schemas.project import BaseProjectAllocation, ProjectAllocationPerUser


def group_allocations_per_user(allocations):
allocations_grouped_per_user = {}

for allocation in allocations:
username = allocation.username
current_date = allocation.start_date
while current_date <= allocation.end_date:
iso_week = current_date.isocalendar()[1]
month_name = current_date.strftime("%b")
day_of_week = current_date.weekday()
days_until_monday = (day_of_week - 0) % 7
first_day_of_week = current_date - timedelta(days=days_until_monday)
week_key = f"{first_day_of_week.year}-{iso_week}"
if username not in allocations_grouped_per_user:
allocations_grouped_per_user[username] = {"username": username, "hours": {}}
if week_key not in allocations_grouped_per_user[username]["hours"]:
allocations_grouped_per_user[username]["hours"][week_key] = {
"days": [],
"ISOWeek": iso_week,
"month": month_name,
"totalHours": 0,
"project": allocation.project_id,
"isLeave": False, # TODO Replace with the logic to determine if it's leave
}
if day_of_week < 5:
allocations_grouped_per_user[username]["hours"][week_key]["totalHours"] += allocation.hours_per_day
allocations_grouped_per_user[username]["hours"][week_key]["days"].append(str(current_date))

current_date += timedelta(days=1)
return list(allocations_grouped_per_user.values())


class ProjectService(AppService):
Expand All @@ -18,3 +51,33 @@ def is_project_active(self, project_id) -> bool:
if project is not None:
return project.is_active
return False

def get_project_allocations(
self, project_id: int, start: date = None, end: date = None
) -> List[ProjectAllocationPerUser]:
# TODO improve the date filter logic to get more precise results
query = self.db.query(ProjectAllocation).where(
ProjectAllocation.project_id == project_id, ProjectAllocation.start_date.between(start, end)
)
allocations = query.all() or []

return group_allocations_per_user(allocations)

def create_project_allocation(self, allocation: BaseProjectAllocation, created_by: str) -> ProjectAllocation:
new_allocation = ProjectAllocation(
user_id=allocation.user_id,
project_id=allocation.project_id,
start_date=allocation.start_date,
end_date=allocation.end_date,
hours_per_day=allocation.hours_per_day,
fte=allocation.fte,
is_tentative=allocation.is_tentative,
is_billable=allocation.is_billable,
notes=allocation.notes,
created_at=datetime.now(),
created_by=created_by,
)
self.db.add(new_allocation)
self.db.commit()
self.db.refresh(new_allocation)
return new_allocation

0 comments on commit 26718fb

Please sign in to comment.