8000 admin: Add the survey export by object widget to Evolution by tahini · Pull Request #499 · chairemobilite/evolution · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

admin: Add the survey export by object widget to Evolution #499

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion locales/en/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,14 @@
},
"InterviewId": "interview ID",
"HouseholdSize": "Household size",
"Select": "Select"
"Select": "Select",
"export": {
"PrepareCsvExportFiles": "Prepare CSV files for export by object",
"RefreshExportFiles": "Refresh export file list",
"ServerErrorForExport": "Server error while preparing export files",
"ServerExportForbidden": "You are not authorized to export data",
"PrepareDataError": "Unexpected error while preparing data for export",
"UpdateOrWaitError": "Error while updating export files. Please wait a few seconds and try again",
"WaitForFileError": "Error getting the files to export from server"
}
}
11 changes: 10 additions & 1 deletion locales/fr/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,14 @@
},
"InterviewId": "ID d'entrevue",
"HouseholdSize": "Taille du ménage",
"Select": "Sélectionner"
"Select": "Sélectionner",
"export": {
"PrepareCsvExportFiles": "Préparer les fichiers CSV pour exportation par objet",
"RefreshExportFiles": "Rafraîchir la liste des fichiers d'export",
"ServerErrorForExport": "Erreur de serveur pendant la préparation des fichiers d'export",
"ServerExportForbidden": "Vous n'êtes pas autorisé à exporter les données d'enquête",
"PrepareDataError": "Erreur inattendue en préparant les fichiers d'export",
"UpdateOrWaitError": "Erreur lors de la mise à jour des fichiers d'export. Veuillez ré-essayer plus tard.",
"WaitForFileError": "Erreur en attendant la fin de la préparation des fichiers d'export"
}
}
4 changes: 4 additions & 0 deletions packages/evolution-backend/src/api/admin.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import moment from 'moment';
import knex from 'chaire-lib-backend/lib/config/shared/db.config';

import router from 'chaire-lib-backend/lib/api/admin.routes';
// Add export routes from admin/exports.routes
import { addExportRoutes } from './admin/exports.routes';

addExportRoutes();

