diff --git a/build.gradle.kts b/build.gradle.kts index aa34ebb11..b1009be5d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,7 +16,7 @@ java { allprojects { group = "fr.insee.eno" - version = "3.26.4" + version = "3.27.0" } subprojects { diff --git a/eno-core/src/main/java/fr/insee/eno/core/exceptions/business/InvalidSuggesterExpression.java b/eno-core/src/main/java/fr/insee/eno/core/exceptions/business/InvalidSuggesterExpression.java new file mode 100644 index 000000000..91d2da7c2 --- /dev/null +++ b/eno-core/src/main/java/fr/insee/eno/core/exceptions/business/InvalidSuggesterExpression.java @@ -0,0 +1,13 @@ +package fr.insee.eno.core.exceptions.business; + +/** + * Exception to be thrown if the magic VTL expression (that uses a left join) used for suggester option responses + * is invalid. + */ +public class InvalidSuggesterExpression extends RuntimeException { + + public InvalidSuggesterExpression(String message) { + super(message); + } + +} diff --git a/eno-core/src/main/java/fr/insee/eno/core/processing/out/LunaticProcessing.java b/eno-core/src/main/java/fr/insee/eno/core/processing/out/LunaticProcessing.java index 03b860155..a6436f7dd 100644 --- a/eno-core/src/main/java/fr/insee/eno/core/processing/out/LunaticProcessing.java +++ b/eno-core/src/main/java/fr/insee/eno/core/processing/out/LunaticProcessing.java @@ -44,6 +44,7 @@ public void applyProcessing(Questionnaire lunaticQuestionnaire, EnoQuestionnaire .then(new LunaticEditLabelTypes()) // this step should be temporary .then(new LunaticSuggestersConfiguration(enoQuestionnaire)) .then(new LunaticVariablesDimension(enoQuestionnaire)) + .then(new LunaticSuggesterOptionResponses()) .thenIf(lunaticParameters.isMissingVariables(), new LunaticAddMissingVariables(enoCatalog, lunaticParameters.isMissingVariables())) .then(new LunaticAddResizing(enoQuestionnaire)) diff --git a/eno-core/src/main/java/fr/insee/eno/core/processing/out/steps/lunatic/LunaticSuggesterOptionResponses.java b/eno-core/src/main/java/fr/insee/eno/core/processing/out/steps/lunatic/LunaticSuggesterOptionResponses.java new file mode 100644 index 000000000..956714aca --- /dev/null +++ b/eno-core/src/main/java/fr/insee/eno/core/processing/out/steps/lunatic/LunaticSuggesterOptionResponses.java @@ -0,0 +1,196 @@ +package fr.insee.eno.core.processing.out.steps.lunatic; + +import fr.insee.eno.core.exceptions.business.InvalidSuggesterExpression; +import fr.insee.eno.core.exceptions.technical.MappingException; +import fr.insee.eno.core.processing.ProcessingStep; +import fr.insee.eno.core.utils.VtlSyntaxUtils; +import fr.insee.lunatic.model.flat.*; +import fr.insee.lunatic.model.flat.variable.CalculatedVariableType; +import fr.insee.lunatic.model.flat.variable.CollectedVariableType; +import fr.insee.lunatic.model.flat.variable.CollectedVariableValues; +import fr.insee.lunatic.model.flat.variable.VariableType; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * In Lunatic, when a respondent chooses an entry in a suggester field, multiple variables can be filled. + * These variables correspond to the different columns of the nomenclature used in the suggester. + * In DDI, these variables are represented as calculated variables with a "magic" expression using a VTL + * left join operator. + * Format of the magic expression: + * left_join(RESPONSE_NAME, NOMENCLATURE_NAME using ID_FIELD, OTHER_FIELD) + */ +@Slf4j +public class LunaticSuggesterOptionResponses implements ProcessingStep { + + // Feature is not designed in Pogues yet. + // This processing will probably have to be refactored when proper Pogues modeling is done. + + /** Identifier field that must always be present in nomenclature fields. */ + private static final String NOMENCLATURE_ID_FIELD = "id"; + + /** + * Record to store information contained in the "magic" suggester response expressions. + * Note: made package-private to be unit tested. + * @param responseName Main response of the suggester component. + * @param storeName "name" in suggesters at questionnaire level. "storeName" in components. + * @param idField Identifier field of the nomenclature. + * @param fieldName Field name to be associated with the calculated variable that holds the expression. + */ + record SuggesterResponseExpression( + String responseName, + String storeName, + String idField, + String fieldName + ){} + + /** + * Unpacks the given expression to return its pieces of information. + * Note: made package-private to be unit-tested. + * @param expression Magic expression of a suggester option response (that contains a left join). + * @return A record with the information held by the expression. + * @throws InvalidSuggesterExpression If the expression does not match the format "left_join(A, B using C, D)". + */ + static SuggesterResponseExpression unpackSuggesterResponseExpression(String expression) + throws InvalidSuggesterExpression { + String content = expression.replace(VtlSyntaxUtils.LEFT_JOIN_OPERATOR, ""); + content = content.replace("(", ""); + content = content.replace(")", ""); + String[] splitContent = content.split(","); + if (3 != splitContent.length) + throw new InvalidSuggesterExpression("Invalid usage of the left join operator."); + String[] splitContent2 = splitContent[1].split(VtlSyntaxUtils.USING_KEYWORD); + if (2 != splitContent2.length) + throw new InvalidSuggesterExpression("The 'using' keyword is missing or misplaced."); + String responseName = splitContent[0].trim(); + String nomenclatureName = splitContent2[0].trim(); + String nomenclatureId = splitContent2[1].trim(); + String fieldName = splitContent[2].trim(); + if (!NOMENCLATURE_ID_FIELD.equals(nomenclatureId)) + log.warn("Nomenclature identifier field " + nomenclatureId + " is not equal to " + NOMENCLATURE_ID_FIELD + "."); + if (NOMENCLATURE_ID_FIELD.equals(fieldName)) + log.warn("Identifier field used in an option response suggester expression."); + return new SuggesterResponseExpression(responseName, nomenclatureName, nomenclatureId, fieldName); + } + + /** + * Transforms the calculated variable with the magic expression that uses a VTL left join into "optionResponses" + * of suggester components. Also creates corresponding collected variables, and removes these fake calculated ones. + * @param lunaticQuestionnaire Lunatic questionnaire. + */ + @Override + public void apply(Questionnaire lunaticQuestionnaire) { + // + Map suggesterResponseExpressions = mapSuggesterResponseExpressions(lunaticQuestionnaire); + Map suggesterComponents = gatherSuggesterComponents(lunaticQuestionnaire); + // + suggesterResponseExpressions.keySet().forEach(optionResponseName -> { + SuggesterResponseExpression suggesterResponseExpression = suggesterResponseExpressions.get(optionResponseName); + Suggester suggester = suggesterComponents.get(suggesterResponseExpression.responseName()); + suggester.getOptionResponses().add(new Suggester.OptionResponse( + optionResponseName, suggesterResponseExpression.fieldName())); + convertOptionResponseVariable(lunaticQuestionnaire, optionResponseName); + }); + } + + private Map gatherSuggesterComponents(Questionnaire lunaticQuestionnaire) { + Map suggesterComponents = new HashMap<>(); + putSuggesterComponents(suggesterComponents, lunaticQuestionnaire.getComponents()); + return suggesterComponents; + } + private void putSuggesterComponents(Map suggesterComponents, List lunaticComponents) { + lunaticComponents.forEach(component -> { + if (component instanceof Suggester suggester){ + ResponseType suggesterResponse = suggester.getResponse(); + if (suggesterResponse == null) + throw new MappingException("Suggester '" + suggester.getId() + "' has no response."); + suggesterComponents.put(suggesterResponse.getName(), suggester); + } + if (component instanceof Loop loop) + putSuggesterComponents(suggesterComponents, loop.getComponents()); + if (component instanceof Roundabout roundabout) + putSuggesterComponents(suggesterComponents, roundabout.getComponents()); + }); + } + + /** + * Maps the information hold by calculated variables that have the magic expression for suggesters, and returns it + * in a map designed to make the link between a suggester component, one of its fields and the corresponding + * option response variable. + * @param lunaticQuestionnaire Lunatic questionnaire. + * @return A map of response name -> field name -> variable name. + */ + private Map mapSuggesterResponseExpressions(Questionnaire lunaticQuestionnaire) { + Map result = new LinkedHashMap<>(); + lunaticQuestionnaire.getVariables().stream() + .filter(CalculatedVariableType.class::isInstance) + .map(CalculatedVariableType.class::cast) + .filter(calculatedVariable -> { + try { + String editedExpression = calculatedVariable.getExpression().getValue() + .replace("\"", ""); // due to dirty workaround in Pogues + if (editedExpression.startsWith(VtlSyntaxUtils.LEFT_JOIN_OPERATOR)) { + calculatedVariable.getExpression().setValue(editedExpression); + return true; + } + return false; + } catch (NullPointerException e) { + throw new MappingException("Calculated variable '" + calculatedVariable.getName() + "' has no expression."); + } + }) + .forEachOrdered(calculatedVariable -> { + String expression = calculatedVariable.getExpression().getValue(); + try { + SuggesterResponseExpression suggesterResponseExpression = unpackSuggesterResponseExpression(expression); + result.put(calculatedVariable.getName(), suggesterResponseExpression); + } catch (InvalidSuggesterExpression e) { + log.error("Invalid usage of the left join operator in calculated variable {}.", + calculatedVariable.getName()); + throw e; + } + }); + return result; + } + + /** + * Transforms the calculated variables that correspond to the option responses of suggesters into collected + * variables. + * @param lunaticQuestionnaire Lunatic questionnaire. + * @param optionResponseName Name of the option response variable to be transformed from calculated into collected. + */ + private void convertOptionResponseVariable(Questionnaire lunaticQuestionnaire, String optionResponseName) { + VariableType fakeVariable = removeVariable(lunaticQuestionnaire, optionResponseName); + if (fakeVariable == null) { + log.error("Unable to remove variable {} in lunatic questionnaire {}.", + optionResponseName, lunaticQuestionnaire.getId()); + throw new InvalidSuggesterExpression( + "Error when converting suggester option response variable " + optionResponseName + "."); + } + CollectedVariableType suggesterOptionVariable = new CollectedVariableType(); + suggesterOptionVariable.setName(fakeVariable.getName()); + suggesterOptionVariable.setIterationReference(fakeVariable.getIterationReference()); + suggesterOptionVariable.setDimension(fakeVariable.getDimension()); + assert fakeVariable.getDimension() != null : "Dimension processing must be called first."; + switch (fakeVariable.getDimension()) { + case SCALAR -> suggesterOptionVariable.setValues(new CollectedVariableValues.Scalar()); + case ARRAY -> suggesterOptionVariable.setValues(new CollectedVariableValues.Array()); + case DOUBLE_ARRAY -> throw new InvalidSuggesterExpression( + "Suggester option variable " + optionResponseName + " has an invalid scope."); + } + lunaticQuestionnaire.getVariables().add(suggesterOptionVariable); + } + private VariableType removeVariable(Questionnaire lunaticQuestionnaire, String variableName) { + for (VariableType variable : lunaticQuestionnaire.getVariables()) { + if (variableName.equals(variable.getName())) { + lunaticQuestionnaire.getVariables().remove(variable); + return variable; + } + } + return null; + } + +} diff --git a/eno-core/src/main/java/fr/insee/eno/core/utils/VtlSyntaxUtils.java b/eno-core/src/main/java/fr/insee/eno/core/utils/VtlSyntaxUtils.java index 0f5206d4a..819d3a1e8 100644 --- a/eno-core/src/main/java/fr/insee/eno/core/utils/VtlSyntaxUtils.java +++ b/eno-core/src/main/java/fr/insee/eno/core/utils/VtlSyntaxUtils.java @@ -5,6 +5,9 @@ */ public class VtlSyntaxUtils { + public static final String LEFT_JOIN_OPERATOR = "left_join"; + public static final String USING_KEYWORD = "using"; + private VtlSyntaxUtils() {} private static final String VTL_CONCATENATION_OPERATOR = "||"; diff --git a/eno-core/src/test/java/fr/insee/eno/core/processing/out/steps/lunatic/LunaticSuggesterOptionResponsesTest.java b/eno-core/src/test/java/fr/insee/eno/core/processing/out/steps/lunatic/LunaticSuggesterOptionResponsesTest.java new file mode 100644 index 000000000..ce48192da --- /dev/null +++ b/eno-core/src/test/java/fr/insee/eno/core/processing/out/steps/lunatic/LunaticSuggesterOptionResponsesTest.java @@ -0,0 +1,158 @@ +package fr.insee.eno.core.processing.out.steps.lunatic; + +import fr.insee.eno.core.DDIToEno; +import fr.insee.eno.core.exceptions.business.DDIParsingException; +import fr.insee.eno.core.exceptions.business.InvalidSuggesterExpression; +import fr.insee.eno.core.mappers.LunaticMapper; +import fr.insee.eno.core.model.EnoQuestionnaire; +import fr.insee.eno.core.parameter.EnoParameters; +import fr.insee.eno.core.parameter.Format; +import fr.insee.eno.core.processing.out.steps.lunatic.LunaticSuggesterOptionResponses.SuggesterResponseExpression; +import fr.insee.lunatic.model.flat.*; +import fr.insee.lunatic.model.flat.variable.*; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static fr.insee.eno.core.processing.out.steps.lunatic.LunaticSuggesterOptionResponses.unpackSuggesterResponseExpression; +import static org.junit.jupiter.api.Assertions.*; + +class LunaticSuggesterOptionResponsesTest { + + @Test + void unpackExpressionTest() throws InvalidSuggesterExpression { + // + String testExpression = """ + left_join(RESPONSE_NAME, NOMENCLATURE_NAME using id, ATTRIBUTE_NAME)"""; + // + SuggesterResponseExpression result = unpackSuggesterResponseExpression(testExpression); + // + assertEquals("RESPONSE_NAME", result.responseName()); + assertEquals("NOMENCLATURE_NAME", result.storeName()); + assertEquals("id", result.idField()); + assertEquals("ATTRIBUTE_NAME", result.fieldName()); + } + + @Test + void unitTest() { + // Given + Questionnaire lunaticQuestionnaire = new Questionnaire(); + // Some collected variable + CollectedVariableType fooCollected = new CollectedVariableType(); + fooCollected.setName("FOO_COLLECTED"); + lunaticQuestionnaire.getVariables().add(fooCollected); + // Some external variable + lunaticQuestionnaire.getVariables().add(new ExternalVariableType()); + // Some calculated variable + CalculatedVariableType fooCalculated = new CalculatedVariableType(); + fooCalculated.setName("FOO_CALCULATED"); + fooCalculated.setExpression(new LabelType()); + fooCalculated.getExpression().setValue(""); + lunaticQuestionnaire.getVariables().add(fooCalculated); + // Suggester component + Suggester suggester = new Suggester(); + suggester.setResponse(new ResponseType()); + suggester.getResponse().setName("CITY_ID"); + lunaticQuestionnaire.getComponents().add(suggester); + CollectedVariableType suggesterResponseVariable = new CollectedVariableType(); + suggesterResponseVariable.setName("CITY_ID"); + suggesterResponseVariable.setDimension(VariableDimension.SCALAR); + suggesterResponseVariable.setValues(new CollectedVariableValues.Scalar()); + lunaticQuestionnaire.getVariables().add(suggesterResponseVariable); + // Suggester option response variables + String expression1 = """ + left_join(CITY_ID, CITY_CODE_LIST using id, label)"""; + CalculatedVariableType suggesterResponseVariable1 = new CalculatedVariableType(); + suggesterResponseVariable1.setName("CITY_LABEL"); + suggesterResponseVariable1.setExpression(new LabelType()); + suggesterResponseVariable1.getExpression().setValue(expression1); + suggesterResponseVariable1.setDimension(VariableDimension.SCALAR); + lunaticQuestionnaire.getVariables().add(suggesterResponseVariable1); + String expression2 = """ + left_join(CITY_ID, CITY_CODE_LIST using id, code)"""; + CalculatedVariableType suggesterResponseVariable2 = new CalculatedVariableType(); + suggesterResponseVariable2.setName("CITY_CODE"); + suggesterResponseVariable2.setExpression(new LabelType()); + suggesterResponseVariable2.getExpression().setValue(expression2); + suggesterResponseVariable2.setDimension(VariableDimension.SCALAR); + lunaticQuestionnaire.getVariables().add(suggesterResponseVariable2); + + // When + LunaticSuggesterOptionResponses processing = new LunaticSuggesterOptionResponses(); + processing.apply(lunaticQuestionnaire); + + // Then + assertEquals(2, suggester.getOptionResponses().size()); + assertEquals("CITY_LABEL", suggester.getOptionResponses().get(0).name()); + assertEquals("CITY_CODE", suggester.getOptionResponses().get(1).name()); + assertEquals("label", suggester.getOptionResponses().get(0).attribute()); + assertEquals("code", suggester.getOptionResponses().get(1).attribute()); + // + assertEquals(1, lunaticQuestionnaire.getVariables().stream() + .filter(CalculatedVariableType.class::isInstance).count()); + // + Map collectedVariables = new HashMap<>(); + lunaticQuestionnaire.getVariables().stream() + .filter(CollectedVariableType.class::isInstance).map(CollectedVariableType.class::cast) + .forEach(collectedVariable -> collectedVariables.put(collectedVariable.getName(), collectedVariable)); + assertEquals(Set.of("FOO_COLLECTED", "CITY_ID", "CITY_LABEL", "CITY_CODE"), collectedVariables.keySet()); + collectedVariables.values().stream() + .filter(variable -> !"FOO_COLLECTED".equals(variable.getName())) + .forEach(collectedVariable -> { + assertEquals(VariableDimension.SCALAR, collectedVariable.getDimension()); + assertInstanceOf(CollectedVariableValues.Scalar.class, collectedVariable.getValues()); + }); + } + + @Test + void integrationTest() throws DDIParsingException { + // + EnoQuestionnaire enoQuestionnaire = DDIToEno.transform( + this.getClass().getClassLoader().getResourceAsStream("integration/ddi/ddi-suggester-options.xml"), + EnoParameters.of(EnoParameters.Context.DEFAULT, EnoParameters.ModeParameter.CAWI, Format.LUNATIC)); + Questionnaire lunaticQuestionnaire = new Questionnaire(); + new LunaticMapper().mapQuestionnaire(enoQuestionnaire, lunaticQuestionnaire); + new LunaticSortComponents(enoQuestionnaire).apply(lunaticQuestionnaire); + new LunaticLoopResolution(enoQuestionnaire).apply(lunaticQuestionnaire); + new LunaticVariablesDimension(enoQuestionnaire).apply(lunaticQuestionnaire); + + // + new LunaticSuggesterOptionResponses().apply(lunaticQuestionnaire); + + // Suggester components option responses + Suggester citySuggester1 = (Suggester) lunaticQuestionnaire.getComponents().get(1); + Suggester citySuggester2 = (Suggester) lunaticQuestionnaire.getComponents().get(2); + Suggester nationalitySuggester = (Suggester) lunaticQuestionnaire.getComponents().get(3); + Loop loop = (Loop) lunaticQuestionnaire.getComponents().get(4); + Suggester activitySuggester = (Suggester) loop.getComponents().get(1); + // + List.of(citySuggester1, citySuggester2, nationalitySuggester, activitySuggester).forEach(suggester -> { + assertEquals(1, suggester.getOptionResponses().size()); + assertEquals("label", suggester.getOptionResponses().getFirst().attribute()); + }); + assertEquals("CITY_OF_BIRTH_LABEL", citySuggester1.getOptionResponses().getFirst().name()); + assertEquals("CURRENT_CITY_LABEL", citySuggester2.getOptionResponses().getFirst().name()); + assertEquals("NATIONALITY_LABEL", nationalitySuggester.getOptionResponses().getFirst().name()); + assertEquals("ACTIVITY_LABEL", activitySuggester.getOptionResponses().getFirst().name()); + + // Corresponding variables + assertTrue(lunaticQuestionnaire.getVariables().stream().noneMatch(CalculatedVariableType.class::isInstance)); + // + Map variables = new HashMap<>(); + lunaticQuestionnaire.getVariables().forEach(variable -> + variables.put(variable.getName(), (CollectedVariableType) variable)); + // + List.of("CITY_OF_BIRTH_LABEL", "CURRENT_CITY_LABEL", "NATIONALITY_LABEL").forEach(variableName -> { + assertNull(variables.get(variableName).getIterationReference()); + assertEquals(VariableDimension.SCALAR, variables.get(variableName).getDimension()); + assertInstanceOf(CollectedVariableValues.Scalar.class, variables.get(variableName).getValues()); + }); + assertEquals(loop.getId(), variables.get("ACTIVITY_LABEL").getIterationReference()); + assertEquals(VariableDimension.ARRAY, variables.get("ACTIVITY_LABEL").getDimension()); + assertInstanceOf(CollectedVariableValues.Array.class, variables.get("ACTIVITY_LABEL").getValues()); + } + +} diff --git a/eno-core/src/test/resources/integration/ddi/ddi-suggester-options.xml b/eno-core/src/test/resources/integration/ddi/ddi-suggester-options.xml new file mode 100644 index 000000000..82085542e --- /dev/null +++ b/eno-core/src/test/resources/integration/ddi/ddi-suggester-options.xml @@ -0,0 +1,1384 @@ + + + fr.insee + INSEE-m16kx7hl + 1 + + + Eno - Suggester multiple variables + + + + fr.insee + RessourcePackage-m16kx7hl + 1 + + fr.insee + InterviewerInstructionScheme-m16kx7hl + 1 + + A définir + + + + fr.insee + ControlConstructScheme-m16kx7hl + 1 + + fr.insee + Sequence-m16kx7hl + 1 + + Eno - Suggester multiple variables + + template + + fr.insee + m16kqzg7 + 1 + Sequence + + + + fr.insee + m16kqzg7 + 1 + + S1 + + + "Sequence" + + module + + fr.insee + m16kjhib-QC + 1 + QuestionConstruct + + + fr.insee + m1adj8d6-QC + 1 + QuestionConstruct + + + fr.insee + m1ad9dyh-QC + 1 + QuestionConstruct + + + fr.insee + m1ageurs + 1 + Loop + + + + fr.insee + m1ag6y3a + 1 + + SS1 + + + "Sub-sequence" + + submodule + + fr.insee + m1ageu2g-QC + 1 + QuestionConstruct + + + + fr.insee + m1ageurs + 1 + + ACTIVITY_LOOP + + + + vtl + 1 + + + + + vtl + 10 + + + + + vtl + 1 + + + + fr.insee + m1ageurs-SEQ + 1 + Sequence + + + + fr.insee + m1ageurs-SEQ + 1 + loopContent + + fr.insee + m1ag6y3a + 1 + Sequence + + + + fr.insee + m16kjhib-QC + 1 + + CITY_OF_BIRTH + + + fr.insee + m16kjhib + 1 + QuestionItem + + + + fr.insee + m1adj8d6-QC + 1 + + CURRENT_CITY + + + fr.insee + m1adj8d6 + 1 + QuestionItem + + + + fr.insee + m1ad9dyh-QC + 1 + + NATIONALITY + + + fr.insee + m1ad9dyh + 1 + QuestionItem + + + + fr.insee + m1ageu2g-QC + 1 + + ACTIVITY + + + fr.insee + m1ageu2g + 1 + QuestionItem + + + + + fr.insee + QuestionScheme-m16kx7hl + 1 + + A définir + + + fr.insee + m16kjhib + 1 + + CITY_OF_BIRTH + + + fr.insee + m16kjhib-QOP-m16kfwqf + 1 + + CITY_OF_BIRTH + + + + + fr.insee + m16kjhib-RDOP-m16kfwqf + 1 + OutParameter + + + fr.insee + m16kjhib-QOP-m16kfwqf + 1 + OutParameter + + + + + "City of birth" + + + + suggester + + fr.insee + L_COMMUNES-2024 + 1 + CodeList + + + fr.insee + m16kjhib-RDOP-m16kfwqf + 1 + + + fr.insee + L_COMMUNES-2024 + 1 + CodeList + + + + + + + + fr.insee + m1adj8d6 + 1 + + CURRENT_CITY + + + fr.insee + m1adj8d6-QOP-m1adayn7 + 1 + + CURRENT_CITY + + + + + fr.insee + m1adj8d6-RDOP-m1adayn7 + 1 + OutParameter + + + fr.insee + m1adj8d6-QOP-m1adayn7 + 1 + OutParameter + + + + + "Current city" + + + + suggester + + fr.insee + L_COMMUNES-2024 + 1 + CodeList + + + fr.insee + m1adj8d6-RDOP-m1adayn7 + 1 + + + fr.insee + L_COMMUNES-2024 + 1 + CodeList + + + + + + + + fr.insee + m1ad9dyh + 1 + + NATIONALITY + + + fr.insee + m1ad9dyh-QOP-m1adexql + 1 + + NATIONALITY + + + + + fr.insee + m1ad9dyh-RDOP-m1adexql + 1 + OutParameter + + + fr.insee + m1ad9dyh-QOP-m1adexql + 1 + OutParameter + + + + + "Nationality" + + + + suggester + + fr.insee + L_NATIONALITE-1-2-0 + 1 + CodeList + + + fr.insee + m1ad9dyh-RDOP-m1adexql + 1 + + + fr.insee + L_NATIONALITE-1-2-0 + 1 + CodeList + + + + + + + + fr.insee + m1ageu2g + 1 + + ACTIVITY + + + fr.insee + m1ageu2g-QOP-m1ajfscg + 1 + + ACTIVITY + + + + + fr.insee + m1ageu2g-RDOP-m1ajfscg + 1 + OutParameter + + + fr.insee + m1ageu2g-QOP-m1ajfscg + 1 + OutParameter + + + + + "Activity" + + + + suggester + + fr.insee + L_ACTIVITES-2-0-0 + 1 + CodeList + + + fr.insee + m1ageu2g-RDOP-m1ajfscg + 1 + + + fr.insee + L_ACTIVITES-2-0-0 + 1 + CodeList + + + + + + + + + fr.insee + CategoryScheme-m16kx7hl + 1 + + A définir + + + fr.insee + INSEE-COMMUN-CA-Booleen-1 + 1 + + + + + + + fr.insee + ENO_SUGGESTER_MULTI-CLS + 1 + + ENO_SUGGESTER_MULTI + + + fr.insee + L_COMMUNES-2024 + 1 + + SuggesterConfiguration + + label + [\w]+ + French + 3 + false + + + tokenized + + French + 3 + [\w.]+ + false + + + 1]]> + + + L_COMMUNES-2024 + + + Communes 2024 + + + urn:ddi:fr.insee:l_communes-2024:1 + CodeList + + Regular + + Ordinal + + + + fr.insee + L_NATIONALITE-1-2-0 + 1 + + SuggesterConfiguration + + label + [\w]+ + French + 3 + false + + + tokenized + + French + 3 + [\w.]+ + false + + + 1]]> + + + L_NATIONALITE-1-2-0 + + + Nationalités + + + urn:ddi:fr.insee:l_nationalite-1-2-0:1 + CodeList + + Regular + + Ordinal + + + + fr.insee + L_ACTIVITES-2-0-0 + 1 + + SuggesterConfiguration + + label + [\w]+ + French + 3 + false + + EHPAD + EPHAD + HEPAD + EPAD + EPAHD + EPADH + + + URSSAF + URSAF + URSAFF + + + ascenseurs + ASCENCEUR + ASSENCEUR + ACSENCEUR + + + joaillerie + JOAILLIER + + + alimentaire + ALIMANTAIRE + + + briqueterie + BRIQUETTERIE + + + prestations + PRESTATAIRE + + + alimentaires + ALIMANTAIRES + + + echafaudages + ECHAFFAUDAGE + ECHAFFAUDEUR + + + plaquisterie + PLACO + PLACOPLATRE + + + pneumatiques + PNEUS + + + agroalimentaire + AGGROALIMANTAIRE + AGROALIMANTAIRE + + + agroalimentaires + AGGROALIMANTAIRES + AGROALIMENTAIRES + + + a + au + dans + de + des + du + en + et + la + le + ou + sur + d + l + aux + dans + un + une + pour + avec + chez + par + les + + tokenized + + French + 3 + [\w.]+ + false + + + 1]]> + + + L_ACTIVITES-2-0-0 + + + Activités + + + urn:ddi:fr.insee:l_activites-2-0-0:1 + CodeList + + Regular + + Ordinal + + + + fr.insee + INSEE-COMMUN-CL-Booleen + 1 + + Booleen + + Regular + + Ordinal + + + fr.insee + INSEE-COMMUN-CL-Booleen-1 + 1 + + fr.insee + INSEE-COMMUN-CA-Booleen-1 + 1 + Category + + 1 + + + + + fr.insee + VariableScheme-m16kx7hl + 1 + + Variable Scheme for the survey + + + fr.insee + m1agh821 + 1 + + CITY_OF_BIRTH_LABEL + + + "City of birth label" + + + fr.insee + m1agh821-VROP + 1 + + + + fr.insee + m1agh821-GI + 1 + GenerationInstruction + + + fr.insee + m1agh821-GOP + 1 + OutParameter + + + fr.insee + m1agh821-VROP + 1 + OutParameter + + + + + + + + fr.insee + m1ajd442 + 1 + + CURRENT_CITY_LABEL + + + "Current city label" + + + fr.insee + m1ajd442-VROP + 1 + + + + fr.insee + m1ajd442-GI + 1 + GenerationInstruction + + + fr.insee + m1ajd442-GOP + 1 + OutParameter + + + fr.insee + m1ajd442-VROP + 1 + OutParameter + + + + + + + + fr.insee + m1awka9n + 1 + + NATIONALITY_LABEL + + + "Nationality label" + + + fr.insee + m1awka9n-VROP + 1 + + + + fr.insee + m1awka9n-GI + 1 + GenerationInstruction + + + fr.insee + m1awka9n-GOP + 1 + OutParameter + + + fr.insee + m1awka9n-VROP + 1 + OutParameter + + + + + + + + fr.insee + m1awevi4 + 1 + + ACTIVITY_LABEL + + + "Activity label" + + + fr.insee + m1awevi4-VROP + 1 + + + + fr.insee + m1awevi4-GI + 1 + GenerationInstruction + + + fr.insee + m1awevi4-GOP + 1 + OutParameter + + + fr.insee + m1awevi4-VROP + 1 + OutParameter + + + + + + + + fr.insee + m1acyc1z + 1 + + CITY_OF_BIRTH + + + CITY_OF_BIRTH label + + + fr.insee + m16kjhib-QOP-m16kfwqf + 1 + OutParameter + + + fr.insee + m16kjhib + 1 + QuestionItem + + + + + fr.insee + L_COMMUNES-2024 + 1 + CodeList + + + + + + fr.insee + m1adjivd + 1 + + CURRENT_CITY + + + CURRENT_CITY label + + + fr.insee + m1adj8d6-QOP-m1adayn7 + 1 + OutParameter + + + fr.insee + m1adj8d6 + 1 + QuestionItem + + + + + fr.insee + L_COMMUNES-2024 + 1 + CodeList + + + + + + fr.insee + m1adfh5f + 1 + + NATIONALITY + + + NATIONALITY label + + + fr.insee + m1ad9dyh-QOP-m1adexql + 1 + OutParameter + + + fr.insee + m1ad9dyh + 1 + QuestionItem + + + + + fr.insee + L_NATIONALITE-1-2-0 + 1 + CodeList + + + + + + fr.insee + m1agq89j + 1 + + ACTIVITY + + + ACTIVITY label + + + fr.insee + m1ageu2g-QOP-m1ajfscg + 1 + OutParameter + + + fr.insee + m1ageu2g + 1 + QuestionItem + + + + + fr.insee + L_ACTIVITES-2-0-0 + 1 + CodeList + + + + + + fr.insee + m1ageurs-vg + 1 + + + fr.insee + m1ageurs + 1 + Loop + + + Loop + + ACTIVITY_LOOP + + + fr.insee + m1awevi4 + 1 + Variable + + + fr.insee + m1agq89j + 1 + Variable + + + + fr.insee + INSEE-Instrument-m16kx7hl-vg + 1 + + + fr.insee + Instrument-m16kx7hl + 1 + Instrument + + + Questionnaire + + ENO_SUGGESTER_MULTI + + + fr.insee + m1agh821 + 1 + Variable + + + fr.insee + m1ajd442 + 1 + Variable + + + fr.insee + m1awka9n + 1 + Variable + + + fr.insee + m1acyc1z + 1 + Variable + + + fr.insee + m1adjivd + 1 + Variable + + + fr.insee + m1adfh5f + 1 + Variable + + + fr.insee + m1ageurs-vg + 1 + VariableGroup + + + + + fr.insee + INSEE-SIMPSONS-PIS-1 + 1 + + SIMPSONS + + + Processing instructions of the Simpsons questionnaire + + + fr.insee + m1agh821-GI + 1 + + fr.insee + m16kjhib + 1 + QuestionItem + + + fr.insee + m1acyc1z + 1 + Variable + + + + vtl + + fr.insee + m1agh821-IP-1 + 1 + + CITY_OF_BIRTH + + + + fr.insee + m1agh821-GOP + 1 + + + + fr.insee + m16kjhib-QOP-m16kfwqf + 1 + OutParameter + + + fr.insee + m1agh821-IP-1 + 1 + InParameter + + + left_join(m1agh821-IP-1, "L_COMMUNES-2024" using id, label) + + + + fr.insee + Sequence-m16kx7hl + 1 + Sequence + + + + fr.insee + m1ajd442-GI + 1 + + fr.insee + m1adj8d6 + 1 + QuestionItem + + + fr.insee + m1adjivd + 1 + Variable + + + + vtl + + fr.insee + m1ajd442-IP-1 + 1 + + CURRENT_CITY + + + + fr.insee + m1ajd442-GOP + 1 + + + + fr.insee + m1adj8d6-QOP-m1adayn7 + 1 + OutParameter + + + fr.insee + m1ajd442-IP-1 + 1 + InParameter + + + left_join(m1ajd442-IP-1, "L_COMMUNES-2024" using id, label) + + + + fr.insee + Sequence-m16kx7hl + 1 + Sequence + + + + fr.insee + m1awka9n-GI + 1 + + fr.insee + m1ad9dyh + 1 + QuestionItem + + + fr.insee + m1adfh5f + 1 + Variable + + + + vtl + + fr.insee + m1awka9n-IP-1 + 1 + + NATIONALITY + + + + fr.insee + m1awka9n-GOP + 1 + + + + fr.insee + m1ad9dyh-QOP-m1adexql + 1 + OutParameter + + + fr.insee + m1awka9n-IP-1 + 1 + InParameter + + + left_join(m1awka9n-IP-1, "L_NATIONALITE-1-2-0" using id, label) + + + + fr.insee + Sequence-m16kx7hl + 1 + Sequence + + + + fr.insee + m1awevi4-GI + 1 + + fr.insee + m1ageu2g + 1 + QuestionItem + + + fr.insee + m1agq89j + 1 + Variable + + + + vtl + + fr.insee + m1awevi4-IP-1 + 1 + + ACTIVITY + + + + fr.insee + m1awevi4-GOP + 1 + + + + fr.insee + m1ageu2g-QOP-m1ajfscg + 1 + OutParameter + + + fr.insee + m1awevi4-IP-1 + 1 + InParameter + + + left_join(m1awevi4-IP-1, "L_ACTIVITES-2-0-0" using id, label) + + + + fr.insee + m1ageurs + 1 + Loop + + + + + fr.insee + INSEE-SIMPSONS-MRS + 1 + + Liste de formats numériques et dates de + l'enquête + Numeric and DateTime list for the survey + + + + + fr.insee + StudyUnit-m16kx7hl + 1 + + + fr.insee + DataCollection-m16kx7hl + 1 + + fr.insee + QuestionScheme-m16kx7hl + 1 + QuestionScheme + + + fr.insee + ControlConstructScheme-m16kx7hl + 1 + ControlConstructScheme + + + fr.insee + InterviewerInstructionScheme-m16kx7hl + 1 + InterviewerInstructionScheme + + + fr.insee + InstrumentScheme-m16kx7hl + 1 + + fr.insee + Instrument-m16kx7hl + 1 + + ENO_SUGGESTER_MULTI + + + Eno - Suggester multiple variables questionnaire + + A définir + + fr.insee + Sequence-m16kx7hl + 1 + Sequence + + + + + + diff --git a/eno-core/src/test/resources/integration/pogues/pogues-suggester-options.json b/eno-core/src/test/resources/integration/pogues/pogues-suggester-options.json new file mode 100644 index 000000000..91f58c467 --- /dev/null +++ b/eno-core/src/test/resources/integration/pogues/pogues-suggester-options.json @@ -0,0 +1,544 @@ +{ + "id": "m16kx7hl", + "Name": "ENO_SUGGESTER_MULTI", + "Child": [ + { + "id": "m16kqzg7", + "Name": "S1", + "type": "SequenceType", + "Child": [ + { + "id": "m16kjhib", + "Name": "CITY_OF_BIRTH", + "type": "QuestionType", + "Label": [ + "\"City of birth\"" + ], + "depth": 2, + "Control": [], + "Response": [ + { + "id": "m16kfwqf", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 1, + "visualizationHint": "SUGGESTER" + }, + "mandatory": false, + "CodeListReference": "L_COMMUNES-2024", + "CollectedVariableReference": "m1acyc1z" + } + ], + "TargetMode": [ + "CAPI", + "CATI", + "CAWI", + "PAPI" + ], + "Declaration": [], + "FlowControl": [], + "questionType": "SINGLE_CHOICE", + "ClarificationQuestion": [] + }, + { + "id": "m1adj8d6", + "Name": "CURRENT_CITY", + "type": "QuestionType", + "Label": [ + "\"Current city\"" + ], + "depth": 2, + "Control": [], + "Response": [ + { + "id": "m1adayn7", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 1, + "visualizationHint": "SUGGESTER" + }, + "mandatory": false, + "CodeListReference": "L_COMMUNES-2024", + "CollectedVariableReference": "m1adjivd" + } + ], + "TargetMode": [ + "CAPI", + "CATI", + "CAWI", + "PAPI" + ], + "Declaration": [], + "FlowControl": [], + "questionType": "SINGLE_CHOICE", + "ClarificationQuestion": [] + }, + { + "id": "m1ad9dyh", + "Name": "NATIONALITY", + "type": "QuestionType", + "Label": [ + "\"Nationality\"" + ], + "depth": 2, + "Control": [], + "Response": [ + { + "id": "m1adexql", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 1, + "visualizationHint": "SUGGESTER" + }, + "mandatory": false, + "CodeListReference": "L_NATIONALITE-1-2-0", + "CollectedVariableReference": "m1adfh5f" + } + ], + "TargetMode": [ + "CAPI", + "CATI", + "CAWI", + "PAPI" + ], + "Declaration": [], + "FlowControl": [], + "questionType": "SINGLE_CHOICE", + "ClarificationQuestion": [] + }, + { + "id": "m1ag6y3a", + "Name": "SS1", + "type": "SequenceType", + "Child": [ + { + "id": "m1ageu2g", + "Name": "ACTIVITY", + "type": "QuestionType", + "Label": [ + "\"Activity\"" + ], + "depth": 3, + "Control": [], + "Response": [ + { + "id": "m1ajfscg", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 1, + "visualizationHint": "SUGGESTER" + }, + "mandatory": false, + "CodeListReference": "L_ACTIVITES-2-0-0", + "CollectedVariableReference": "m1agq89j" + } + ], + "TargetMode": [ + "CAPI", + "CATI", + "CAWI", + "PAPI" + ], + "Declaration": [], + "FlowControl": [], + "questionType": "SINGLE_CHOICE", + "ClarificationQuestion": [] + } + ], + "Label": [ + "\"Sub-sequence\"" + ], + "depth": 2, + "Control": [], + "TargetMode": [ + "CAPI", + "CATI", + "CAWI", + "PAPI" + ], + "Declaration": [], + "FlowControl": [], + "genericName": "SUBMODULE" + } + ], + "Label": [ + "\"Sequence\"" + ], + "depth": 1, + "Control": [], + "TargetMode": [ + "CAPI", + "CATI", + "CAWI", + "PAPI" + ], + "Declaration": [], + "FlowControl": [], + "genericName": "MODULE" + }, + { + "id": "idendquest", + "Name": "QUESTIONNAIRE_END", + "type": "SequenceType", + "Child": [], + "Label": [ + "QUESTIONNAIRE_END" + ], + "depth": 1, + "Control": [], + "TargetMode": [ + "CAPI", + "CATI", + "CAWI", + "PAPI" + ], + "Declaration": [], + "FlowControl": [], + "genericName": "MODULE" + } + ], + "Label": [ + "Eno - Suggester multiple variables" + ], + "final": false, + "owner": "DR59-SNDI59", + "agency": "fr.insee", + "CodeLists": { + "CodeList": [ + { + "id": "L_COMMUNES-2024", + "Urn": "urn:ddi:fr.insee:l_communes-2024:1", + "Name": "L_COMMUNES-2024", + "Label": "Communes 2024", + "SuggesterParameters": { + "fields": [ + { + "min": 3, + "name": "label", + "rules": [ + "[\\w]+" + ], + "stemmer": false, + "language": "French" + } + ], + "version": 1, + "queryParser": { + "type": "tokenized", + "params": { + "min": 3, + "pattern": "[\\w.]+", + "stemmer": false, + "language": "French" + } + } + } + }, + { + "id": "L_NATIONALITE-1-2-0", + "Urn": "urn:ddi:fr.insee:l_nationalite-1-2-0:1", + "Name": "L_NATIONALITE-1-2-0", + "Label": "Nationalités", + "SuggesterParameters": { + "fields": [ + { + "min": 3, + "name": "label", + "rules": [ + "[\\w]+" + ], + "stemmer": false, + "language": "French" + } + ], + "version": 1, + "queryParser": { + "type": "tokenized", + "params": { + "min": 3, + "pattern": "[\\w.]+", + "stemmer": false, + "language": "French" + } + } + } + }, + { + "id": "L_ACTIVITES-2-0-0", + "Urn": "urn:ddi:fr.insee:l_activites-2-0-0:1", + "Name": "L_ACTIVITES-2-0-0", + "Label": "Activités", + "SuggesterParameters": { + "fields": [ + { + "min": 3, + "name": "label", + "rules": [ + "[\\w]+" + ], + "stemmer": false, + "language": "French", + "synonyms": { + "EHPAD": [ + "EPHAD", + "HEPAD", + "EPAD", + "EPAHD", + "EPADH" + ], + "URSSAF": [ + "URSAF", + "URSAFF" + ], + "ascenseurs": [ + "ASCENCEUR", + "ASSENCEUR", + "ACSENCEUR" + ], + "joaillerie": [ + "JOAILLIER" + ], + "alimentaire": [ + "ALIMANTAIRE" + ], + "briqueterie": [ + "BRIQUETTERIE" + ], + "prestations": [ + "PRESTATAIRE" + ], + "alimentaires": [ + "ALIMANTAIRES" + ], + "echafaudages": [ + "ECHAFFAUDAGE", + "ECHAFFAUDEUR" + ], + "plaquisterie": [ + "PLACO", + "PLACOPLATRE" + ], + "pneumatiques": [ + "PNEUS" + ], + "agroalimentaire": [ + "AGGROALIMANTAIRE", + "AGROALIMANTAIRE" + ], + "agroalimentaires": [ + "AGGROALIMANTAIRES", + "AGROALIMENTAIRES" + ] + } + } + ], + "version": 1, + "stopWords": [ + "a", + "au", + "dans", + "de", + "des", + "du", + "en", + "et", + "la", + "le", + "ou", + "sur", + "d", + "l", + "aux", + "dans", + "un", + "une", + "pour", + "avec", + "chez", + "par", + "les" + ], + "queryParser": { + "type": "tokenized", + "params": { + "min": 3, + "pattern": "[\\w.]+", + "stemmer": false, + "language": "French" + } + } + } + } + ] + }, + "Variables": { + "Variable": [ + { + "id": "m1agh821", + "Name": "CITY_OF_BIRTH_LABEL", + "type": "CalculatedVariableType", + "Label": "\"City of birth label\"", + "Formula": "left_join($CITY_OF_BIRTH$, \"L_COMMUNES-2024\" using id, label)", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 249 + } + }, + { + "id": "m1ajd442", + "Name": "CURRENT_CITY_LABEL", + "type": "CalculatedVariableType", + "Label": "\"Current city label\"", + "Formula": "left_join($CURRENT_CITY$, \"L_COMMUNES-2024\" using id, label)", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 249 + } + }, + { + "id": "m1awka9n", + "Name": "NATIONALITY_LABEL", + "type": "CalculatedVariableType", + "Label": "\"Nationality label\"", + "Formula": "left_join($NATIONALITY$, \"L_NATIONALITE-1-2-0\" using id, label)", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 249 + } + }, + { + "id": "m1awevi4", + "Name": "ACTIVITY_LABEL", + "type": "CalculatedVariableType", + "Label": "\"Activity label\"", + "Scope": "m1ageurs", + "Formula": "left_join($ACTIVITY$, \"L_ACTIVITES-2-0-0\" using id, label)", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 249 + } + }, + { + "id": "m1acyc1z", + "Name": "CITY_OF_BIRTH", + "type": "CollectedVariableType", + "Label": "CITY_OF_BIRTH label", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 1 + }, + "CodeListReference": "L_COMMUNES-2024" + }, + { + "id": "m1adjivd", + "Name": "CURRENT_CITY", + "type": "CollectedVariableType", + "Label": "CURRENT_CITY label", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 1 + }, + "CodeListReference": "L_COMMUNES-2024" + }, + { + "id": "m1adfh5f", + "Name": "NATIONALITY", + "type": "CollectedVariableType", + "Label": "NATIONALITY label", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 1 + }, + "CodeListReference": "L_NATIONALITE-1-2-0" + }, + { + "id": "m1agq89j", + "Name": "ACTIVITY", + "type": "CollectedVariableType", + "Label": "ACTIVITY label", + "Scope": "m1ageurs", + "Datatype": { + "type": "TextDatatypeType", + "Pattern": "", + "typeName": "TEXT", + "MaxLength": 1 + }, + "CodeListReference": "L_ACTIVITES-2-0-0" + } + ] + }, + "flowLogic": "FILTER", + "Iterations": { + "Iteration": [ + { + "id": "m1ageurs", + "Name": "ACTIVITY_LOOP", + "Step": "1", + "type": "DynamicIterationType", + "Maximum": "10", + "Minimum": "1", + "MemberReference": [ + "m1ag6y3a", + "m1ag6y3a" + ] + } + ] + }, + "TargetMode": [ + "CAPI", + "CATI", + "CAWI", + "PAPI" + ], + "FlowControl": [], + "genericName": "QUESTIONNAIRE", + "ComponentGroup": [ + { + "id": "m16kx30l", + "Name": "PAGE_1", + "Label": [ + "Components for page 1" + ], + "MemberReference": [ + "m16kqzg7", + "m16kjhib", + "m1adj8d6", + "m1ad9dyh", + "m1ag6y3a", + "m1ageu2g", + "idendquest" + ] + } + ], + "DataCollection": [ + { + "id": "s2106-dc", + "uri": "http://ddi:fr.insee:DataCollection.s2106-dc" + } + ], + "lastUpdatedDate": "Fri Sep 20 2024 17:46:28 GMT+0200 (heure d’été d’Europe centrale)", + "formulasLanguage": "VTL", + "childQuestionnaireRef": [] +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index d731f5384..a07a69117 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,7 +8,7 @@ pluginManagement { dependencyResolutionManagement { versionCatalogs { create("libs") { - version("lunatic-model", "3.14.0") + version("lunatic-model", "3.15.0") version("pogues-model", "1.3.14") library("lunatic-model", "fr.insee.lunatic", "lunatic-model").versionRef("lunatic-model") library("pogues-model", "fr.insee.pogues", "pogues-model").versionRef("pogues-model")