diff --git a/src/backend/app/projects/project_logic.py b/src/backend/app/projects/project_logic.py index 01a36390..624337b3 100644 --- a/src/backend/app/projects/project_logic.py +++ b/src/backend/app/projects/project_logic.py @@ -180,7 +180,7 @@ def process_drone_images(project_id: uuid.UUID, task_id: uuid.UUID): ) -async def get_project_info_from_s3(project_id: uuid.UUID, task_id: uuid.UUID): +def get_project_info_from_s3(project_id: uuid.UUID, task_id: uuid.UUID): """ Helper function to get the number of images and the URL to download the assets. """ diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index f6f4e0cc..5f985051 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -365,4 +365,4 @@ async def get_assets_info( """ Endpoint to get the number of images and the URL to download the assets for a given project and task. """ - return await project_logic.get_project_info_from_s3(project.id, task_id) + return project_logic.get_project_info_from_s3(project.id, task_id) diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index b06b4dc6..7b4f4ab2 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -2,6 +2,7 @@ import uuid from typing import Annotated, Optional, List from datetime import datetime, date +from app.projects import project_logic import geojson from loguru import logger as log from pydantic import BaseModel, computed_field, Field, model_validator @@ -26,6 +27,13 @@ from app.s3 import get_image_dir_url +class AssetsInfo(BaseModel): + project_id: str + task_id: str + image_count: int + assets_url: Optional[str] + + def validate_geojson( value: FeatureCollection | Feature | Polygon, ) -> geojson.FeatureCollection: @@ -121,16 +129,41 @@ def validate_to_json(cls, value): return value +class AssetsInfoData(BaseModel): + project_id: int + + class TaskOut(BaseModel): """Base project model.""" id: uuid.UUID + project_id: uuid.UUID project_task_index: int - outline: Optional[Polygon | Feature | FeatureCollection] + outline: Optional[Polygon | Feature | FeatureCollection] = None state: Optional[str] = None user_id: Optional[str] = None task_area: Optional[float] = None name: Optional[str] = None + image_count: Optional[int] = None + assets_url: Optional[str] = None + + @model_validator(mode="after") + def set_assets_url(cls, values): + """Set image_url and image count before rendering the model.""" + task_id = values.id + project_id = values.project_id + + if task_id and project_id: + data = project_logic.get_project_info_from_s3(project_id, task_id) + if data: + return values.copy( + update={ + "assets_url": data.assets_url, + "image_count": data.image_count, + } + ) + + return values class DbProject(BaseModel): @@ -227,6 +260,7 @@ async def one(db: Connection, project_id: uuid.UUID): SELECT t.id, t.project_task_index, + t.project_id, ST_AsGeoJSON(t.outline)::jsonb -> 'coordinates' AS coordinates, ST_AsGeoJSON(t.outline)::jsonb -> 'type' AS type, ST_XMin(ST_Envelope(t.outline)) AS xmin, @@ -253,6 +287,7 @@ async def one(db: Connection, project_id: uuid.UUID): user_id, name, task_area, + project_id, jsonb_build_object( 'type', 'Feature', 'geometry', jsonb_build_object( @@ -457,10 +492,3 @@ class PresignedUrlRequest(BaseModel): task_id: uuid.UUID image_name: List[str] expiry: int # Expiry time in hours - - -class AssetsInfo(BaseModel): - project_id: str - task_id: str - image_count: int - assets_url: Optional[str] diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index 596e75b0..cee3fa23 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -121,6 +121,7 @@ async def all(db: Connection, project_id: uuid.UUID): existing_tasks = await cur.fetchall() # Get all task_ids from the tasks table task_ids = await Task.get_all_tasks(db, project_id) + # Create a set of existing task_ids for quick lookup existing_task_ids = {task.task_id for task in existing_tasks} @@ -136,7 +137,6 @@ async def all(db: Connection, project_id: uuid.UUID): } for task_id in remaining_task_ids ] - # Combine both existing tasks and remaining tasks combined_tasks = existing_tasks + remaining_tasks return combined_tasks diff --git a/src/frontend/src/components/IndividualProject/Contributions/TableSection/index.tsx b/src/frontend/src/components/IndividualProject/Contributions/TableSection/index.tsx index 7279bbeb..96b5a7c9 100644 --- a/src/frontend/src/components/IndividualProject/Contributions/TableSection/index.tsx +++ b/src/frontend/src/components/IndividualProject/Contributions/TableSection/index.tsx @@ -1,6 +1,8 @@ import DataTable from '@Components/common/DataTable'; +import Icon from '@Components/common/Icon'; import { useTypedSelector } from '@Store/hooks'; import { useMemo } from 'react'; +import { toast } from 'react-toastify'; const contributionsDataColumns = [ { @@ -15,6 +17,41 @@ const contributionsDataColumns = [ header: 'Task Status', accessorKey: 'task_state', }, + { header: 'Image count', accessorKey: 'image_count' }, + + { + header: 'Orthophoto', + accessorKey: 'assets_url', + cell: ({ row }: any) => { + const handleDownloadResult = () => { + const { original: rowData } = row; + if (!rowData?.assets_url) return; + try { + const link = document.createElement('a'); + link.href = rowData?.assets_url; + link.download = 'assets.zip'; + document.body.appendChild(link); + link.click(); + link.remove(); + } catch (error) { + toast.error(`There wan an error while downloading file ${error}`); + } + }; + + return ( +