diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py index d6346be80b..bd5ef60383 100644 --- a/src/backend/app/db/db_models.py +++ b/src/backend/app/db/db_models.py @@ -384,6 +384,32 @@ class DbTaskHistory(Base): ) +class TaskComment(Base): + """Represents a comment associated with a task.""" + + __tablename__ = "task_comment" + + id = Column(Integer, primary_key=True) + task_id = Column(Integer, nullable=False) + project_id = Column(Integer, ForeignKey("projects.id"), index=True) + comment_text = Column(String) + commented_by = Column( + BigInteger, + ForeignKey("users.id", name="fk_users"), + index=True, + nullable=False, + ) + created_at = Column(DateTime, nullable=False, default=timestamp) + + __table_args__ = ( + ForeignKeyConstraint( + [task_id, project_id], ["tasks.id", "tasks.project_id"], name="fk_tasks" + ), + Index("idx_task_comment_composite", "task_id", "project_id"), + {}, + ) + + class DbTask(Base): """Describes an individual mapping Task.""" diff --git a/src/backend/app/db/postgis_utils.py b/src/backend/app/db/postgis_utils.py index 9a7a5b90a2..0a77c88b78 100644 --- a/src/backend/app/db/postgis_utils.py +++ b/src/backend/app/db/postgis_utils.py @@ -246,7 +246,6 @@ def parse_and_filter_geojson( geojson_str: str, filter: bool = True ) -> Optional[geojson.FeatureCollection]: """Parse geojson string and filter out incomaptible geometries.""" - log.debug("Parsing geojson string") geojson_parsed = geojson.loads(geojson_str) if isinstance(geojson_parsed, geojson.FeatureCollection): log.debug("Already in FeatureCollection format, skipping reparse") @@ -255,7 +254,7 @@ def parse_and_filter_geojson( log.debug("Converting Feature to FeatureCollection") featcol = geojson.FeatureCollection(features=[geojson_parsed]) else: - log.debug("Converting geometry to FeatureCollection") + log.debug("Converting Geometry to FeatureCollection") featcol = geojson.FeatureCollection( features=[geojson.Feature(geometry=geojson_parsed)] ) diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_crud.py index 0866bc9cc3..2211d22e05 100644 --- a/src/backend/app/projects/project_crud.py +++ b/src/backend/app/projects/project_crud.py @@ -594,7 +594,6 @@ def remove_z_dimension(coord): meters=meters, ) for index, poly in enumerate(tasks["features"]): - log.debug(poly) db_task = db_models.DbTask( project_id=project_id, outline=wkblib.dumps(shape(poly["geometry"]), hex=True), @@ -607,8 +606,6 @@ def remove_z_dimension(coord): ) db.add(db_task) db.commit() - - # FIXME: write to tasks table return True diff --git a/src/backend/app/tasks/tasks_crud.py b/src/backend/app/tasks/tasks_crud.py index a442b70eb3..6290346271 100644 --- a/src/backend/app/tasks/tasks_crud.py +++ b/src/backend/app/tasks/tasks_crud.py @@ -29,6 +29,7 @@ from sqlalchemy.orm import Session from sqlalchemy.sql import text +from app.auth.osm import AuthUser from app.central import central_crud from app.db import database, db_models from app.models.enums import ( @@ -315,6 +316,103 @@ async def edit_task_boundary(db: Session, task_id: int, boundary: str): return True +async def get_task_comments(db: Session, project_id: int, task_id: int): + """Get a list of tasks id for a project.""" + query = text( + """ + SELECT + task_history.id, task_history.task_id, users.username, + task_history.action_text, task_history.action_date + FROM + task_history + LEFT JOIN + users ON task_history.user_id = users.id + WHERE + project_id = :project_id + AND task_id = :task_id + AND action = 'COMMENT' + """ + ) + + params = {"project_id": project_id, "task_id": task_id} + + result = db.execute(query, params) + + # Convert the result to a list of dictionaries + result_dict_list = [ + { + "id": row[0], + "task_id": row[1], + "commented_by": row[2], + "comment": row[3], + "created_at": row[4], + } + for row in result.fetchall() + ] + + return result_dict_list + + +async def add_task_comments( + db: Session, comment: tasks_schemas.TaskCommentBase, user_data: AuthUser +): + """Add a comment to a task. + + Parameters: + - db: SQLAlchemy database session + - comment: TaskCommentBase instance containing the comment details + - user_data: AuthUser instance containing the user details + + Returns: + - Dictionary with the details of the added comment + """ + currentdate = datetime.now() + # Construct the query to insert the comment and retrieve inserted comment details + query = text( + """ + INSERT INTO task_history ( + project_id, task_id, action, action_text, + action_date, user_id + ) + VALUES ( + :project_id, :task_id, 'COMMENT', :comment_text, + :current_date, :user_id + ) + RETURNING + task_history.id, + task_history.task_id, + (SELECT username FROM users WHERE id = task_history.user_id) AS user_id, + task_history.action_text, + task_history.action_date; + """ + ) + + # Define a dictionary with the parameter values + params = { + "project_id": comment.project_id, + "task_id": comment.task_id, + "comment_text": comment.comment, + "current_date": currentdate, + "user_id": user_data.id, + } + + # Execute the query with the named parameters and commit the transaction + result = db.execute(query, params) + db.commit() + + # Fetch the first row of the query result + row = result.fetchone() + + # Return the details of the added comment as a dictionary + return { + "id": row[0], + "task_id": row[1], + "commented_by": row[2], + "comment": row[3], + "created_at": row[4], + } + + async def update_task_history( tasks: List[tasks_schemas.Task], db: Session = Depends(database.get_db) ): diff --git a/src/backend/app/tasks/tasks_routes.py b/src/backend/app/tasks/tasks_routes.py index eef7682d68..9b8bf04fb2 100644 --- a/src/backend/app/tasks/tasks_routes.py +++ b/src/backend/app/tasks/tasks_routes.py @@ -25,7 +25,7 @@ from sqlalchemy.orm import Session from sqlalchemy.sql import text -from app.auth.osm import AuthUser +from app.auth.osm import AuthUser, login_required from app.auth.roles import get_uid, mapper, project_admin from app.central import central_crud from app.db import database @@ -196,6 +196,47 @@ async def task_features_count( return data +@router.get("/task-comments/", response_model=list[tasks_schemas.TaskCommentResponse]) +async def task_comments( + project_id: int, + task_id: int, + db: Session = Depends(database.get_db), +): + """Retrieve a list of task comments for a specific project and task. + + Args: + project_id (int): The ID of the project. + task_id (int): The ID of the task. + db (Session, optional): The database session. + + Returns: + List[tasks_schemas.TaskCommentResponse]: A list of task comments. + """ + task_comment_list = await tasks_crud.get_task_comments(db, project_id, task_id) + + return task_comment_list + + +@router.post("/task-comments/", response_model=tasks_schemas.TaskCommentResponse) +async def add_task_comments( + comment: tasks_schemas.TaskCommentRequest, + db: Session = Depends(database.get_db), + user_data: AuthUser = Depends(login_required), +): + """Create a new task comment. + + Parameters: + comment (TaskCommentRequest): The task comment to be created. + db (Session): The database session. + user_data (AuthUser): The authenticated user. + + Returns: + TaskCommentResponse: The created task comment. + """ + task_comment_list = await tasks_crud.add_task_comments(db, comment, user_data) + return task_comment_list + + @router.get("/activity/", response_model=List[tasks_schemas.TaskHistoryCount]) async def task_activity( project_id: int, days: int = 10, db: Session = Depends(database.get_db) diff --git a/src/backend/app/tasks/tasks_schemas.py b/src/backend/app/tasks/tasks_schemas.py index 96ea8fe213..50986562f5 100644 --- a/src/backend/app/tasks/tasks_schemas.py +++ b/src/backend/app/tasks/tasks_schemas.py @@ -133,6 +133,32 @@ def decrypt_password(self, value: str) -> Optional[str]: return decrypt_value(value) +class TaskCommentResponse(BaseModel): + """Task mapping history.""" + + id: int + task_id: int + comment: Optional[str] = None + commented_by: str + created_at: datetime + + +class TaskCommentBase(BaseModel): + """Task mapping history.""" + + comment: str + commented_by: str + created_at: datetime + + +class TaskCommentRequest(BaseModel): + """Task mapping history.""" + + task_id: int + project_id: int + comment: str + + class ReadTask(Task): """Task details plus updated task history."""