Skip to content

Commit

Permalink
Add PUT endpoint for tasks
Browse files Browse the repository at this point in the history
- Add the endpoint
- Add TemplateUpdate schema
- Add a simple get method to tasks service so we can check for existing task
- Small updates to endpoint docs to provide more clarity
- Update validation.

The validation updates for update were a bit extensive. Since it should be possible for someone to send a PUT request with only the task_id and the properties they want to update, we needed several additional checks to ensure the validation would work with a partial object.
  • Loading branch information
dmtrek14 committed Oct 10, 2023
1 parent 3150753 commit 2564b6b
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 25 deletions.
120 changes: 96 additions & 24 deletions api/routers/v1/timelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
TemplateUpdate as TemplateUpdateSchema,
Task as TaskSchema,
TaskNew as TaskNewSchema,
TaskUpdate as TaskUpdateSchema,
)
from schemas.validation import ValidatedObject
from services.timelog import TaskTypeService, TemplateService, TaskService
Expand All @@ -18,6 +19,7 @@
from db.db_connection import get_db
from auth.auth_bearer import BearerToken
from dependencies import get_current_user, AppUser
from helpers.time import time_string_to_int

router = APIRouter(
prefix="/timelog",
Expand Down Expand Up @@ -54,8 +56,8 @@ async def add_template(
- **story**: the task story
- **description**: the task description
- **task type**: the task type
- **start time**: the task start time
- **end time**: the task end time
- **start time**: the task start time in 24h time notation (HH:mm)
- **end time**: the task end time in 24h time notation (HH:mm)
- **user id**: the user id (global templates should leave this null; user template should fill)
- **project id**: the project id
- **is global***: whether or not this template is global for all users (required)
Expand Down Expand Up @@ -86,8 +88,8 @@ async def update_template(
- **user id**: the user id (global templates should leave this null; user template should fill)
- **project id**: the project id
- **is global**: whether or not this template is global for all users (required)
- **start time**: the task start time
- **end time**: the task end time
- **start time**: the task start time in 24h time notation (HH:mm)
- **end time**: the task end time in 24h time notation (HH:mm)
\f
:param item: User input.
"""
Expand Down Expand Up @@ -127,9 +129,9 @@ async def add_task(task: TaskNewSchema, current_user=Depends(get_current_user),
- **story**: the task story
- **description**: the task description
- **task type**: the task type
- **date**: the task date
- **start time**: the task start time
- **end time**: the task end time
- **date**: the task date (YYYY-MM-DD)
- **start time**: the task start time in 24h time notation (HH:mm)
- **end time**: the task end time in 24h time notation (HH:mm)
\f
:param item: User input.
Expand All @@ -143,25 +145,95 @@ async def add_task(task: TaskNewSchema, current_user=Depends(get_current_user),
return result


def validate_task(task: TaskSchema, db: Session):
@router.put("/tasks/{task_id}", response_model=TaskSchema)
async def update_task(
task_id: int, task: TaskUpdateSchema, current_user=Depends(get_current_user), db: Session = Depends(get_db)
):
"""
Update an task with any of the following data:
- **user id**: the user id
- **project_id**: the project the task is associated with
- **story**: the task story
- **description**: the task description
- **task type**: the task type
- **date**: the task date (YYYY-MM-DD)
- **start time**: the task start time in 24h time notation (HH:mm)
- **end time**: the task end time in 24h time notation (HH:mm)
\f
:param item: User input.
"""
existing_task = TaskService(db).get_task(task_id)
if not existing_task:
raise HTTPException(status_code=404, detail=f"Task with id {task_id} not found")
user_id_to_check = task.user_id if task.user_id else existing_task.user_id
if current_user.id != user_id_to_check:
raise HTTPException(status_code=403, detail="You are not authorized to update tasks for this user.")
if not task.user_id:
task.user_id = user_id_to_check
validated_task = validate_task(task, db, existing_task)
if not validated_task.is_valid:
raise HTTPException(status_code=422, detail=validated_task.message)
result = TaskService(db).update_task(existing_task, task)
return result


def validate_task(task_to_validate: TaskSchema, db: Session, existing_task=None):
validated = ValidatedObject(is_valid=False, message="")
user_can_create_tasks = ConfigService(db).can_user_edit_task(task.date)
if not user_can_create_tasks:
validated.message += "You cannot create or edit a task for this date - it is outside the allowed range."
return validated
if task.task_type:
task_type_valid = TaskTypeService(db).slug_is_valid(task.task_type)
if not task_type_valid:
validated.message += f"Task type {task.task_type} does not exist."

if existing_task is not None:
if task_to_validate.start_time:
task_to_validate.init = time_string_to_int(task_to_validate.start_time)
else:
task_to_validate.init = existing_task.init
if task_to_validate.end_time:
task_to_validate.end = time_string_to_int(task_to_validate.end_time)
else:
task_to_validate.end = existing_task.end

dates_differ = False
if existing_task is not None and task_to_validate.date and task_to_validate.date != existing_task.date:
dates_differ = True

times_differ = False
if (
existing_task is not None
and (task_to_validate.init or task_to_validate.end)
and (existing_task.init != task_to_validate.init or existing_task.end != task_to_validate.end)
):
times_differ = True

if existing_task is not None and not dates_differ:
task_to_validate.date = existing_task.date

if existing_task is None or (
existing_task is not None and task_to_validate.date and existing_task.date != task_to_validate.date
):
user_can_create_tasks = ConfigService(db).can_user_edit_task(task_to_validate.date)
if not user_can_create_tasks:
validated.message += "You cannot create or edit a task for this date - it is outside the allowed range."
return validated
if existing_task is None or (task_to_validate.task_type and existing_task.task_type != task_to_validate.task_type):
if task_to_validate.task_type:
task_type_valid = TaskTypeService(db).slug_is_valid(task_to_validate.task_type)
if not task_type_valid:
validated.message += f"Task type {task_to_validate.task_type} does not exist."
return validated
if existing_task is None or (
existing_task is not None
and task_to_validate.project_id
and existing_task.project_id != task_to_validate.project_id
):
project_active = ProjectService(db).is_project_active(task_to_validate.project_id)
if not project_active:
validated.message += "You cannot add a task for an inactive project."
return validated
if existing_task is None or (dates_differ or times_differ):
overlapping_tasks = TaskService(db).check_task_for_overlap(task_to_validate)
if not overlapping_tasks.is_valid:
validated.message += overlapping_tasks.message
return validated
project_active = ProjectService(db).is_project_active(task.project_id)
if not project_active:
validated.message += "You cannot add a task for an inactive project."
return validated
overlapping_tasks = TaskService(db).check_task_for_overlap(task)
if not overlapping_tasks.is_valid:
validated.message += overlapping_tasks.message
return validated
validated.is_valid = True
return validated

Expand Down
10 changes: 10 additions & 0 deletions api/schemas/timelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,21 @@ def end(self) -> int:
return time_string_to_int(self.end_time)


class TaskUpdate(TaskBase):
date: Optional[date] = None
start_time: Optional[Annotated[str, StringConstraints(pattern=r"^[0-9]{2}:[0-9]{2}")]] = None
end_time: Optional[Annotated[str, StringConstraints(pattern=r"^[0-9]{2}:[0-9]{2}")]] = None
project_name: Optional[str] = None
init: Optional[int] = None
end: Optional[int] = None


# Properties shared by models stored in db
class TaskInDb(TaskBase):
id: int
init: int
end: int
date: date
model_config = ConfigDict(from_attributes=True)


Expand Down
17 changes: 16 additions & 1 deletion api/services/timelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from services.main import AppService
from models.timelog import TaskType, Template, Task
from schemas.timelog import TemplateNew, TemplateUpdate, TaskNew
from schemas.timelog import TemplateNew, TemplateUpdate, TaskNew, TaskUpdate
from schemas.validation import ValidatedObject


Expand Down Expand Up @@ -69,6 +69,10 @@ def get_user_tasks(self, user_id: int, offset: int, limit: int, start: date, end
)
return tasks

def get_task(self, task_id: int) -> Task:
task = self.db.query(Task).where(Task.id == task_id).first() or None
return task

def create_task(self, task: TaskNew) -> Task:
new_task = Task(
date=task.date,
Expand All @@ -86,6 +90,17 @@ def create_task(self, task: TaskNew) -> Task:
self.db.refresh(new_task)
return new_task

def update_task(self, existing_task: Task, task_updates: TaskUpdate) -> Task:
existing_data = jsonable_encoder(existing_task)
update_data = task_updates.dict(exclude_unset=True)
for field in existing_data:
if field in update_data:
setattr(existing_task, field, update_data[field])
self.db.add(existing_task)
self.db.commit()
self.db.refresh(existing_task)
return existing_task

def check_task_for_overlap(self, task: Task) -> ValidatedObject:
validated_task = ValidatedObject(is_valid=True, message="")
user_tasks_for_day = self.db.query(Task).where(Task.user_id == task.user_id, Task.date == task.date).all() or []
Expand Down

0 comments on commit 2564b6b

Please sign in to comment.