From 2564b6ba4487ea628f62eff4f1b7202f2756a6eb Mon Sep 17 00:00:00 2001 From: Danielle Mayabb Date: Tue, 10 Oct 2023 11:07:53 -0700 Subject: [PATCH] Add PUT endpoint for tasks - 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. --- api/routers/v1/timelog.py | 120 ++++++++++++++++++++++++++++++-------- api/schemas/timelog.py | 10 ++++ api/services/timelog.py | 17 +++++- 3 files changed, 122 insertions(+), 25 deletions(-) diff --git a/api/routers/v1/timelog.py b/api/routers/v1/timelog.py index f34394fcd..53fbe6d99 100644 --- a/api/routers/v1/timelog.py +++ b/api/routers/v1/timelog.py @@ -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 @@ -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", @@ -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) @@ -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. """ @@ -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. @@ -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 diff --git a/api/schemas/timelog.py b/api/schemas/timelog.py index 9c68ccbc6..2f55df19a 100644 --- a/api/schemas/timelog.py +++ b/api/schemas/timelog.py @@ -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) diff --git a/api/services/timelog.py b/api/services/timelog.py index 693a4f753..8fa909dfd 100644 --- a/api/services/timelog.py +++ b/api/services/timelog.py @@ -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 @@ -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, @@ -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 []