Skip to content

Commit

Permalink
fix: prevent project generation if api failure (#1627)
Browse files Browse the repository at this point in the history
* fix(frontend): prevent project generation if api failure

* Prevent project generation if api failure pt2 (#1635)

* fix(hotfix): download of basemaps in ui, max zoom level 22 used for tms

* ci: update all gh-workflows to latest v1.6.0

* build: upgrade rclone --> v1 pin to avoid CVE-2024-24790

* build: remove unnecessary tables and field from db schema (#1623)

* build: remove unnecessary tables via migration & simplify schema

* refactor: remove references to removed tables in sqlalchemy models

* build: add IF EXISTS to DROP COLUMN in migration

* build: remove default columns from previous migration

* fix(backend): minor fixes to HTTPException on endpoints

* fix(backend): addded created date on the project response, set expiry of access token to 1 hour (#1633)

* docs: add placeholder for axiom repo activity

* fix(createProjectSlice): set generateProject & drawToggle status to default

* feat(commonUtils): isStatusSuccess function add

* test: get the detailed task history for a project (#1626)

* build(backend): add async-lru dep, remove cpuinfo dep

* fix: replace lru_cache with async for getting odk creds

* fix(splitTasks): dependency add to useEffect

* fix(createProjectService): halt project creation if api failure

---------

Co-authored-by: spwoodcock <sam.woodcock@protonmail.com>
Co-authored-by: Sam <78538841+spwoodcock@users.noreply.github.com>
Co-authored-by: Sujan Adhikari <109404840+Sujanadh@users.noreply.github.com>
Co-authored-by: Azhar Ismagulova <31756707+azharcodeit@users.noreply.github.com>

* build: update to latest ms playwright image v1.45.1

---------

Co-authored-by: Nishit Suwal <81785002+NSUWAL123@users.noreply.github.com>
Co-authored-by: Sujan Adhikari <109404840+Sujanadh@users.noreply.github.com>
Co-authored-by: Azhar Ismagulova <31756707+azharcodeit@users.noreply.github.com>
  • Loading branch information
4 people authored Jul 8, 2024
1 parent 33b86a7 commit 4db96ea
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 107 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pr_test_frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ jobs:
e2e-tests:
uses: hotosm/gh-workflows/.github/workflows/test_pnpm.yml@main
with:
container_config: '{"image": "mcr.microsoft.com/playwright:v1.44.1"}'
container_config: '{"image": "mcr.microsoft.com/playwright:v1.45.1"}'
working_dir: src/frontend
run_command: "test:e2e"
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ services:

ui-test:
profiles: ["ui-test"]
image: mcr.microsoft.com/playwright:v1.44.1
image: mcr.microsoft.com/playwright:v1.45.1
working_dir: /app
environment:
- DISPLAY=:0
Expand Down
200 changes: 121 additions & 79 deletions src/frontend/src/api/CreateProjectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@/models/createproject/createProjectModel';
import { CommonActions } from '@/store/slices/CommonSlice';
import { ValidateCustomFormResponse } from '@/store/types/ICreateProject';
import { isStatusSuccess } from '@/utilfunctions/commonUtils';

const CreateProjectService: Function = (
url: string,
Expand All @@ -21,72 +22,91 @@ const CreateProjectService: Function = (
dispatch(CreateProjectActions.CreateProjectLoading(true));
dispatch(CommonActions.SetLoading(true));

const postCreateProjectDetails = async (url, projectData, taskAreaGeojson, formUpload) => {
try {
// Create project
const postNewProjectDetails = await axios.post(url, projectData);
const resp: ProjectDetailsModel = postNewProjectDetails.data;
await dispatch(CreateProjectActions.PostProjectDetails(resp));

// Submit task boundaries
await dispatch(
UploadTaskAreasService(
`${import.meta.env.VITE_API_URL}/projects/${resp.id}/upload-task-boundaries`,
taskAreaGeojson,
),
);
let projectId: null | number = null;
try {
// halt project creation if any api call fails
let hasAPISuccess = false;

dispatch(
CommonActions.SetSnackBar({
open: true,
message: 'Project Successfully Created Now Generating QR For Project',
variant: 'success',
duration: 2000,
}),
);
const postNewProjectDetails = await API.post(url, projectData);
hasAPISuccess = isStatusSuccess(postNewProjectDetails.status);

if (isOsmExtract) {
// Upload data extract generated from raw-data-api
const response = await axios.get(
`${import.meta.env.VITE_API_URL}/projects/data-extract-url/?project_id=${resp.id}&url=${
projectData.data_extract_url
}`,
);
} else if (dataExtractFile) {
// Upload custom data extract from user
const dataExtractFormData = new FormData();
dataExtractFormData.append('custom_extract_file', dataExtractFile);
const response = await axios.post(
`${import.meta.env.VITE_API_URL}/projects/upload-custom-extract/?project_id=${resp.id}`,
dataExtractFormData,
);
}
const projectCreateResp: ProjectDetailsModel = postNewProjectDetails.data;
await dispatch(CreateProjectActions.PostProjectDetails(projectCreateResp));

if (!hasAPISuccess) {
throw new Error(`Request failed with status ${projectCreateResp.status}`);
}
projectId = projectCreateResp.id;

// Submit task boundaries
hasAPISuccess = await dispatch(
UploadTaskAreasService(
`${import.meta.env.VITE_API_URL}/projects/${projectId}/upload-task-boundaries`,
taskAreaGeojson,
),
);

if (!hasAPISuccess) {
throw new Error(`Request failed`);
}

// Generate QR codes
await dispatch(
GenerateProjectQRService(
`${import.meta.env.VITE_API_URL}/projects/${resp.id}/generate-project-data`,
projectData,
formUpload,
),
// Upload data extract
let extractResponse;
if (isOsmExtract) {
// Generated extract from raw-data-api
extractResponse = await API.get(
`${import.meta.env.VITE_API_URL}/projects/data-extract-url/?project_id=${projectId}&url=${
projectData.data_extract_url
}`,
);
} catch (error: any) {
// Added Snackbar toast for error message
dispatch(
CommonActions.SetSnackBar({
open: true,
message: JSON.stringify(error?.response?.data?.detail) || 'Something went wrong.',
variant: 'error',
duration: 2000,
}),
} else if (dataExtractFile) {
// Custom data extract from user
const dataExtractFormData = new FormData();
dataExtractFormData.append('custom_extract_file', dataExtractFile);
extractResponse = await API.post(
`${import.meta.env.VITE_API_URL}/projects/upload-custom-extract/?project_id=${projectId}`,
dataExtractFormData,
);
dispatch(CreateProjectActions.CreateProjectLoading(false));
} finally {
dispatch(CommonActions.SetLoading(false));
}
};
hasAPISuccess = isStatusSuccess(extractResponse.status);

if (!hasAPISuccess) {
throw new Error(`Request failed with status ${extractResponse.status}`);
}

await postCreateProjectDetails(url, projectData, taskAreaGeojson, formUpload);
// Generate project files
const generateProjectFile = await dispatch(
GenerateProjectFilesService(
`${import.meta.env.VITE_API_URL}/projects/${projectId}/generate-project-data`,
projectData,
formUpload,
),
);

hasAPISuccess = generateProjectFile;
if (!hasAPISuccess) {
throw new Error(`Request failed`);
}
dispatch(CreateProjectActions.GenerateProjectError(false));
// dispatch(CreateProjectActions.CreateProjectLoading(false));
} catch (error: any) {
if (projectId) {
await dispatch(DeleteProjectService(`${import.meta.env.VITE_API_URL}/projects/${projectId}`, false));
}

await dispatch(CreateProjectActions.GenerateProjectError(true));
dispatch(
CommonActions.SetSnackBar({
open: true,
message: JSON.stringify(error?.response?.data?.detail) || 'Something went wrong. Please try again.',
variant: 'error',
duration: 2000,
}),
);
dispatch(CreateProjectActions.CreateProjectLoading(false));
} finally {
dispatch(CommonActions.SetLoading(false));
}
};
};

Expand All @@ -107,10 +127,12 @@ const FormCategoryService: Function = (url: string) => {
await getFormCategoryList(url);
};
};

const UploadTaskAreasService: Function = (url: string, filePayload: any, projectData: any) => {
return async (dispatch) => {
dispatch(CreateProjectActions.UploadAreaLoading(true));
const postUploadArea = async (url, filePayload) => {
let isAPISuccess = true;
try {
const areaFormData = new FormData();
areaFormData.append('task_geojson', filePayload);
Expand All @@ -119,10 +141,17 @@ const UploadTaskAreasService: Function = (url: string, filePayload: any, project
'Content-Type': 'multipart/form-data',
},
});
// const resp: UploadAreaDetailsModel = postNewProjectDetails.data;
await dispatch(CreateProjectActions.UploadAreaLoading(false));
await dispatch(CreateProjectActions.PostUploadAreaSuccess(postNewProjectDetails.data));
isAPISuccess = isStatusSuccess(postNewProjectDetails.status);

if (isAPISuccess) {
await dispatch(CreateProjectActions.UploadAreaLoading(false));
await dispatch(CreateProjectActions.PostUploadAreaSuccess(postNewProjectDetails.data));
} else {
throw new Error(`Request failed with status ${postNewProjectDetails.status}`);
}
} catch (error: any) {
isAPISuccess = false;
await dispatch(CreateProjectActions.GenerateProjectError(true));
dispatch(
CommonActions.SetSnackBar({
open: true,
Expand All @@ -133,39 +162,48 @@ const UploadTaskAreasService: Function = (url: string, filePayload: any, project
);
dispatch(CreateProjectActions.UploadAreaLoading(false));
}
return isAPISuccess;
};

await postUploadArea(url, filePayload);
return await postUploadArea(url, filePayload);
};
};
const GenerateProjectQRService: Function = (url: string, projectData: any, formUpload: any) => {

const GenerateProjectFilesService: Function = (url: string, projectData: any, formUpload: any) => {
return async (dispatch) => {
dispatch(CreateProjectActions.GenerateProjectQRLoading(true));
dispatch(CreateProjectActions.GenerateProjectLoading(true));
dispatch(CommonActions.SetLoading(true));

const postUploadArea = async (url, projectData: any, formUpload) => {
let isAPISuccess = true;
try {
let postNewProjectDetails;
let response;

if (projectData.form_ways === 'custom_form') {
// TODO move form upload to a separate service / endpoint?
const generateApiFormData = new FormData();
generateApiFormData.append('xls_form_upload', formUpload);
postNewProjectDetails = await axios.post(url, generateApiFormData, {
response = await axios.post(url, generateApiFormData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
} else {
postNewProjectDetails = await axios.post(url, {});
response = await axios.post(url, {});
}
isAPISuccess = isStatusSuccess(response.status);
if (!isAPISuccess) {
throw new Error(`Request failed with status ${response.status}`);
}

const resp: string = postNewProjectDetails.data;
await dispatch(CreateProjectActions.GenerateProjectQRLoading(false));
await dispatch(CreateProjectActions.GenerateProjectLoading(false));
dispatch(CommonActions.SetLoading(false));
await dispatch(CreateProjectActions.GenerateProjectQRSuccess(resp));
// Trigger the watcher and redirect after success
await dispatch(CreateProjectActions.GenerateProjectSuccess(true));
} catch (error: any) {
isAPISuccess = false;
dispatch(CommonActions.SetLoading(false));
await dispatch(CreateProjectActions.GenerateProjectError(true));
dispatch(
CommonActions.SetSnackBar({
open: true,
Expand All @@ -174,11 +212,12 @@ const GenerateProjectQRService: Function = (url: string, projectData: any, formU
duration: 2000,
}),
);
dispatch(CreateProjectActions.GenerateProjectQRLoading(false));
dispatch(CreateProjectActions.GenerateProjectLoading(false));
}
return isAPISuccess;
};

await postUploadArea(url, projectData, formUpload);
return await postUploadArea(url, projectData, formUpload);
};
};

Expand Down Expand Up @@ -333,6 +372,7 @@ const PatchProjectDetails: Function = (url: string, projectData: any) => {
await patchProjectDetails(url, projectData);
};
};

const PostFormUpdate: Function = (url: string, projectData: any) => {
return async (dispatch) => {
dispatch(CreateProjectActions.SetPostFormUpdateLoading(true));
Expand Down Expand Up @@ -452,23 +492,25 @@ const ValidateCustomForm: Function = (url: string, formUpload: any) => {
};
};

const DeleteProjectService: Function = (url: string) => {
const DeleteProjectService: Function = (url: string, hasRedirect: boolean = true) => {
return async (dispatch) => {
const deleteProject = async (url: string) => {
try {
await API.delete(url);
dispatch(
CommonActions.SetSnackBar({
open: true,
message: 'Project deleted. Redirecting...',
message: `Project deleted. ${hasRedirect && 'Redirecting...'}`,
variant: 'success',
duration: 2000,
}),
);
// Redirect to homepage
setTimeout(() => {
window.location.href = '/';
}, 2000);
if (hasRedirect) {
setTimeout(() => {
window.location.href = '/';
}, 2000);
}
} catch (error) {
if (error.response.status === 404) {
dispatch(
Expand All @@ -492,7 +534,7 @@ export {
UploadTaskAreasService,
CreateProjectService,
FormCategoryService,
GenerateProjectQRService,
GenerateProjectFilesService,
OrganisationService,
GetDividedTaskFromGeojson,
TaskSplittingPreviewService,
Expand Down
10 changes: 5 additions & 5 deletions src/frontend/src/components/createnewproject/SplitTasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customDataExtractUpload
const projectDetails = useAppSelector((state) => state.createproject.projectDetails);
const dataExtractGeojson = useAppSelector((state) => state.createproject.dataExtractGeojson);

const generateQrSuccess = useAppSelector((state) => state.createproject.generateQrSuccess);
const generateProjectSuccess = useAppSelector((state) => state.createproject.generateProjectSuccess);
const generateProjectError = useAppSelector((state) => state.createproject.generateProjectError);
const projectDetailsResponse = useAppSelector((state) => state.createproject.projectDetailsResponse);
const dividedTaskGeojson = useAppSelector((state) => state.createproject.dividedTaskGeojson);
const projectDetailsLoading = useAppSelector((state) => state.createproject.projectDetailsLoading);
Expand Down Expand Up @@ -191,12 +192,12 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customDataExtractUpload
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const handleQRGeneration = async () => {
if (generateQrSuccess) {
if (!generateProjectError && generateProjectSuccess) {
const projectId = projectDetailsResponse?.id;
dispatch(
CommonActions.SetSnackBar({
open: true,
message: 'QR Generation Completed. Redirecting...',
message: 'Project Generation Completed. Redirecting...',
variant: 'success',
duration: 2000,
}),
Expand All @@ -205,15 +206,14 @@ const SplitTasks = ({ flag, geojsonFile, setGeojsonFile, customDataExtractUpload
// Add 5-second delay to allow backend Entity generation to catch up
await delay(5000);
dispatch(CreateProjectActions.CreateProjectLoading(false));
dispatch(CreateProjectActions.SetGenerateProjectQRSuccess(null));
navigate(`/project/${projectId}`);
dispatch(CreateProjectActions.ClearCreateProjectFormData());
dispatch(CreateProjectActions.SetCanSwitchCreateProjectSteps(false));
}
};

handleQRGeneration();
}, [generateQrSuccess]);
}, [generateProjectSuccess, generateProjectError]);

const renderTraceback = (errorText: string) => {
if (!errorText) {
Expand Down
Loading

0 comments on commit 4db96ea

Please sign in to comment.