From f38d633be1d3cc8ef451cf9219acdd205bc38e65 Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Mon, 16 Dec 2024 15:33:15 +0100 Subject: [PATCH 1/7] feat(wip): get context import by query params url - Format params to FieldsMapping interface - Send on upload route frontend - Set fieldsmapping for t_import on upload route backend Reviewed-by: andriacap --- .../geonature/core/imports/routes/imports.py | 13 +++++++++ .../upload-file-step.component.ts | 27 ++++++++++++++----- .../modules/imports/services/data.service.ts | 6 +++-- .../imports/utils/format-to-fieldsmapping.ts | 19 +++++++++++++ 4 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 frontend/src/app/modules/imports/utils/format-to-fieldsmapping.ts diff --git a/backend/geonature/core/imports/routes/imports.py b/backend/geonature/core/imports/routes/imports.py index 92b1dbb819..af3e6d1e63 100644 --- a/backend/geonature/core/imports/routes/imports.py +++ b/backend/geonature/core/imports/routes/imports.py @@ -177,6 +177,17 @@ def upload_file(scope, imprt, destination=None): # destination is set when impr assert destination author = g.current_user f = request.files["file"] + field_to_map_str = request.form.get("fieldsToMap") + if field_to_map_str: + fields_to_map = json.loads(field_to_map_str) + # NOTES: Pas possible d'utiliser le validate value ici + # try: + # FieldMapping.validate_values(fields_to_map) + # except ValueError as e: + # raise BadRequest(*e.args) + else: + fields_to_map = {} + size = get_file_size(f) # value in config file is in Mo max_file_size = current_app.config["IMPORT"]["MAX_FILE_SIZE"] * 1024 * 1024 @@ -203,6 +214,8 @@ def upload_file(scope, imprt, destination=None): # destination is set when impr if not dataset.active: raise Forbidden("Le jeu de données est fermé.") imprt = TImports(destination=destination, dataset=dataset) + if fields_to_map: + imprt.fieldmapping = fields_to_map imprt.authors.append(author) db.session.add(imprt) else: diff --git a/frontend/src/app/modules/imports/components/import_process/upload-file-step/upload-file-step.component.ts b/frontend/src/app/modules/imports/components/import_process/upload-file-step/upload-file-step.component.ts index d34403894e..8210e3aa61 100644 --- a/frontend/src/app/modules/imports/components/import_process/upload-file-step/upload-file-step.component.ts +++ b/frontend/src/app/modules/imports/components/import_process/upload-file-step/upload-file-step.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Observable } from 'rxjs'; +import { combineLatest, Observable } from 'rxjs'; import { ImportDataService } from '../../../services/data.service'; import { CommonService } from '@geonature_common/service/common.service'; import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms'; @@ -8,6 +8,9 @@ import { Step } from '../../../models/enums.model'; import { Destination, Import } from '../../../models/import.model'; import { ImportProcessService } from '../import-process.service'; import { ConfigService } from '@geonature/services/config.service'; +import { switchMap } from 'rxjs/operators'; +import { FieldMappingValues } from '@geonature/modules/imports/models/mapping.model'; +import { formatQueryParams } from '@geonature/modules/imports/utils/format-to-fieldsmapping'; @Component({ selector: 'upload-file-step', @@ -27,6 +30,7 @@ export class UploadFileStepComponent implements OnInit { public maxFileNameLength: number = 255; public acceptedExtensions: string = null; public destination: Destination = null; + public paramsFieldMapping: FieldMappingValues; constructor( private ds: ImportDataService, @@ -57,11 +61,20 @@ export class UploadFileStepComponent implements OnInit { } setupDatasetSelect() { - this.route.parent.params.subscribe((params) => { - this.ds.getDestination(params['destination']).subscribe((dest) => { - this.destination = dest; + combineLatest([ + this.route.parent.queryParams, + this.route.parent?.params || [], + ]) + .pipe( + switchMap(([queryParams, parentParams]) => { + this.paramsFieldMapping = formatQueryParams(queryParams); + const destinationLabel = parentParams['destination']; + return this.ds.getDestination(destinationLabel); + }) + ) + .subscribe((destination) => { + this.destination = destination; }); - }); } isNextStepAvailable() { @@ -91,9 +104,9 @@ export class UploadFileStepComponent implements OnInit { } onSaveData(): Observable { if (this.importData) { - return this.ds.updateFile(this.importData.id_import, this.file); + return this.ds.updateFile(this.importData.id_import, this.file, this.paramsFieldMapping); } else { - return this.ds.addFile(this.uploadForm.get('dataset').value, this.file); + return this.ds.addFile(this.uploadForm.get('dataset').value, this.file, this.paramsFieldMapping); } } onNextStep() { diff --git a/frontend/src/app/modules/imports/services/data.service.ts b/frontend/src/app/modules/imports/services/data.service.ts index 8d799f2c7f..27044d6ee1 100644 --- a/frontend/src/app/modules/imports/services/data.service.ts +++ b/frontend/src/app/modules/imports/services/data.service.ts @@ -55,17 +55,19 @@ export class ImportDataService { return this._http.get(`${this.getUrlApiForADestination()}/imports/${id_import}/`); } - addFile(datasetId: number, file: File): Observable { + addFile(datasetId: number, file: File, fieldsToMap: FieldMappingValues): Observable { let fd = new FormData(); fd.append('file', file, file.name); fd.append('datasetId', String(datasetId)); + fd.append('fieldsToMap', JSON.stringify(fieldsToMap)); const url = `${this.getUrlApiForADestination()}/imports/upload`; return this._http.post(url, fd); } - updateFile(importId: number, file: File): Observable { + updateFile(importId: number, file: File, fieldsToMap: FieldMappingValues): Observable { let fd = new FormData(); fd.append('file', file, file.name); + fd.append('fieldsToMap', JSON.stringify(fieldsToMap)); const url = `${this.getUrlApiForADestination()}/imports/${importId}/upload`; return this._http.put(url, fd); } diff --git a/frontend/src/app/modules/imports/utils/format-to-fieldsmapping.ts b/frontend/src/app/modules/imports/utils/format-to-fieldsmapping.ts new file mode 100644 index 0000000000..9941305406 --- /dev/null +++ b/frontend/src/app/modules/imports/utils/format-to-fieldsmapping.ts @@ -0,0 +1,19 @@ +import { FieldMappingValues } from "../models/mapping.model"; + +export function formatQueryParams(queryParams: { [key: string]: string }): FieldMappingValues { + const formattedParams: FieldMappingValues = {}; + + Object.keys(queryParams).forEach((key) => { + const value = queryParams[key]; + + const parsedValue = !isNaN(Number(value)) ? Number(value) : value; + + formattedParams[key] = { + column_src: key, + default_value: parsedValue, + }; + }); + + return formattedParams; + } + \ No newline at end of file From 546a94c5c7f56c2fc11128c52c96e61c9e9a4f1d Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Mon, 16 Dec 2024 15:35:00 +0100 Subject: [PATCH 2/7] feat(test): test frontend for context params url - Synthese test frontend (ok) - wip : make occhab condition and refact test Reviewed-by: andriacap --- ...ldmapping-context-from-destination-spec.js | 106 ++++++++++++++++++ .../dynamic-form/dynamic-form.component.html | 18 +++ .../fields-mapping-step.component.html | 5 +- .../mapping-theme.component.html | 2 + 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 frontend/cypress/e2e/import/fieldmapping-context-from-destination-spec.js diff --git a/frontend/cypress/e2e/import/fieldmapping-context-from-destination-spec.js b/frontend/cypress/e2e/import/fieldmapping-context-from-destination-spec.js new file mode 100644 index 0000000000..75a51a337b --- /dev/null +++ b/frontend/cypress/e2e/import/fieldmapping-context-from-destination-spec.js @@ -0,0 +1,106 @@ +import { USERS } from './constants/users'; +import { TIMEOUT_WAIT, VIEWPORTS } from './constants/common'; +import { FILES } from './constants/files'; + +const USER = USERS[0]; +const VIEWPORT = VIEWPORTS[0]; + +function testQueryParamField(dataQa, paramName, expectedValue, fieldType) { + if (fieldType === 'textarea' || fieldType === 'text' || fieldType === 'number') { + cy.get(dataQa) + .find(`[data-qa^="field-${fieldType}-${paramName}_default_value_"]`) + .should('have.value', expectedValue); + } +} + +const paramsByDestination = [ + { + destination: 'synthese', + queryParams: [ + { + paramsName: 'nom_cite', + paramsValue: 'test_nomcite', + fieldType: 'textarea', + isTypeComp: false, + expectedValue: 'test_nomcite', + }, + { + paramsName: 'altitude_max', + paramsValue: 10, + fieldType: 'number', + isTypeComp: false, + expectedValue: 10, + }, + { + paramsName: 'date_min', + paramsValue: '2024-12-12', + fieldParentType: 'field-date', + fieldType: 'date', + isTypeComp: true, + expectedValue: '12/12/2024', + }, + { + paramsName: 'id_nomenclature_geo_object_nature', + paramsValue: 'Inventoriel', + fieldParentType: 'field-nomenclature', + fieldType: 'nomenclature', + isTypeComp: true, + expectedValue: 'Inventoriel', + }, + ], + }, + // { + // destination: 'occhab', + // queryParams: [ + // { paramsName: 'nom_cite', paramsValue: 'test_nomcite' }, + // { paramsName: 'date_min', paramsValue: '2024-12-12' }, + // ], + // }, +]; + +describe('Import - Upload step', () => { + context(`viewport: ${VIEWPORT.width}x${VIEWPORT.height}`, () => { + beforeEach(() => { + cy.viewport(VIEWPORT.width, VIEWPORT.height); + cy.geonatureLogin(USER.login.username, USER.login.password); + cy.wait(TIMEOUT_WAIT); + }); + + paramsByDestination.forEach(({ destination, queryParams }) => { + it(`Should handle for destination: ${destination}`, () => { + const urlParams = queryParams + .map((param) => `${param.paramsName}=${param.paramsValue}`) + .join('&'); + cy.visit(`/#/import/${destination}/process/upload?${urlParams}`); + + cy.pickDataset(USER.dataset); + cy.loadImportFile(FILES.synthese.valid.fixture); + cy.configureImportFile(); + + queryParams.forEach(({ paramsName, paramsValue, fieldType, isTypeComp, expectedValue }) => { + let dataQa = `[data-qa="import-fieldmapping-theme-${paramsName}"]`; + + // Récupérer et vérifier la valeur en fonction du type de champ + if (!isTypeComp) { + testQueryParamField(dataQa, paramsName, expectedValue, fieldType); + } else if (fieldType === 'date' || fieldType === 'nomenclature') { + // Si c'est un champ de type 'date' ou 'nomenclature' + dataQa = `[data-qa="field-${fieldType}-${paramsName}_default_value"]`; + cy.get(dataQa) + .find(`input`) + .each(($el) => cy.wrap($el).scrollIntoView().should('be.visible')); + + if (fieldType === 'date') { + cy.get(dataQa).find('[data-qa="input-date"]').should('have.value', expectedValue); + } else if (fieldType === 'nomenclature') { + cy.get(`${dataQa} .ng-value-container .ng-value-label`).should( + 'have.text', + expectedValue + ); + } + } + }); + }); + }); + }); +}); diff --git a/frontend/src/app/GN2CommonModule/form/dynamic-form/dynamic-form.component.html b/frontend/src/app/GN2CommonModule/form/dynamic-form/dynamic-form.component.html index f023e1aa0d..6063bbb6f3 100644 --- a/frontend/src/app/GN2CommonModule/form/dynamic-form/dynamic-form.component.html +++ b/frontend/src/app/GN2CommonModule/form/dynamic-form/dynamic-form.component.html @@ -50,6 +50,7 @@ [formControl]="form.get(formDefComp['attribut_name'])" id="{{ formDefComp['attribut_name'] }}_{{ rand }}" type="text" + data-qa="field-text-{{ formDefComp['attribut_name'] }}_{{ rand }}" />
@@ -93,6 +97,7 @@ class="form-control form-control-sm" id="{{ formDefComp['attribut_name'] }}_{{ rand }}" [formControl]="form.get(formDefComp['attribut_name'])" + [attr.data-qa]="'field-select-' + '-' + formDefComp['attribut_name'] + '_' + rand" >
-
+