router.all('/data/widgets/:widget/', (req, res, next) => {
const widgetName = req.params.widget;
Expand Down
71 changes: 71 additions & 0 deletions packages/evolution-backend/src/api/admin/exports.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2024, Polytechnique Montreal and contributors
*
* This file is licensed under the MIT License.
* License text available at https://opensource.org/licenses/MIT
*/
import path from 'path';
import {
exportAllToCsvByObject,
filePathOnServer,
getExportFiles,
isExportRunning
} from '../../services/adminExport/exportAllToCsvByObject';
import { fileManager } from 'chaire-lib-backend/lib/utils/filesystem/fileManager';
import * as Status from 'chaire-lib-common/lib/utils/Status';

// TODO Do not use the main admin router. Fine-tune the permissions for the export routes.
import router from 'chaire-lib-backend/lib/api/admin.routes';

export const addExportRoutes = () => {
console.log('addin export routes');
// Get a specific export file per object
router.get('/data/exportcsv/exports/:filePath', (req, res, next) => {
console.log('requesting csv file from path', req.params.filePath);
const projectRelativeFilePath = `${filePathOnServer}/${req.params.filePath}`;
const fileExists = fileManager.fileExists(projectRelativeFilePath);
if (fileExists) {
const fileName = path.basename(req.params.filePath);
res.setHeader('Content-disposition', `attachment; filename=${fileName}`);
res.set('Content-Type', 'text/csv');
return res.status(200).sendFile(fileManager.getAbsolutePath(projectRelativeFilePath));
} else {
return res.status(500).json({
status: 'error',
error: 'file does not exist'
});
}
});

// Route to get the status of the export task and the list of files
router.get('/data/getExportTaskResults', (req, res, next) => {
console.log('getting csv export files...');
try {
if (isExportRunning()) {
return res.status(200).json(Status.createOk({ taskRunning: true, files: [] }));
}
const files = getExportFiles();
return res.status(200).json(Status.createOk({ taskRunning: false, files }));
} catch (error) {
console.log('error getting csv export files', error);
return res.status(500).json(Status.createError('Error getting csv files'));
}
});

// Route to prepare the csv files to export
router.get('/data/prepareCsvFileForExportByObject', (req, res, next) => {
console.log('preparing csv export files...');
try {
const taskRunning = exportAllToCsvByObject();
return res.status(200).json({
status: taskRunning
});
} catch (error) {
return res.status(500).json({
status: 'error',
csvExportFilePaths: [],
error: 'could not prepare csv files for export: ' + error
});
}
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright 2024, Polytechnique Montreal and contributors
*
* This file is licensed under the MIT License.
* License text available at https://opensource.org/licenses/MIT
*/
import React, { useEffect, useState } from 'react';
import { WithTranslation, withTranslation } from 'react-i18next';
import Button from 'chaire-lib-frontend/lib/components/input/Button';
import FormErrors from 'chaire-lib-frontend/lib/components/pageParts/FormErrors';
import LoadingPage from 'chaire-lib-frontend/lib/components/pages/LoadingPage';
import * as Status from 'chaire-lib-common/lib/utils/Status';
import TrError from 'chaire-lib-common/lib/utils/TrError';

const ExportInterviewData = ({ t }: WithTranslation) => {
const [error, setError] = useState<string | undefined>(undefined);
const [isPreparingCsvExportFiles, setIsPreparingCsvExportFiles] = useState(false);
const [csvExportFilesReady, setCsvExportFilesReady] = useState(false);
const [csvExportFilePaths, setCsvExportFilePaths] = useState<string[]>([]);

useEffect(() => {
updateOrWaitForFiles(false);
}, []);

const () => {
setIsPreparingCsvExportFiles(true);
try {
const response = await fetch('/api/admin/data/prepareCsvFileForExportByObject', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (response.status === 200) {
updateOrWaitForFiles();
} else {
throw new TrError(
'Wrong response status ' + response.status,
'prepareNoResponse',
response.status >= 500
? 'admin:export:ServerErrorForExport'
: response.status === 401
? 'admin:export:ServerExportForbidden'
: 'admin:export:PrepareDataError'
);
}
} catch (err) {
console.log('Error preparing export files.', err);
setError(TrError.isTrError(err) ? err.message : 'admin:export:PrepareDataError');
setCsvExportFilesReady(false);
setCsvExportFilePaths([]);
setIsPreparingCsvExportFiles(false);
}
};

const updateOrWaitForFiles = async (listenForFiles = true) => {
try {
const response = await fetch('/api/admin/data/getExportTaskResults', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (response.status === 200) {
const status: Status.Status<{ taskRunning: boolean; files: string[] }> = await response.json();
if (Status.isStatusOk(status)) {
const { taskRunning, files } = Status.unwrap(status);
setCsvExportFilesReady(!taskRunning);
setCsvExportFilePaths(files);
setError(undefined);
setIsPreparingCsvExportFiles(taskRunning);
if (taskRunning && listenForFiles) {
setTimeout(updateOrWaitForFiles, 10000);
}
} else {
throw status.error;
}
} else {
throw new TrError(
'Wrong response status ' + response.status,
'updateOrWaitResponse',
response.status >= 500
? 'admin:export:ServerErrorForExport'
: response.status === 401
? 'admin:export:ServerExportForbidden'
: 'admin:export:UpdateOrWaitError'
);
}
} catch (err) {
console.log('Error fetching export files.', err);
setError(TrError.isTrError(err) ? err.message : 'admin:export:WaitForFileError');
setCsvExportFilesReady(false);
setCsvExportFilePaths([]);
setIsPreparingCsvExportFiles(false);
}
};

const => updateOrWaitForFiles(false);

const csvFileExportLinks = csvExportFilePaths.map((csvFilePath) => (
<li key={csvFilePath}>
<a
href={`/api/admin/data/exportcsv/${
csvFilePath.startsWith('exports/') ? csvFilePath : 'exports/' + csvFilePath
}`}
>
{csvFilePath}
</a>
</li>
));

return (
<div className="admin-widget-container">
{isPreparingCsvExportFiles && <LoadingPage />}
<Button color="blue" label={t('admin:export:PrepareCsvExportFiles')} />
{error && <FormErrors errors={[error]} />}
{csvExportFilesReady && <ul>{csvFileExportLinks}</ul>}
<ul>
<li>
<a style={{ cursor: 'pointer' }}>
{t('admin:export:RefreshExportFiles')}
</a>
</li>
</ul>
</div>
);
};

export default withTranslation()(ExportInterviewData);
2 changes: 2 additions & 0 deletions packages/evolution-legacy/src/components/admin/Monitoring.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import moment from 'moment';

import appConfig from 'evolution-frontend/lib/config/application.config';
import StartedAndCompletedInterviewsByDay from './monitoring/StartedAndCompletedInterviewByDay';
import ExportInterviewData from 'evolution-frontend/lib/components/admin/ExportInterviewData';
// FIXME Commented 2023-11-07 because of od_mtl_2023, it takes too long. Should it be a default widget? Or rather a widget implemented in evolution that surveys can optionally add?
//import InterviewsByHouseholdSize from './monitoring/InterviewsByHouseholdSize';
//import config from 'chaire-lib-common/lib/config/shared/project.config';
Expand Down Expand Up @@ -43,6 +44,7 @@ class Monitoring extends React.Component {
<div className="survey">
<div className="admin">
<StartedAndCompletedInterviewsByDay lastUpdateAt={this.state.lastUpdateAt} />
<ExportInterviewData/>
{customMonitoringComponentsArray}
</div>
</div>
Expand Down
0