From df26ec53ce319a6cc555e95fb7258044c6fae841 Mon Sep 17 00:00:00 2001 From: lucile varloteaux <lucile.varloteaux@inrae.fr> Date: Tue, 14 May 2024 11:08:05 +0200 Subject: [PATCH 1/4] les colonnes sont toujours optionnal et jamais mandatory --- .../oresing/domain/data/deposit/context/DataImporterContext.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/fr/inra/oresing/domain/data/deposit/context/DataImporterContext.java b/src/main/java/fr/inra/oresing/domain/data/deposit/context/DataImporterContext.java index ab45f4626..68ebe2202 100644 --- a/src/main/java/fr/inra/oresing/domain/data/deposit/context/DataImporterContext.java +++ b/src/main/java/fr/inra/oresing/domain/data/deposit/context/DataImporterContext.java @@ -222,7 +222,6 @@ public class DataImporterContext { public ImmutableSet<String> getMandatoryHeaders() { return getExpectedColumnsPerHeaders().values().stream() - .filter(Column::isMandatory) .map(Column::getExpectedHeader) .collect(ImmutableSet.toImmutableSet()); } -- GitLab From b0e02a930befbe479efc529b0b2633e2a9eb2530 Mon Sep 17 00:00:00 2001 From: lucile varloteaux <lucile.varloteaux@inrae.fr> Date: Tue, 14 May 2024 11:47:44 +0200 Subject: [PATCH 2/4] ajout de traduction de message : badAuthorizationScopeForRepository et timeRangeOutOfInterval --- .../domain/data/deposit/DataImporter.java | 4 ++-- .../configuration/ConfigurationException.java | 4 ++-- .../fr/inra/oresing/rest/OreSiService.java | 4 ++-- ui/src/locales/en.json | 10 ++++++-- ui/src/locales/fr.json | 12 +++++++--- ui/src/services/ErrorsService.js | 2 ++ ui/src/views/data/DataVersioningView.vue | 24 ++++++++----------- 7 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/main/java/fr/inra/oresing/domain/data/deposit/DataImporter.java b/src/main/java/fr/inra/oresing/domain/data/deposit/DataImporter.java index 4af2f760e..848297c08 100644 --- a/src/main/java/fr/inra/oresing/domain/data/deposit/DataImporter.java +++ b/src/main/java/fr/inra/oresing/domain/data/deposit/DataImporter.java @@ -459,7 +459,7 @@ public class DataImporter { builder.put("to", DISPLAY_DATE_FORMATTER_DDMMYYYY.format(to)); } if (!dateTimeRange.getRange().encloses(timeScope.getRange())) { - errors.add(new CsvRowValidationCheckResult(DefaultValidationCheckResult.error("timerangeoutofinterval", builder.build(), null), rowNumber)); + errors.add(new CsvRowValidationCheckResult(DefaultValidationCheckResult.error("timeRangeOutOfInterval", builder.build(), null), rowNumber)); } } @@ -470,7 +470,7 @@ public class DataImporter { } binaryFileDataset.getRequiredAuthorizations().entrySet().forEach(entry -> { if (!requiredAuthorizations.get(entry.getKey()).equals(entry.getValue())) { - errors.add(new CsvRowValidationCheckResult(DefaultValidationCheckResult.error("badauthorizationscopeforrepository", ImmutableMap.of("authorization", entry.getKey(), "expectedValue", entry.getValue(), "givenValue", requiredAuthorizations.get(entry.getKey())), null), rowNumber)); + errors.add(new CsvRowValidationCheckResult(DefaultValidationCheckResult.error("badAuthorizationScopeForRepository", ImmutableMap.of("authorization", entry.getKey(), "expectedValue", entry.getValue(), "givenValue", requiredAuthorizations.get(entry.getKey())), null), rowNumber)); } }); diff --git a/src/main/java/fr/inra/oresing/domain/exceptions/configuration/ConfigurationException.java b/src/main/java/fr/inra/oresing/domain/exceptions/configuration/ConfigurationException.java index de15187c3..c5f6b4ef3 100644 --- a/src/main/java/fr/inra/oresing/domain/exceptions/configuration/ConfigurationException.java +++ b/src/main/java/fr/inra/oresing/domain/exceptions/configuration/ConfigurationException.java @@ -16,7 +16,7 @@ public enum ConfigurationException { authorizationScopeComponentWrongChecker, //Le composant <code>{component}</code> de la variable <code>{variable}</code> ne peut pas être utilisé comme portant l’information temporelle car ce n’est pas une donnée déclarée comme <code>{expectedChecker}</code>, authorizationComponentKeyMissingComponent, //Il faut indiquer le composant de la variable <code>{variable}</code> dans laquelle on recueille les informations spatiales à laquelle rattacher la donnée pour le gestion des droits jeu de données <code>{dataName}</code> pour l'autorisation <code>{authorizationName}</code>. Les valeurs possibles sont : <code>{knownComponents}</code>, authorizationComponentKeyunknownComponent, //<code>{component}</code> ne fait pas partie des composants connus pour la variable <code>{variable}</code>. Les composants connus sont : <code>{knownComponents}</code>, - badauthorizationscopeforrepository, //L'autorisation <code>{submissionScope}</code>) doit être <code>{expectedValue}/code> et non <code>{givenValue}</code>, + badAuthorizationScopeForRepository, //L'autorisation <code>{submissionScope}</code>) doit être <code>{expectedValue}/code> et non <code>{givenValue}</code>, characterNotAcceptInName, //Il y a des caractère non accepté dans le nom de l'application : <code>{name}</code>, checkerExpressionReturnedFalse, //La contrainte suivante n'est pas respectée : <code>{expression}</code>, csvBoundToUnknownVariable, //Dans le format CSV, l’en-tête <code>{header}</code> est lié à la variable <code>{variable}</code> qui n’est pas connue. Les variables connues sont : <code>{variables}</code>, @@ -115,7 +115,7 @@ public enum ConfigurationException { timeScopeComponentWrongChecker, //Le composant <code>{component}</code> de la variable <code>{variable}</code> ne peut pas être utilisé comme portant l’information temporelle car ce n’est pas une donnée déclarée comme <code>{expectedChecker}</code>, timeComponentKeyMissingComponent, //Il faut indiquer le composant de la variable <code>{variable}</code> dans laquelle on recueille la période de temps à laquelle rattacher la donnée pour le gestion des droits jeu de données <code>{dataName}</code>. Valeurs possibles <code>{knownComponents}</code>, timeComponentKeyunknownComponent, //<code>{component}</code> ne fait pas partie des composants connus pour la variable <code>{variable}</code>. Composants connus : <code>{knownComponents}</code>, - timerangeoutofinterval, //La date <code>{value}</code> est incompatible avec l'intervale de dates du dépôt. Elle doit être comprise entre <code>{from}</code> et <code>{to}</code>., + timeRangeOutOfInterval, //La date <code>{value}</code> est incompatible avec l'intervale de dates du dépôt. Elle doit être comprise entre <code>{from}</code> et <code>{to}</code>., tooBigRowLineForConstantDescription, //Dans la section format->constant du dataName {dataName} le numéro de ligne doit être inférieur à celui de la ligne de données., tooLittleRowLineForConstantDescription, //Dans la section format->constant du dataName {dataName} le numéro de ligne doit être positif., unDeclaredValueForChart, //Pour visualiser correctement le graphe de la variable <code> {variable} </code> du type de données <code> {dataName} </code>, il est nécessaire de déclarer la valeur.<br /> La valeur doit être parmi : <code>{components}</code>, diff --git a/src/main/java/fr/inra/oresing/rest/OreSiService.java b/src/main/java/fr/inra/oresing/rest/OreSiService.java index 208973b4f..be6f3a0c1 100644 --- a/src/main/java/fr/inra/oresing/rest/OreSiService.java +++ b/src/main/java/fr/inra/oresing/rest/OreSiService.java @@ -128,7 +128,7 @@ public class OreSiService { final LocalDateTime to = LocalDate.from(LocalDateTimeRange.DATE_TIME_FORMATTER.parse(binaryFileDataset.getTo())).plusDays(1).atStartOfDay(); if (!LocalDateTimeRange.between(from, to).getRange().encloses(timeScope.getRange())) { errors.add(new CsvRowValidationCheckResult(DefaultValidationCheckResult.error( - "timerangeoutofinterval", + "timeRangeOutOfInterval", ImmutableMap.of("from", LocalDateTimeRange.DATE_FORMATTER_DDMMYYYY.format(from), "to", LocalDateTimeRange.DATE_FORMATTER_DDMMYYYY.format(to), "value", LocalDateTimeRange.DATE_FORMATTER_DDMMYYYY.format(timeScope.getRange().lowerEndpoint())) , null ), rowNumber) @@ -144,7 +144,7 @@ public class OreSiService { binaryFileDataset.getRequiredAuthorizations().entrySet().forEach(entry -> { if (!requiredAuthorizations.get(entry.getKey()).equals(entry.getValue())) { errors.add(new CsvRowValidationCheckResult(DefaultValidationCheckResult.error( - "badauthorizationscopeforrepository", + "badAuthorizationScopeForRepository", ImmutableMap.of("submissionScope", entry.getKey(), "expectedValue", entry.getValue(), "givenValue", requiredAuthorizations.get(entry.getKey())) , null ), rowNumber) diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json index ba1f5f665..1cbbd2b54 100644 --- a/ui/src/locales/en.json +++ b/ui/src/locales/en.json @@ -301,7 +301,7 @@ "authorizationScopeComponentWrongChecker": "The <code> {component} </code> component of the <code> {variable} </code> variable cannot be used as carrying time information because it is not declared data like <code> {expectedChecker} </code> ", "authorizationComponentKeyMissingComponent": "You must indicate the component of the variable <code> {variable} </code> in which we collect the spatial information to which to attach the data for the management of rights dataset <code> {dataType} < / code> for authorization <code> {authorizationName} </code>. Possible values ​​<code> {knownComponents} </code> ", "authorizationComponentKeyunknownComponent": "<code> {component} </code> is not one of the known components for the variable <code> {variable} </code>. Known components: <code> {knownComponents} </code>", - "badauthorizationscopeforrepository": "Authorization <code> {authorization} </code>) must be <code> {expectedValue} / code> and not <code> {givenValue} </code>", + "badAuthorizationScopeForRepository": "Authorization <code> {authorization} </code>) must be <code> {expectedValue} / code> and not <code> {givenValue} </code>", "characterNotAcceptInName": "There are characters not accepted in the name of the application: <code>{name}</code>", "checkerExpressionReturnedFalse": "The following constraint is not respected: <code> {expression} </code>", "csvBoundToUnknownVariable": "In the CSV format, header <code>{header}</code> is bound to unknown variable <code>{variable}</code>. Known variables: <code>{variables}</code>", @@ -401,7 +401,7 @@ "timeScopeComponentWrongChecker": "The component <code>{component}</code> of variable <code>{variable}</code> can't be used for carrying time information because it's not declared as : <code>{expectedChecker}</code>", "timeComponentKeyMissingComponent": "Mandatory indication of the component of : <code>{variable}</code> in which we collect the time period for which we need to attach the data for rights management of data type : <code>{dataType}</code>. <br>Accepted values : <code>{knownComponents}</code>", "timeComponentKeyunknownComponent": "<code>{component}</code> doesn't belong to any of known variables : <code>{variable}</code>. <br>Known components : <code>{knownComponents}</code>", - "timerangeoutofinterval": "The date <code> {value} </code> is incompatible with the date range of the deposit. It must be between <code> {from} </code> and <code> {to} </code>. ", + "timeRangeOutOfInterval": "The date <code> {value} </code> is incompatible with the date range of the deposit. It must be between <code> {from} </code> and <code> {to} </code>. ", "tooBigRowLineForConstantDescription": "In the format->constant section of dataType <code>{dataType}</code> the row number must be less than the data row.", "tooLittleRowLineForConstantDescription": "In the format->constant section of dataType <code>{dataType}</code> the row number must be positive.", "unDeclaredValueForChart": "In the chart description of variable <code> {variable} </code> of data type <code> {dataType} </code>, the value is not declared.<br />Expected values ​​<code>{components}</code>", @@ -433,6 +433,9 @@ "exception": "The translation string for the following error is missing. Please verify that this is not an indentation problem before contacting the administrators. " }, "errors-csv": { + "badAuthorizationScopeForRepository" : { + "message": "Authorization <code> {authorization} </code>) must be <code> {expectedValue} / code> and not <code> {givenValue} </code>" + }, "badMaxIntervalFloatWithComponent": { "message": "For column: <code> {target} </code> the value <code> {value} </code> must be a decimal number and <code>{type}</code> for <code>{bound}</code>." }, @@ -469,6 +472,9 @@ "invalidReferenceWithComponent": { "message": "For column: <code> {target} </code> the value <code> {value} </code> does not exist in the reference <code> {refType} </code>. Values expected <code> {referenceValues} </code>. " }, + "timeRangeOutOfInterval": { + "message": "The date value: <code>{value}</code> must be between <code>{from}</code> and <code>{to}</code>." + }, "unexpectedHeaderColumn": { "message": "" }, diff --git a/ui/src/locales/fr.json b/ui/src/locales/fr.json index 0c4d64095..b0e090dc7 100644 --- a/ui/src/locales/fr.json +++ b/ui/src/locales/fr.json @@ -301,7 +301,7 @@ "authorizationScopeComponentWrongChecker": "Le composant <code>{component}</code> de la variable <code>{variable}</code> ne peut pas être utilisé comme portant l’information temporelle car ce n’est pas une donnée déclarée comme <code>{expectedChecker}</code>", "authorizationComponentKeyMissingComponent": "Il faut indiquer le composant de la variable <code>{variable}</code> dans laquelle on recueille les informations spatiales à laquelle rattacher la donnée pour le gestion des droits jeu de données <code>{dataType}</code> pour l'autorisation <code>{authorizationName}</code>. Les valeurs possibles sont : <code>{knownComponents}</code>", "authorizationComponentKeyunknownComponent": "<code>{component}</code> ne fait pas partie des composants connus pour la variable <code>{variable}</code>. Les composants connus sont : <code>{knownComponents}</code>", - "badauthorizationscopeforrepository": "L'autorisation <code>{authorization}</code>) doit être <code>{expectedValue}/code> et non <code>{givenValue}</code>", + "badAuthorizationScopeForRepository": "L'autorisation <code>{authorization}</code>) doit être <code>{expectedValue}/code> et non <code>{givenValue}</code>", "characterNotAcceptInName": "Il y a des caractère non accepté dans le nom de l'application : <code>{name}</code>", "checkerExpressionReturnedFalse": "La contrainte suivante n'est pas respectée : <code>{expression}</code>", "csvBoundToUnknownVariable": "Dans le format CSV, l’en-tête <code>{header}</code> est lié à la variable <code>{variable}</code> qui n’est pas connue. Les variables connues sont : <code>{variables}</code>", @@ -401,7 +401,7 @@ "timeScopeComponentWrongChecker": "Le composant <code>{component}</code> de la variable <code>{variable}</code> ne peut pas être utilisé comme portant l’information temporelle car ce n’est pas une donnée déclarée comme <code>{expectedChecker}</code>", "timeComponentKeyMissingComponent": "Il faut indiquer le composant de la variable <code>{variable}</code> dans laquelle on recueille la période de temps à laquelle rattacher la donnée pour le gestion des droits jeu de données <code>{dataType}</code>. Valeurs possibles <code>{knownComponents}</code>", "timeComponentKeyunknownComponent": "<code>{component}</code> ne fait pas partie des composants connus pour la variable <code>{variable}</code>. Composants connus : <code>{knownComponents}</code>", - "timerangeoutofinterval": "La date <code>{value}</code> est incompatible avec l'intervalle de dates du dépôt. Elle doit être comprise entre <code>{from}</code> et <code>{to}</code>.", + "timeRangeOutOfInterval": "La date <code>{value}</code> est incompatible avec l'intervalle de dates du dépôt. Elle doit être comprise entre <code>{from}</code> et <code>{to}</code>.", "tooBigRowLineForConstantDescription": "Dans la section format->constant du dataType {dataType} le numéro de ligne doit être inférieur à celui de la ligne de données.", "tooLittleRowLineForConstantDescription": "Dans la section format->constant du dataType {dataType} le numéro de ligne doit être positif.", "unDeclaredValueForChart": "Pour visualiser correctement le graphe de la variable <code> {variable} </code> du type de données <code> {dataType} </code>, il est nécessaire de déclarer la valeur.<br /> La valeur doit être parmi : <code>{components}</code>", @@ -429,9 +429,12 @@ "variableInMultipleDataGroup": "La variable <code>{variable}</code> du dataType <code>{dataType}</code> est déclarée dans plusieurs groupes de données, elle ne doit être présente que dans un seul groupe" }, "errors": { - "exception": "Il manque la chaine de traduction pour l'erreur suivante. Merci de vérifier que ce n'est pas un problème d'indentation avant de contacter les administrateurs. " + "exception": "Il manque la chaine de traduction pour l'erreur suivante. Merci de vérifier que ce n'est pas un problème d'indentation ou de fichier avant de contacter les administrateurs. " }, "errors-csv": { + "badAuthorizationScopeForRepository" : { + "message": "L'autorisation <code>{authorization}</code>) doit être <code>{expectedValue}/code> et non <code>{givenValue}</code>" + }, "badMaxIntervalFloatWithComponent": { "message": "Pour la colonne : <code>{component}</code> la valeur <code>{value}</code> doit être un nombre décimal et inférieur à la valeur limite qui est de <code>{bound}</code>." }, @@ -468,6 +471,9 @@ "invalidReferenceWithComponent": { "message": "Pour la colonne : <code>{target}</code> la valeur <code>{value}</code> n'existe pas dans la référence <code>{refType}</code>. Valeurs attendues <code>{referenceValues}</code>." }, + "timeRangeOutOfInterval": { + "message": "La valeur de la date : <code>{value}</code> doit être comprise entre <code>{from}</code> et <code>{to}</code>." + }, "unexpectedHeaderColumn": { "message": "En-tête de colonne inattendu. En-tête attendu : <code>{expectedHeaderColumn}</code> <br />En-tête actuel : <code>{actualHeaderColumn}</code>" }, diff --git a/ui/src/services/ErrorsService.js b/ui/src/services/ErrorsService.js index 19299f77d..c74845f81 100644 --- a/ui/src/services/ErrorsService.js +++ b/ui/src/services/ErrorsService.js @@ -52,6 +52,7 @@ const ERRORS = { invalidConfigurationFile:(params) => i18n.t("errors-yaml.invalidConfigurationFile.message", params), //erreur csv + badAuthorizationScopeForRepository:(params) => i18n.t("errors-csv.badAuthorizationScopeForRepository.message", params), badMaxIntervalFloatWithComponent:(params) => i18n.t("errors-csv.badMaxIntervalFloatWithComponent.message", params), badMinIntervalFloatWithComponent:(params) => i18n.t("errors-csv.badMinIntervalFloatWithComponent.message", params), duplicatedHeaders:(params) => i18n.t("errors-csv.duplicatedHeaders.message", params), @@ -62,6 +63,7 @@ const ERRORS = { headerColumnPatternNotMatching:(params) => i18n.t("errors-csv.headerColumnPatternNotMatching.message", params), invalidHeaders:(params) => i18n.t("errors-csv.invalidHeaders.message", params), invalidReferenceWithComponent:(params) => i18n.t("errors-csv.invalidReferenceWithComponent.message", params), + timeRangeOutOfInterval:(params) => i18n.t("errors-csv.timeRangeOutOfInterval.message", params), unexpectedHeaderColumn:(params) => i18n.t("errors-csv.unexpectedHeaderColumn.message", params), unexpectedHeaderColumnsInList:(params) => i18n.t("errors-csv.unexpectedHeaderColumnsInList.message", params), unexpectedTokenCount:(params) => i18n.t("errors-csv.unexpectedTokenCount.message", params), diff --git a/ui/src/views/data/DataVersioningView.vue b/ui/src/views/data/DataVersioningView.vue index 6ee002895..6eefaedea 100644 --- a/ui/src/views/data/DataVersioningView.vue +++ b/ui/src/views/data/DataVersioningView.vue @@ -222,18 +222,12 @@ {{ currentDataset[0].periode }} - <div v-if="errorsMessages.length" style="margin: 10px"> - <div v-for="msg in errorsMessages" v-bind:key="msg"> - <b-message - :aria-close-label="$t('message.close')" - :title="$t('message.data-type-config-error')" - class="mt-4 DataTypesManagementView-message" - has-icon - type="is-danger" - > - <span v-html="msg"/> - </b-message> - </div> + <div v-if="errorsMessages.length !== 0" style="margin: 10px"> + <ShowErrors + :errors-messages="errorsMessages" + :errors-messages-length="errorsMessages.length" + :title="$t('message.data-type-config-error')" + ></ShowErrors> </div> </caption> @@ -346,6 +340,7 @@ import moment from "moment"; import {BinaryFile} from "@/model/file/BinaryFile"; import {Dataset} from "@/model/file/Dataset"; import InputDate from "@/components/common/InputDate.vue"; +import ShowErrors from "@/components/application/ShowErrors.vue"; export default { name: "DataVersioningView", @@ -356,6 +351,7 @@ export default { }, }, components: { + ShowErrors, InputDate, CollapseMenu, LoadingAnimate, @@ -428,9 +424,9 @@ export default { }); function updateDate({name, value}) { - if ("startDate" == name) { + if ("startDate" === name) { startDate.value = value - } else if ("endDate" == name) { + } else if ("endDate" === name) { endDate.value = value } } -- GitLab From 1f18fb37a00e971916349c13570c3ebd5654bf1d Mon Sep 17 00:00:00 2001 From: lucile varloteaux <lucile.varloteaux@inrae.fr> Date: Tue, 14 May 2024 18:00:24 +0200 Subject: [PATCH 3/4] =?UTF-8?q?remonter=20erreur=20oubli=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/fr/inra/oresing/rest/OreSiService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/inra/oresing/rest/OreSiService.java b/src/main/java/fr/inra/oresing/rest/OreSiService.java index be6f3a0c1..973d6f80a 100644 --- a/src/main/java/fr/inra/oresing/rest/OreSiService.java +++ b/src/main/java/fr/inra/oresing/rest/OreSiService.java @@ -383,8 +383,12 @@ public class OreSiService { //TODO test à faire entre version ancienne et nouvelle final Version oldVersion = oldConfiguration.applicationDescription().version(); final Version newVersion = newConfiguration.applicationDescription().version(); - Preconditions.checkArgument(newVersion.compareTo(oldVersion) > 0, "l'application " + applicationName + " est déjà dans la version " + oldVersion); - progression1.pushMessage("start", Map.of("application", applicationName, "oldVersion", oldVersion.version(), "newVersion", newVersion.version())); + try { + Preconditions.checkArgument(newVersion.compareTo(oldVersion) > 0, "l'application " + applicationName + " est déjà dans la version " + oldVersion); + } catch (final IllegalArgumentException e) { + progression1.pushError(e); + progression1.pushMessage("start", Map.of("application", applicationName, "oldVersion", oldVersion.version(), "newVersion", newVersion.version())); + } if (log.isInfoEnabled()) { log.info("va migrer les données de {} de la version actuelle {} à la nouvelle version {}", applicationName, oldVersion, newVersion); } -- GitLab From 90c3949f2515e507062a68ea40936d69b8979f92 Mon Sep 17 00:00:00 2001 From: lucile varloteaux <lucile.varloteaux@inrae.fr> Date: Thu, 16 May 2024 16:14:11 +0200 Subject: [PATCH 4/4] =?UTF-8?q?r=C3=A9paration=20test=20r=C3=A9curcivit?= =?UTF-8?q?=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/data/recursivite/recusivite.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/resources/data/recursivite/recusivite.yaml b/src/test/resources/data/recursivite/recusivite.yaml index 522742076..b87a080a6 100644 --- a/src/test/resources/data/recursivite/recusivite.yaml +++ b/src/test/resources/data/recursivite/recusivite.yaml @@ -148,7 +148,6 @@ OA_data: OA_importHeader: code sandre du taxon sandre_superieur: OA_importHeader: code sandre du taxon supérieur - incertitude: OA_dynamicComponents: proprietesDeTaxon: OA_exportHeader: -- GitLab