From dc47eac6ea9551511fd535d6ff3b71e4306031ab Mon Sep 17 00:00:00 2001 From: sujanadh Date: Wed, 27 Dec 2023 17:36:48 +0545 Subject: [PATCH 1/4] feat: separate task history end point --- src/backend/app/tasks/tasks_crud.py | 25 ++++++++++++++++++++----- src/backend/app/tasks/tasks_routes.py | 20 +++++++++++++++++++- src/backend/app/tasks/tasks_schemas.py | 21 +++++++++++++++++++++ 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/backend/app/tasks/tasks_crud.py b/src/backend/app/tasks/tasks_crud.py index 654a0d33f0..cf206e1ca1 100644 --- a/src/backend/app/tasks/tasks_crud.py +++ b/src/backend/app/tasks/tasks_crud.py @@ -341,7 +341,7 @@ def process_history_entry(history_entry): return tasks -def get_task_history( +def get_project_task_history( project_id: int, end_date: Optional[datetime], db: Session, @@ -349,10 +349,10 @@ def get_task_history( """Retrieves the task history records for a specific project. Args: - project_id: The ID of the project. - end_date: The end date of the task history - records to retrieve (optional). - db: The database session. + project_id (int): The ID of the project. + end_date (datetime, optional): The end date of the task history + records to retrieve. + db (Session): The database session. Returns: A list of task history records for the specified project. @@ -413,3 +413,18 @@ async def count_validated_and_mapped_tasks( entry.update({"validated": total_validated, "mapped": total_mapped}) return results + + +async def append(tasks:List, db: Session): + """Get task history of project.""" + response = [] + + for task in tasks if isinstance(tasks, list) else [tasks]: + task_id = task.id + task_history = task.task_history + if isinstance(task_history, list): + for history_entry in task_history: + user = db.query(db_models.DbUser).filter_by(id=history_entry.user_id).first() + response.append(tasks_schemas.TaskHistory.map_entry_to_model(task_id, history_entry, user)) + + return response diff --git a/src/backend/app/tasks/tasks_routes.py b/src/backend/app/tasks/tasks_routes.py index 4c239a8cb9..bca61937da 100644 --- a/src/backend/app/tasks/tasks_routes.py +++ b/src/backend/app/tasks/tasks_routes.py @@ -216,9 +216,27 @@ async def task_activity( """ end_date = datetime.now() - timedelta(days=days) - task_history = tasks_crud.get_task_history(project_id, end_date, db) + task_history = tasks_crud.get_project_task_history(project_id, end_date, db) return await tasks_crud.count_validated_and_mapped_tasks( task_history, end_date, ) + + +@router.get("/task_history/", response_model = List[tasks_schemas.TaskHistory]) +async def task_history(project_id: int, days: int = 10, db: Session = Depends(database.get_db)): + """ + Get the detailed task history for a project. + + Args: + project_id (int): The ID of the project. + db (Session): The database session. (default: Depends(database.get_db)) + + Returns: + List[TaskHistory]: A list of task history. + """ + end_date = datetime.now() - timedelta(days=days) + task_history = tasks_crud.get_project_task_history(project_id, end_date, db) + + return await tasks_crud.get_task_history(tasks, db) diff --git a/src/backend/app/tasks/tasks_schemas.py b/src/backend/app/tasks/tasks_schemas.py index ace91f1857..5dbb2f4e69 100644 --- a/src/backend/app/tasks/tasks_schemas.py +++ b/src/backend/app/tasks/tasks_schemas.py @@ -135,3 +135,24 @@ class ReadTask(Task): """Task details plus updated task history.""" task_history: Optional[List[TaskHistoryOut]] = None + + +class TaskHistory(BaseModel): + """Task history details.""" + task_id: int + action_text: str + action_date: datetime + status: str + username: str + profile_img: Optional[str] + + @classmethod + def map_entry_to_model(cls, task_id, history_entry, user): + return cls( + task_id=task_id, + action_text=history_entry.action_text, + action_date=history_entry.action_date, + status=history_entry.action_text.split()[5], + username=user.username if user else None, + profile_img=user.profile_img if user else None + ) From 12c0f543279194c5eddf2dc2a625819237ffde68 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 6 Feb 2024 16:10:00 +0000 Subject: [PATCH 2/4] fix: add user relationship backref to task history --- src/backend/app/db/db_models.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py index 5c5b648891..097aa03c44 100644 --- a/src/backend/app/db/db_models.py +++ b/src/backend/app/db/db_models.py @@ -367,6 +367,7 @@ class DbTaskHistory(Base): ) # Define relationships + user = relationship(DbUser, uselist=False, backref="task_history_user") invalidation_history = relationship( DbTaskInvalidationHistory, lazy="dynamic", cascade="all" ) @@ -461,9 +462,15 @@ class DbProject(Base): server_default="20386219", ), ) - author = relationship(DbUser, uselist=False, backref="user") + author = relationship(DbUser, uselist=False, backref="project_user") created = cast(datetime, Column(DateTime, default=timestamp, nullable=False)) + task_split_type = Column(Enum(TaskSplitType), nullable=True) + # split_strategy = Column(Integer) + # grid_meters = Column(Integer) + # task_type = Column(Integer) + # target_number_of_features = Column(Integer) + # PROJECT DETAILS project_name_prefix = cast(str, Column(String)) task_type_prefix = cast(str, Column(String)) @@ -471,7 +478,7 @@ class DbProject(Base): DbProjectInfo, cascade="all, delete, delete-orphan", uselist=False, - backref="project", + backref="project_info", ) location_str = cast(str, Column(String)) From 8dac0760b57e61fffd9ab581582733b89b32e191 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 6 Feb 2024 16:11:39 +0000 Subject: [PATCH 3/4] fix: add user details to task_history endpoint --- src/backend/app/tasks/tasks_crud.py | 17 +------- src/backend/app/tasks/tasks_routes.py | 27 ++++++------ src/backend/app/tasks/tasks_schemas.py | 60 ++++++++++++++++++-------- 3 files changed, 57 insertions(+), 47 deletions(-) diff --git a/src/backend/app/tasks/tasks_crud.py b/src/backend/app/tasks/tasks_crud.py index cf206e1ca1..a442b70eb3 100644 --- a/src/backend/app/tasks/tasks_crud.py +++ b/src/backend/app/tasks/tasks_crud.py @@ -341,7 +341,7 @@ def process_history_entry(history_entry): return tasks -def get_project_task_history( +async def get_project_task_history( project_id: int, end_date: Optional[datetime], db: Session, @@ -413,18 +413,3 @@ async def count_validated_and_mapped_tasks( entry.update({"validated": total_validated, "mapped": total_mapped}) return results - - -async def append(tasks:List, db: Session): - """Get task history of project.""" - response = [] - - for task in tasks if isinstance(tasks, list) else [tasks]: - task_id = task.id - task_history = task.task_history - if isinstance(task_history, list): - for history_entry in task_history: - user = db.query(db_models.DbUser).filter_by(id=history_entry.user_id).first() - response.append(tasks_schemas.TaskHistory.map_entry_to_model(task_id, history_entry, user)) - - return response diff --git a/src/backend/app/tasks/tasks_routes.py b/src/backend/app/tasks/tasks_routes.py index bca61937da..b174049b08 100644 --- a/src/backend/app/tasks/tasks_routes.py +++ b/src/backend/app/tasks/tasks_routes.py @@ -206,17 +206,17 @@ async def task_activity( """Retrieves the validate and mapped task count for a specific project. Args: - project_id: The ID of the project. - days: The number of days to consider for the - task activity (default: 10). - db: The database session. + project_id (int): The ID of the project. + days (int): The number of days to consider for the + task activity (default: 10). + db (Session): The database session. Returns: list[TaskHistoryCount]: A list of task history counts. """ end_date = datetime.now() - timedelta(days=days) - task_history = tasks_crud.get_project_task_history(project_id, end_date, db) + task_history = await tasks_crud.get_project_task_history(project_id, end_date, db) return await tasks_crud.count_validated_and_mapped_tasks( task_history, @@ -224,19 +224,20 @@ async def task_activity( ) -@router.get("/task_history/", response_model = List[tasks_schemas.TaskHistory]) -async def task_history(project_id: int, days: int = 10, db: Session = Depends(database.get_db)): - """ - Get the detailed task history for a project. +@router.get("/task_history/", response_model=List[tasks_schemas.TaskHistory]) +async def task_history( + project_id: int, days: int = 10, db: Session = Depends(database.get_db) +): + """Get the detailed task history for a project. Args: project_id (int): The ID of the project. - db (Session): The database session. (default: Depends(database.get_db)) + days (int): The number of days to consider for the + task activity (default: 10). + db (Session): The database session. Returns: List[TaskHistory]: A list of task history. """ end_date = datetime.now() - timedelta(days=days) - task_history = tasks_crud.get_project_task_history(project_id, end_date, db) - - return await tasks_crud.get_task_history(tasks, db) + return await tasks_crud.get_project_task_history(project_id, end_date, db) diff --git a/src/backend/app/tasks/tasks_schemas.py b/src/backend/app/tasks/tasks_schemas.py index 5dbb2f4e69..175a9ebded 100644 --- a/src/backend/app/tasks/tasks_schemas.py +++ b/src/backend/app/tasks/tasks_schemas.py @@ -22,7 +22,7 @@ from geojson_pydantic import Feature as GeojsonFeature from loguru import logger as log -from pydantic import BaseModel, ConfigDict, Field, ValidationInfo +from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, computed_field from pydantic.functional_serializers import field_serializer from pydantic.functional_validators import field_validator @@ -96,7 +96,9 @@ def get_geojson_from_outline(cls, value: Any, info: ValidationInfo) -> str: @field_validator("outline_centroid", mode="before") @classmethod - def get_centroid_from_outline(cls, value: Any, info: ValidationInfo) -> str: + def get_centroid_from_outline( + cls, value: Any, info: ValidationInfo + ) -> Optional[str]: """Get outline_centroid from Shapely geom.""" if outline := info.data.get("outline"): properties = { @@ -109,21 +111,21 @@ def get_centroid_from_outline(cls, value: Any, info: ValidationInfo) -> str: return None @field_serializer("locked_by_uid") - def get_locked_by_uid(self, value: str) -> str: + def get_locked_by_uid(self, value: str) -> Optional[str]: """Get lock uid from lock_holder details.""" if self.lock_holder: return self.lock_holder.id return None @field_serializer("locked_by_username") - def get_locked_by_username(self, value: str) -> str: + def get_locked_by_username(self, value: str) -> Optional[str]: """Get lock username from lock_holder details.""" if self.lock_holder: return self.lock_holder.username return None @field_serializer("odk_token") - def decrypt_password(self, value: str) -> str: + def decrypt_password(self, value: str) -> Optional[str]: """Decrypt the ODK Token extracted from the db.""" if not value: return "" @@ -139,20 +141,42 @@ class ReadTask(Task): class TaskHistory(BaseModel): """Task history details.""" + + model_config = ConfigDict( + from_attributes=True, + ) + + # Excluded + user: Any = Field(exclude=True) + task_id: int action_text: str action_date: datetime - status: str - username: str - profile_img: Optional[str] - @classmethod - def map_entry_to_model(cls, task_id, history_entry, user): - return cls( - task_id=task_id, - action_text=history_entry.action_text, - action_date=history_entry.action_date, - status=history_entry.action_text.split()[5], - username=user.username if user else None, - profile_img=user.profile_img if user else None - ) + @computed_field + @property + def username(self) -> Optional[str]: + """Get username from user db obj.""" + if self.user: + return self.user.username + return None + + @computed_field + @property + def profile_img(self) -> Optional[str]: + """Get profile_img from user db obj.""" + if self.user: + return self.user.profile_img + return None + + @computed_field + @property + def status(self) -> Optional[str]: + """Extract status from standard format action_text.""" + if self.action_text: + split_text = self.action_text.split() + if len(split_text) > 5: + return split_text[5] + else: + return self.action_text + return None From 5f6fd0d17ef65164f38694bc961f84e456ee602a Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 6 Feb 2024 16:21:57 +0000 Subject: [PATCH 4/4] refactor: revert to sqlalchemy backref names --- src/backend/app/db/db_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py index 097aa03c44..e4f22d9bd3 100644 --- a/src/backend/app/db/db_models.py +++ b/src/backend/app/db/db_models.py @@ -462,7 +462,7 @@ class DbProject(Base): server_default="20386219", ), ) - author = relationship(DbUser, uselist=False, backref="project_user") + author = relationship(DbUser, uselist=False, backref="user") created = cast(datetime, Column(DateTime, default=timestamp, nullable=False)) task_split_type = Column(Enum(TaskSplitType), nullable=True) @@ -478,7 +478,7 @@ class DbProject(Base): DbProjectInfo, cascade="all, delete, delete-orphan", uselist=False, - backref="project_info", + backref="project", ) location_str = cast(str, Column(String))