diff --git a/src/backend/app/projects/project_deps.py b/src/backend/app/projects/project_deps.py index 6fc80742cf..b7c5f384f6 100644 --- a/src/backend/app/projects/project_deps.py +++ b/src/backend/app/projects/project_deps.py @@ -34,7 +34,7 @@ async def get_project_by_id( db: Session = Depends(get_db), project_id: Optional[int] = None -) -> Optional[DbProject]: +) -> DbProject: """Get a single project by id.""" if not project_id: # Skip if no project id passed @@ -96,3 +96,28 @@ async def get_odk_credentials(db: Session, project_id: int): odk_central_user=user, odk_central_password=password, ) + + +async def get_project_xform(db, project_id): + """Retrieve the transformation associated with a specific project. + + Args: + db: Database connection object. + project_id: The ID of the project to retrieve the transformation for. + + Returns: + The transformation record associated with the specified project. + + Raises: + None + """ + sql = text( + """ + SELECT * FROM xforms + WHERE project_id = :project_id; + """ + ) + + result = db.execute(sql, {"project_id": project_id}) + db_xform = result.first() + return db_xform diff --git a/src/backend/app/submissions/submission_crud.py b/src/backend/app/submissions/submission_crud.py index 2cf4d28d0e..01445f6420 100644 --- a/src/backend/app/submissions/submission_crud.py +++ b/src/backend/app/submissions/submission_crud.py @@ -32,7 +32,6 @@ import sozipfile.sozipfile as zipfile from asgiref.sync import async_to_sync -from dateutil import parser from fastapi import HTTPException, Response from fastapi.responses import FileResponse from loguru import logger as log @@ -757,12 +756,9 @@ async def get_submissions_by_date( async def get_submission_by_project( project_id: int, - skip: 0, - limit: 100, + filters: dict, db: Session, - submitted_by: Optional[str] = None, - review_state: Optional[str] = None, - submitted_date: Optional[str] = None, + task_id: Optional[int] = None, ): """Get submission by project. @@ -770,12 +766,10 @@ async def get_submission_by_project( Args: project_id (int): The ID of the project. - skip (int): The number of submissions to skip. - limit (int): The maximum number of submissions to retrieve. + filters (dict): The filters to apply directly to submissions + in odk central. db (Session): The database session. - submitted_by: username of submitter. - review_state: reviewState of the submission. - submitted_date: date of submissions. + task_id (Optional[int]): The index task of the project. Returns: Tuple[int, List]: A tuple containing the total number of submissions and @@ -786,81 +780,63 @@ async def get_submission_by_project( """ project = await project_crud.get_project(db, project_id) - s3_project_path = f"/{project.organisation_id}/{project_id}" - s3_submission_path = f"/{s3_project_path}/submission.zip" + db_xform = await project_deps.get_project_xform(db, project.id) + odk_central = await project_deps.get_odk_credentials(db, project_id) - try: - file = get_obj_from_bucket(settings.S3_BUCKET_NAME, s3_submission_path) - except ValueError: - return 0, [] + xform = get_odk_form(odk_central) + data = xform.listSubmissions(project.odkid, db_xform.odk_form_id, filters) + submissions = data.get("value", []) + count = data.get("@odata.count", 0) - with zipfile.ZipFile(file, "r") as zip_ref: - with zip_ref.open("submissions.json") as file_in_zip: - content = json.loads(file_in_zip.read()) - if submitted_by: - content = [ - sub for sub in content if submitted_by.lower() in sub["username"].lower() - ] - if review_state: - content = [ - sub - for sub in content - if sub.get("__system", {}).get("reviewState") == review_state - ] - if submitted_date: - content = [ + if task_id: + submissions = [ sub - for sub in content - if parser.parse(sub.get("end")).date() - == parser.parse(submitted_date).date() + for sub in submissions + if sub.get("all", {}).get("task_id") == str(task_id) ] - start_index = skip - end_index = skip + limit - paginated_content = content[start_index:end_index] - return len(content), paginated_content + return count, submissions -async def get_submission_by_task( - project: db_models.DbProject, - task_id: int, - filters: dict, - db: Session, -): - """Get submissions and count by task. +# FIXME this is not needed now it can be directly filtered from submission table +# async def get_submission_by_task( +# project: db_models.DbProject, +# task_id: int, +# filters: dict, +# db: Session, +# ): +# """Get submissions and count by task. - Args: - project: The project instance. - task_id: The ID of the task. - filters: A dictionary of filters. - db: The database session. +# Args: +# project: The project instance. +# task_id: The ID of the task. +# filters: A dictionary of filters. +# db: The database session. - Returns: - Tuple: A tuple containing the list of submissions and the count. - """ - odk_credentials = await project_deps.get_odk_credentials(db, project.id) +# Returns: +# Tuple: A tuple containing the list of submissions and the count. +# """ +# odk_credentials = await project_deps.get_odk_credentials(db, project.id) - xform = get_odk_form(odk_credentials) - xform_name = f"{project.project_name_prefix}_task_{task_id}" - data = xform.listSubmissions(project.odkid, xform_name, filters) - submissions = data.get("value", []) - count = data.get("@odata.count", 0) +# xform = get_odk_form(odk_credentials) +# db_xform = await project_deps.get_project_xform(db, project.id) +# data = xform.listSubmissions(project.odkid, db_xform.odk_form_id, filters) +# submissions = data.get("value", []) +# count = data.get("@odata.count", 0) - return submissions, count +# return submissions, count async def get_submission_detail( - project: db_models.DbProject, - task_id: int, submission_id: str, + project: db_models.DbProject, db: Session, ): """Get the details of a submission. Args: + submission_id: The intance uuid of the submission. project: The project object representing the project. - task_id: The ID of the task associated with the submission. - submission_id: The ID of the submission. db: The database session. Returns: @@ -868,7 +844,8 @@ async def get_submission_detail( """ odk_credentials = await project_deps.get_odk_credentials(db, project.id) odk_form = get_odk_form(odk_credentials) - xform = f"{project.project_name_prefix}_task_{task_id}" - submission = odk_form.getSubmissions(project.odkid, xform, submission_id) - - return json.loads(submission) + db_xform = await project_deps.get_project_xform(db, project.id) + submission = json.loads( + odk_form.getSubmissions(project.odkid, db_xform.odk_form_id, submission_id) + ) + return submission.get("value", [])[0] diff --git a/src/backend/app/submissions/submission_routes.py b/src/backend/app/submissions/submission_routes.py index 72a337c4b9..e2560c59d4 100644 --- a/src/backend/app/submissions/submission_routes.py +++ b/src/backend/app/submissions/submission_routes.py @@ -360,16 +360,17 @@ async def get_submission_form_fields( project = await project_crud.get_project(db, project_id) odk_credentials = await project_deps.get_odk_credentials(db, project_id) odk_form = central_crud.get_odk_form(odk_credentials) - xform_name = project.forms[0].odk_form_id - return odk_form.formFields(project.odkid, xform_name) + db_xform = await project_deps.get_project_xform(db, project.id) + + return odk_form.formFields(project.odkid, db_xform.odk_form_id) @router.get("/submission_table/{project_id}") async def submission_table( - background_tasks: BackgroundTasks, project_id: int, page: int = Query(1, ge=1), results_per_page: int = Query(13, le=100), + task_id: Optional[int] = None, submitted_by: Optional[str] = None, review_state: Optional[str] = None, submitted_date: Optional[str] = Query( @@ -387,51 +388,8 @@ async def submission_table( task_id: The ID of the task. """ skip = (page - 1) * results_per_page - limit = results_per_page - count, data = await submission_crud.get_submission_by_project( - project_id, skip, limit, db, submitted_by, review_state, submitted_date - ) - background_task_id = await project_crud.insert_background_task_into_database( - db, "sync_submission", project_id - ) - - background_tasks.add_task( - submission_crud.update_submission_in_s3, db, project_id, background_task_id - ) - pagination = await project_crud.get_pagination(page, count, results_per_page, count) - response = submission_schemas.PaginatedSubmissions( - results=data, - pagination=submission_schemas.PaginationInfo(**pagination.model_dump()), - ) - return response - - -@router.get("/task_submissions/{project_id}") -async def task_submissions( - task_id: int, - project: db_models.DbProject = Depends(project_deps.get_project_by_id), - page: int = Query(1, ge=1), - limit: int = Query(13, le=100), - submission_id: Optional[str] = None, - submitted_by: Optional[str] = None, - review_state: Optional[str] = None, - submitted_date: Optional[str] = Query( - None, title="Submitted Date", description="Date in format (e.g., 'YYYY-MM-DD')" - ), - db: Session = Depends(database.get_db), - current_user: AuthUser = Depends(mapper), -): - """This api returns the submission table of a project. - - It takes two parameter: project_id and task_id. - - project_id: The ID of the project. - - task_id: The ID of the task. - """ - skip = (page - 1) * limit filters = { - "$top": limit, + "$top": results_per_page, "$skip": skip, "$count": True, "$wkt": True, @@ -455,29 +413,108 @@ async def task_submissions( else: filters["$filter"] = f"__system/reviewState eq '{review_state}'" - data, count = await submission_crud.get_submission_by_task( - project, task_id, filters, db + count, data = await submission_crud.get_submission_by_project( + project_id, filters, db, task_id ) - pagination = await project_crud.get_pagination(page, count, limit, count) + + pagination = await project_crud.get_pagination(page, count, results_per_page, count) response = submission_schemas.PaginatedSubmissions( results=data, pagination=submission_schemas.PaginationInfo(**pagination.model_dump()), ) - if submission_id: - submission_detail = await submission_crud.get_submission_detail( - project, task_id, submission_id, db - ) - response = submission_detail.get("value", [])[0] return response +# FIXME remove it since separate endpoint is not required now. +# @router.get("/task_submissions/{project_id}") +# async def task_submissions( +# task_id: int, +# project: db_models.DbProject = Depends(project_deps.get_project_by_id), +# page: int = Query(1, ge=1), +# limit: int = Query(13, le=100), +# submission_id: Optional[str] = None, +# submitted_by: Optional[str] = None, +# review_state: Optional[str] = None, +# submitted_date: Optional[str] = Query( +# None, title="Submitted Date", description="Date in format (e.g., 'YYYY-MM-DD')" +# ), +# db: Session = Depends(database.get_db), +# current_user: AuthUser = Depends(mapper), +# ): +# """This api returns the submission table of a project. + +# It takes two parameter: project_id and task_id. + +# project_id: The ID of the project. + +# task_id: The ID of the task. +# """ +# skip = (page - 1) * limit +# filters = { +# "$top": limit, +# "$skip": skip, +# "$count": True, +# "$wkt": True, +# } + +# if submitted_date: +# filters["$filter"] = ( +# "__system/submissionDate ge {}T00:00:00+00:00 " +# "and __system/submissionDate le {}T23:59:59.999+00:00" +# ).format(submitted_date, submitted_date) + +# if submitted_by: +# if "$filter" in filters: +# filters["$filter"] += f"and (username eq '{submitted_by}')" +# else: +# filters["$filter"] = f"username eq '{submitted_by}'" + +# if review_state: +# if "$filter" in filters: +# filters["$filter"] += f" and (__system/reviewState eq '{review_state}')" +# else: +# filters["$filter"] = f"__system/reviewState eq '{review_state}'" + +# data, count = await submission_crud.get_submission_by_task( +# project, task_id, filters, db +# ) +# pagination = await project_crud.get_pagination(page, count, limit, count) +# response = submission_schemas.PaginatedSubmissions( +# results=data, +# pagination=submission_schemas.PaginationInfo(**pagination.model_dump()), +# ) +# if submission_id: +# submission_detail = await submission_crud.get_submission_detail( +# project, task_id, submission_id, db +# ) +# response = submission_detail.get("value", [])[0] + +# return response + + +@router.get("/submission-detail/{project_id}") +async def submission_detail( + submission_id: str, + project: db_models.DbProject = Depends(project_deps.get_project_by_id), + db: Session = Depends(database.get_db), + current_user: AuthUser = Depends(mapper), +) -> dict: + """This api returns the submission detail of individual submission. + + It takes two parameter: project_id and submission_id. + """ + submission_detail = await submission_crud.get_submission_detail( + submission_id, project, db + ) + return submission_detail + + @router.post("/update_review_state/{project_id}") async def update_review_state( project_id: int, instance_id: str, review_state: ReviewStateEnum, - task_id: int, current_user: AuthUser = Depends(project_admin), db: Session = Depends(database.get_db), ): @@ -485,9 +522,8 @@ async def update_review_state( Args: project_id (int): The ID of the project. - instance_id (str): The ID of the submission instance. + instance_id (str): The uuid of the submission instance. review_state (ReviewStateEnum): The new review state to be set. - task_id (int): The ID of the task associated with the submission. current_user(AuthUser): logged in user. db (Session): The database session dependency. """ @@ -495,9 +531,11 @@ async def update_review_state( project = await project_crud.get_project(db, project_id) odk_creds = await project_deps.get_odk_credentials(db, project_id) odk_project = central_crud.get_odk_project(odk_creds) + db_xform = await project_deps.get_project_xform(db, project.id) + response = odk_project.updateReviewState( project.odkid, - f"{project.project_name_prefix}_task_{task_id}", + db_xform.odk_form_id, instance_id, {"reviewState": review_state}, ) diff --git a/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx b/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx index a83749be4b..c7547f1899 100644 --- a/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx +++ b/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx @@ -101,21 +101,12 @@ const SubmissionsTable = ({ toggleView }) => { }, []); useEffect(() => { - if (!filter.task_id) { - dispatch( - SubmissionTableService(`${import.meta.env.VITE_API_URL}/submission/submission_table/${projectId}`, { - page: paginationPage, - ...filter, - }), - ); - } else { - dispatch( - SubmissionTableService(`${import.meta.env.VITE_API_URL}/submission/task_submissions/${projectId}`, { - page: paginationPage, - ...filter, - }), - ); - } + dispatch( + SubmissionTableService(`${import.meta.env.VITE_API_URL}/submission/submission_table/${projectId}`, { + page: paginationPage, + ...filter, + }), + ); }, [filter, paginationPage]); useEffect(() => { @@ -127,21 +118,12 @@ const SubmissionsTable = ({ toggleView }) => { SubmissionFormFieldsService(`${import.meta.env.VITE_API_URL}/submission/submission_form_fields/${projectId}`), ); dispatch(SubmissionActions.SetSubmissionTableRefreshing(true)); - if (!filter.task_id) { - dispatch( - SubmissionTableService(`${import.meta.env.VITE_API_URL}/submission/submission_table/${projectId}`, { - page: paginationPage, - ...filter, - }), - ); - } else { - dispatch( - SubmissionTableService(`${import.meta.env.VITE_API_URL}/submission/task_submissions/${projectId}`, { - page: paginationPage, - ...filter, - }), - ); - } + dispatch( + SubmissionTableService(`${import.meta.env.VITE_API_URL}/submission/submission_table/${projectId}`, { + page: paginationPage, + ...filter, + }), + ); }; useEffect(() => { diff --git a/src/frontend/src/views/SubmissionDetails.tsx b/src/frontend/src/views/SubmissionDetails.tsx index 7d5bc8f4fe..f15dfd82af 100644 --- a/src/frontend/src/views/SubmissionDetails.tsx +++ b/src/frontend/src/views/SubmissionDetails.tsx @@ -16,7 +16,6 @@ const SubmissionDetails = () => { const params = CoreModules.useParams(); const navigate = useNavigate(); const projectId = params.projectId; - const taskId = params.taskId; const paramsInstanceId = params.instanceId; const projectDashboardDetail = CoreModules.useAppSelector((state) => state.project.projectDashboardDetail); const projectDashboardLoading = CoreModules.useAppSelector((state) => state.project.projectDashboardLoading); @@ -24,6 +23,8 @@ const SubmissionDetails = () => { const submissionDetails = useAppSelector((state) => state.submission.submissionDetails); const submissionDetailsLoading = useAppSelector((state) => state.submission.submissionDetailsLoading); + const taskId = submissionDetails?.all?.task_id; + useEffect(() => { dispatch(GetProjectDashboard(`${import.meta.env.VITE_API_URL}/projects/project_dashboard/${projectId}`)); }, []); @@ -31,12 +32,10 @@ const SubmissionDetails = () => { useEffect(() => { dispatch( SubmissionService( - `${ - import.meta.env.VITE_API_URL - }/submission/task_submissions/${projectId}?task_id=${taskId}&submission_id=${paramsInstanceId}`, + `${import.meta.env.VITE_API_URL}/submission/submission-detail/${projectId}?submission_id=${paramsInstanceId}`, ), ); - }, [projectId, taskId, paramsInstanceId]); + }, [projectId, paramsInstanceId]); function removeNullValues(obj) { const newObj = {};