From 915e2882832b47a9758f8e1250051137acf3bc75 Mon Sep 17 00:00:00 2001 From: Thibaut Dusanter Date: Wed, 21 Aug 2024 17:44:15 +0200 Subject: [PATCH] feat(indicateurs): possibilite de telechargement du modele, correction d'un bug de telechargement de l'excel --- backend/.env.default | 1 + backend/README.md | 3 +- backend/src/common/services/gcloud.helper.ts | 21 +++++ .../controllers/trajectoires.controller.ts | 6 ++ .../service/trajectoires.service.ts | 88 +++++++++++++++---- backend/src/main.ts | 12 +-- .../spreadsheets/services/sheet.service.ts | 13 +++ 7 files changed, 115 insertions(+), 29 deletions(-) create mode 100644 backend/src/common/services/gcloud.helper.ts diff --git a/backend/.env.default b/backend/.env.default index be2cb3de304..4efc7d4ead7 100644 --- a/backend/.env.default +++ b/backend/.env.default @@ -1,3 +1,4 @@ TRAJECTOIRE_SNBC_SHEET_ID= +TRAJECTOIRE_SNBC_XLSX_ID= TRAJECTOIRE_SNBC_RESULT_FOLDER_ID= GCLOUD_SERVICE_ACCOUNT_KEY= \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index 6f1161b2e15..30a693eaed4 100644 --- a/backend/README.md +++ b/backend/README.md @@ -15,12 +15,13 @@ $ npm install Les variables d'environnement suivantes doivent être définies dans un fichier .env (voir le fichier [.env.default](.env.default)): - **TRAJECTOIRE_SNBC_SHEET_ID**: Identifiant du Google spreadsheet utilisé pour le calcul de la trajectoire. +- **TRAJECTOIRE_SNBC_XLSX_ID**: Identifiant du Xlsx original utilisé pour le calcul de la trajectoire (stocké sur le drive) - **TRAJECTOIRE_SNBC_RESULT_FOLDER_ID**: Identifiant du dossier Google Drive dans lequel les spreadsheets des trajectoires calculées doivent être sauvés (un fichier par EPCI). A noter qu'il existe **un dossier par environnement** (dev, preprod, prod). - **GCLOUD_SERVICE_ACCOUNT_KEY**: contenu au format json du fichier de clé de compte de service permettant l'utilisation des api Google Drive et Google Spreadsheet. Ces variables d'environnement sont définies: -- pour **TRAJECTOIRE_SNBC_SHEET_ID** et **TRAJECTOIRE_SNBC_RESULT_FOLDER_ID** dans les [variables d'environnement de Github](https://github.com/incubateur-ademe/territoires-en-transitions/settings/environments/1431973268/edit) utilisées pour configurer le [déploiement Koyeb](https://app.koyeb.com/services/c7001069-ca11-4fd7-86c6-7feb45b9b68d/settings). Les identifiants peuvent également être récupérés à partir du drive de `territoiresentransitions`. +- pour **TRAJECTOIRE_SNBC_SHEET_ID**, **TRAJECTOIRE_SNBC_XLSX_ID** et **TRAJECTOIRE_SNBC_RESULT_FOLDER_ID** dans les [variables d'environnement de Github](https://github.com/incubateur-ademe/territoires-en-transitions/settings/environments/1431973268/edit) utilisées pour configurer le [déploiement Koyeb](https://app.koyeb.com/services/c7001069-ca11-4fd7-86c6-7feb45b9b68d/settings). Les identifiants peuvent également être récupérés à partir du drive de `territoiresentransitions`. - pour **GCLOUD_SERVICE_ACCOUNT_KEY** dans le [gestionnaire de secret de Koyeb](https://app.koyeb.com/secrets) ## Scripts disponibles diff --git a/backend/src/common/services/gcloud.helper.ts b/backend/src/common/services/gcloud.helper.ts new file mode 100644 index 00000000000..b7585d25de8 --- /dev/null +++ b/backend/src/common/services/gcloud.helper.ts @@ -0,0 +1,21 @@ +import { Logger } from '@nestjs/common'; +import * as fs from 'fs'; + +const logger = new Logger('gcloud.helper'); + +export const initApplicationCredentials = () => { + if ( + process.env.GCLOUD_SERVICE_ACCOUNT_KEY && + !process.env.GOOGLE_APPLICATION_CREDENTIALS + ) { + const serviceAccountFile = `${__dirname}/keyfile.json`; + logger.log( + `Writing Google Cloud credentials to file: ${serviceAccountFile}`, + ); + fs.writeFileSync( + serviceAccountFile, + process.env.GCLOUD_SERVICE_ACCOUNT_KEY, + ); + process.env.GOOGLE_APPLICATION_CREDENTIALS = serviceAccountFile; + } +}; diff --git a/backend/src/indicateurs/controllers/trajectoires.controller.ts b/backend/src/indicateurs/controllers/trajectoires.controller.ts index f2372e6ecdb..79c2452efd2 100644 --- a/backend/src/indicateurs/controllers/trajectoires.controller.ts +++ b/backend/src/indicateurs/controllers/trajectoires.controller.ts @@ -29,6 +29,12 @@ export class TrajectoiresController { return response; } + @Get('snbc/modele') + downloadModeleSnbc(@Res() res: Response, @Next() next: NextFunction) { + this.logger.log(`Téléchargement du modele de trajectoire SNBC`); + this.trajectoiresService.downloadModeleTrajectoireSnbc(res, next); + } + @Get('snbc/telechargement') downloadDataSnbc( @Query() request: CollectiviteRequest, diff --git a/backend/src/indicateurs/service/trajectoires.service.ts b/backend/src/indicateurs/service/trajectoires.service.ts index 68f7685bbb6..a97696dc6a8 100644 --- a/backend/src/indicateurs/service/trajectoires.service.ts +++ b/backend/src/indicateurs/service/trajectoires.service.ts @@ -123,21 +123,63 @@ export default class TrajectoiresService { private readonly sheetService: SheetService, ) {} + getIdentifiantSpreadsheetCalcul() { + return process.env.TRAJECTOIRE_SNBC_SHEET_ID!; + } + + getIdentifiantXlsxCalcul() { + return process.env.TRAJECTOIRE_SNBC_XLSX_ID!; + } + getIdentifiantDossierResultat() { - return process.env.TRAJECTOIRE_SNBC_RESULT_FOLDER_ID; + return process.env.TRAJECTOIRE_SNBC_RESULT_FOLDER_ID!; } getNomFichierTrajectoire(epci: EpciType) { return `Trajectoire SNBC - ${epci.siren} - ${epci.nom}`; } + async downloadModeleTrajectoireSnbc(res: Response, next: NextFunction) { + try { + if (!this.getIdentifiantXlsxCalcul()) { + throw new InternalServerErrorException( + "L'identifiant du Xlsx pour le calcul des trajectoires SNBC est manquant", + ); + } + + const xlsxBuffer = await this.sheetService.getFileData( + this.getIdentifiantXlsxCalcul(), + ); + const nomFichier = await this.sheetService.getFileName( + this.getIdentifiantXlsxCalcul(), + ); + // Set the output file name. + res.attachment(nomFichier); + + // Send the workbook. + res.send(xlsxBuffer); + } catch (error) { + next(error); + } + } + async downloadTrajectoireSnbc( request: CalculTrajectoireRequest, res: Response, next: NextFunction, ) { try { - const resultatVerification = await this.verificationDonneesSnbc(request); + if (!this.getIdentifiantXlsxCalcul()) { + throw new InternalServerErrorException( + "L'identifiant du Xlsx pour le calcul des trajectoires SNBC est manquant", + ); + } + + const resultatVerification = await this.verificationDonneesSnbc( + request, + undefined, + true, + ); if ( resultatVerification.status === @@ -166,7 +208,7 @@ export default class TrajectoiresService { const nomFichier = this.getNomFichierTrajectoire(epci); const xlsxBuffer = await this.sheetService.getFileData( - '14dZbAf8yRqhfqKNnvXUTMgTT34dqkc32', + this.getIdentifiantXlsxCalcul(), ); // Utilisation de xlsx-template car: @@ -244,6 +286,18 @@ export default class TrajectoiresService { let mode: CalculTrajectoireResultatMode = CalculTrajectoireResultatMode.NOUVEAU_SPREADSHEET; + if (!this.getIdentifiantSpreadsheetCalcul()) { + throw new InternalServerErrorException( + "L'identifiant de la feuille de calcul pour les trajectoires SNBC est manquante", + ); + } + + if (!this.getIdentifiantDossierResultat()) { + throw new InternalServerErrorException( + "L'identifiant du dossier pour le stockage des trajectoires SNBC calculées est manquant", + ); + } + // Création de la source métadonnée SNBC si elle n'existe pas let indicateurSourceMetadonnee = await this.indicateurSourcesService.getIndicateurSourceMetadonnee( @@ -348,16 +402,10 @@ export default class TrajectoiresService { } epci = resultatVerification.epci; - if (!process.env.TRAJECTOIRE_SNBC_SHEET_ID) { - throw new InternalServerErrorException( - "L'identifiant de la feuille de calcul pour les trajectoires SNBC est manquante", - ); - } - const nomFichier = this.getNomFichierTrajectoire(epci); let trajectoireCalculSheetId = await this.sheetService.getFileIdByName( nomFichier, - process.env.TRAJECTOIRE_SNBC_RESULT_FOLDER_ID, + this.getIdentifiantDossierResultat(), ); if ( trajectoireCalculSheetId && @@ -375,14 +423,12 @@ export default class TrajectoiresService { await this.sheetService.deleteFile(trajectoireCalculSheetId); } trajectoireCalculSheetId = await this.sheetService.copyFile( - process.env.TRAJECTOIRE_SNBC_SHEET_ID, + this.getIdentifiantSpreadsheetCalcul(), nomFichier, - process.env.TRAJECTOIRE_SNBC_RESULT_FOLDER_ID - ? [process.env.TRAJECTOIRE_SNBC_RESULT_FOLDER_ID] - : undefined, + [this.getIdentifiantDossierResultat()], ); this.logger.log( - `Fichier de trajectoire SNBC créé à partir du master ${process.env.TRAJECTOIRE_SNBC_SHEET_ID} avec l'identifiant ${trajectoireCalculSheetId}`, + `Fichier de trajectoire SNBC créé à partir du master ${this.getIdentifiantSpreadsheetCalcul()} avec l'identifiant ${trajectoireCalculSheetId}`, ); } @@ -643,9 +689,9 @@ export default class TrajectoiresService { identifiantIndicateurValeur2015.indicateur_valeur.resultat !== undefined // 0 est une valeur valide ) { - /*console.log( - `${identifiant}: ${indicateurValeur.indicateur_valeur.resultat} ${indicateurValeur.indicateur_definition?.unite}`, - );*/ + console.log( + `${identifiant}: ${identifiantIndicateurValeur2015.indicateur_valeur.resultat} ${identifiantIndicateurValeur2015.indicateur_definition?.unite}`, + ); // Si il n'y a pas déjà eu une valeur manquante qui a placé la valeur à null if (valeurARemplir.valeur !== null) { @@ -669,6 +715,12 @@ export default class TrajectoiresService { } } } else { + identifiantIndicateurValeurs.forEach((v) => { + console.log( + `${identifiant}: ${v.indicateur_valeur.resultat} ${v.indicateur_definition?.unite} (${v.indicateur_valeur.date_valeur})`, + ); + }); + const interpolationResultat = this.getInterpolationValeur( identifiantIndicateurValeurs.map((v) => v.indicateur_valeur), ); diff --git a/backend/src/main.ts b/backend/src/main.ts index f976b515591..81bf5aaf62b 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -7,8 +7,8 @@ import { } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import * as Sentry from '@sentry/nestjs'; -import * as fs from 'fs'; import { AppModule } from './app.module'; +import { initApplicationCredentials } from './common/services/gcloud.helper'; import './common/services/sentry.service'; import { SENTRY_DSN } from './common/services/sentry.service'; import { TrpcRouter } from './trpc.router'; @@ -18,15 +18,7 @@ const port = process.env.PORT || 8080; logger.log(`Launching NestJS app on port ${port}`); async function bootstrap() { - if (process.env.GCLOUD_SERVICE_ACCOUNT_KEY) { - const serviceAccountFile = `${__dirname}/keyfile.json`; - logger.log('Writing Google Cloud credentials to file:', serviceAccountFile); - fs.writeFileSync( - serviceAccountFile, - process.env.GCLOUD_SERVICE_ACCOUNT_KEY, - ); - process.env.GOOGLE_APPLICATION_CREDENTIALS = serviceAccountFile; - } + initApplicationCredentials(); const app = await NestFactory.create(AppModule); const { httpAdapter } = app.get(HttpAdapterHost); diff --git a/backend/src/spreadsheets/services/sheet.service.ts b/backend/src/spreadsheets/services/sheet.service.ts index da324fb824d..639231726b3 100644 --- a/backend/src/spreadsheets/services/sheet.service.ts +++ b/backend/src/spreadsheets/services/sheet.service.ts @@ -4,6 +4,7 @@ import { Response } from 'express'; import * as gaxios from 'gaxios'; import * as auth from 'google-auth-library'; import { drive_v3, google, sheets_v4 } from 'googleapis'; +import { initApplicationCredentials } from '../../common/services/gcloud.helper'; import { SheetValueInputOption, SheetValueRenderOption, @@ -35,6 +36,7 @@ export default class SheetService { | auth.Impersonated > { if (!this.authClient) { + initApplicationCredentials(); this.authClient = await google.auth.getClient({ scopes: [ 'https://www.googleapis.com/auth/spreadsheets', @@ -136,6 +138,17 @@ export default class SheetService { this.logger.log(`Spreadsheet ${fileId} correctement supprimé.`); } + async getFileName(fileId: string): Promise { + const authClient = await this.getAuthClient(); + const getOptions: drive_v3.Params$Resource$Files$Get = { + auth: authClient, + fileId: fileId, + fields: 'name', + }; + const res = await drive.files.get(getOptions); + return res.data.name!; + } + async getFileData(fileId: string): Promise { const authClient = await this.getAuthClient(); const getOptions: drive_v3.Params$Resource$Files$Get = {