From 0b54435468b79f35168e38fcdbe5929d01984118 Mon Sep 17 00:00:00 2001 From: Sharon Akinyi <79141719+sharon2719@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:36:56 +0300 Subject: [PATCH 1/3] Create new release snapshot (#326) --- efsity-cli/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efsity-cli/build.gradle.kts b/efsity-cli/build.gradle.kts index cdf95454..85e00b3e 100644 --- a/efsity-cli/build.gradle.kts +++ b/efsity-cli/build.gradle.kts @@ -18,7 +18,7 @@ repositories { group = "org.smartregister" -version = "2.3.12-SNAPSHOT" +version = "2.3.13-SNAPSHOT" description = "fhircore-tooling (efsity)" From 537e285b1e83d26e9e593ee891d6eff035c04019 Mon Sep 17 00:00:00 2001 From: Sharon Akinyi <79141719+sharon2719@users.noreply.github.com> Date: Wed, 15 Jan 2025 23:50:57 +0300 Subject: [PATCH 2/3] Add helper function to ignore hidden questions (#325) * Add helper function to ignore hidden questions * Add test cases for isHiddenQuestion * Remove print statement --- ...QuestionnaireResponseGeneratorCommand.java | 53 +++- .../command/ValidateStructureMapCommand.java | 3 +- ...tionnaireResponseGeneratorCommandTest.java | 229 ++++++++++++++++++ 3 files changed, 276 insertions(+), 9 deletions(-) diff --git a/efsity-cli/src/main/java/org/smartregister/command/QuestionnaireResponseGeneratorCommand.java b/efsity-cli/src/main/java/org/smartregister/command/QuestionnaireResponseGeneratorCommand.java index 91a47569..86190600 100644 --- a/efsity-cli/src/main/java/org/smartregister/command/QuestionnaireResponseGeneratorCommand.java +++ b/efsity-cli/src/main/java/org/smartregister/command/QuestionnaireResponseGeneratorCommand.java @@ -83,6 +83,12 @@ public class QuestionnaireResponseGeneratorCommand implements Runnable { defaultValue = ".") private String outputFilePath; + @CommandLine.Option( + names = {"-ih", "--ignore-hidden"}, + description = "Ignore hidden questions when generating responses", + defaultValue = "true") + private boolean ignoreHiddenQuestions; + private static final Random random = new Random(); private static final Faker faker = new Faker(); @@ -99,7 +105,8 @@ public void run() { fhir_base_url, apiKey, aiModel, - maxTokens); + maxTokens, + ignoreHiddenQuestions); } catch (IOException e) { throw new RuntimeException(e); } @@ -127,7 +134,8 @@ public static void generateResponse( String fhir_base_url, String apiKey, String aiModel, - String maxTokens) + String maxTokens, + boolean ignoreHiddenQuestions) throws IOException { long start = System.currentTimeMillis(); @@ -141,7 +149,7 @@ public static void generateResponse( String questionnaireResponseString = (Objects.equals(mode, "populate")) - ? populateMode(questionnaireData, fhir_base_url, extrasPath) + ? populateMode(questionnaireData, fhir_base_url, extrasPath, ignoreHiddenQuestions) : aiMode(questionnaireData, apiKey, aiModel, maxTokens); // Write response to file @@ -321,9 +329,15 @@ static JSONObject generateAnswer( } } - static JSONArray getAnswers(JSONArray questions, JSONArray responses, JSONObject extras) { + static JSONArray getAnswers( + JSONArray questions, JSONArray responses, JSONObject extras, boolean ignoreHiddenQuestions) { for (int i = 0; i < questions.length(); i++) { JSONObject current_question = questions.getJSONObject(i); + + if (ignoreHiddenQuestions && isHiddenQuestion(current_question)) { + continue; + } + String question_type = current_question.getString("type"); String link_id = current_question.getString("linkId"); @@ -335,7 +349,7 @@ static JSONArray getAnswers(JSONArray questions, JSONArray responses, JSONObject if (current_question.has("item")) { JSONArray group_questions = current_question.getJSONArray("item"); JSONArray group_responses = responses.getJSONObject(i).getJSONArray("item"); - getAnswers(group_questions, group_responses, extras); + getAnswers(group_questions, group_responses, extras, ignoreHiddenQuestions); } } responses.getJSONObject(i).put("answer", answer_arr); @@ -343,7 +357,30 @@ static JSONArray getAnswers(JSONArray questions, JSONArray responses, JSONObject return responses; } - static String populateMode(String questionnaireData, String fhir_base_url, String extrasPath) + static boolean isHiddenQuestion(JSONObject question) { + boolean isHidden = false; + + if (question.has("extension")) { + JSONArray extensions = question.getJSONArray("extension"); + for (int i = 0; i < extensions.length(); i++) { + JSONObject extension = extensions.getJSONObject(i); + if (extension + .getString("url") + .equals("http://hl7.org/fhir/StructureDefinition/questionnaire-hidden")) { + isHidden = extension.optBoolean("valueBoolean", true); + break; + } + } + } + + return isHidden; + } + + static String populateMode( + String questionnaireData, + String fhir_base_url, + String extrasPath, + boolean ignoreHiddenQuestions) throws IOException { JSONObject resource = new JSONObject(questionnaireData); String questionnaire_id = resource.getString("id"); @@ -377,7 +414,6 @@ static String populateMode(String questionnaireData, String fhir_base_url, Strin String populate_endpoint = String.join("/", fhir_base_url, resourceType, questionnaire_id, "$populate"); List result = HttpClient.postRequest(params.toString(), populate_endpoint, null); - JSONObject questionnaire_response = new JSONObject(result.get(1)); FctUtils.printError("Debug: response from questionnaireResponse: " + questionnaire_response); @@ -387,7 +423,8 @@ static String populateMode(String questionnaireData, String fhir_base_url, Strin if (questionnaire_response.has("item")) { JSONArray response = (JSONArray) questionnaire_response.get("item"); JSONArray questions = resource.getJSONArray("item"); - JSONArray response_with_answers = getAnswers(questions, response, extras); + JSONArray response_with_answers = + getAnswers(questions, response, extras, ignoreHiddenQuestions); questionnaire_response.put("item", response_with_answers); } return String.valueOf(questionnaire_response); diff --git a/efsity-cli/src/main/java/org/smartregister/command/ValidateStructureMapCommand.java b/efsity-cli/src/main/java/org/smartregister/command/ValidateStructureMapCommand.java index d10c85af..d168de25 100644 --- a/efsity-cli/src/main/java/org/smartregister/command/ValidateStructureMapCommand.java +++ b/efsity-cli/src/main/java/org/smartregister/command/ValidateStructureMapCommand.java @@ -127,7 +127,8 @@ void validateStructureMap(String inputFilePath, String structureMapFilePath, boo "http://localhost:8080/fhir", "", "", - ""); + "", + true); // Extract Resources using the StructureMap and the generated QuestionnaireResponse String generatedQuestionnaireResponsePath = diff --git a/efsity-cli/src/test/java/org/smartregister/command/QuestionnaireResponseGeneratorCommandTest.java b/efsity-cli/src/test/java/org/smartregister/command/QuestionnaireResponseGeneratorCommandTest.java index ed053ef3..5cc9d405 100644 --- a/efsity-cli/src/test/java/org/smartregister/command/QuestionnaireResponseGeneratorCommandTest.java +++ b/efsity-cli/src/test/java/org/smartregister/command/QuestionnaireResponseGeneratorCommandTest.java @@ -5,6 +5,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.Test; @@ -299,4 +300,232 @@ void testGenerateDefaultAnswer() { assertNotNull(answer); assertTrue(answer.isEmpty()); } + + @Test + void testIsHiddenQuestion_hiddenExtensionTrue() { + JSONObject question = new JSONObject(); + JSONArray extensions = new JSONArray(); + JSONObject hiddenExtension = new JSONObject(); + hiddenExtension.put("url", "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden"); + hiddenExtension.put("valueBoolean", true); + extensions.put(hiddenExtension); + question.put("extension", extensions); + + assertTrue(QuestionnaireResponseGeneratorCommand.isHiddenQuestion(question)); + } + + @Test + void testIsHiddenQuestion_hiddenExtensionFalse() { + JSONObject question = new JSONObject(); + JSONArray extensions = new JSONArray(); + JSONObject hiddenExtension = new JSONObject(); + hiddenExtension.put("url", "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden"); + hiddenExtension.put("valueBoolean", false); + extensions.put(hiddenExtension); + question.put("extension", extensions); + + assertFalse(QuestionnaireResponseGeneratorCommand.isHiddenQuestion(question)); + } + + @Test + void testIsHiddenQuestion_noHiddenExtension() { + JSONObject question = new JSONObject(); + JSONArray extensions = new JSONArray(); + JSONObject otherExtension = new JSONObject(); + otherExtension.put("url", "http://example.com/other-extension"); + otherExtension.put("valueBoolean", true); + extensions.put(otherExtension); + question.put("extension", extensions); + + assertFalse(QuestionnaireResponseGeneratorCommand.isHiddenQuestion(question)); + } + + @Test + void testIsHiddenQuestion_noExtensions() { + JSONObject question = new JSONObject(); + assertFalse(QuestionnaireResponseGeneratorCommand.isHiddenQuestion(question)); + } + + @Test + void testIsHiddenQuestion_hiddenExtensionNoValueBoolean() { + JSONObject question = new JSONObject(); + JSONArray extensions = new JSONArray(); + JSONObject hiddenExtension = new JSONObject(); + hiddenExtension.put("url", "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden"); + extensions.put(hiddenExtension); + question.put("extension", extensions); + + assertTrue(QuestionnaireResponseGeneratorCommand.isHiddenQuestion(question)); + } + + @Test + void testGetAnswers_simpleQuestion() { + JSONArray questions = new JSONArray(); + JSONObject question = new JSONObject(); + question.put("type", "string"); + question.put("linkId", "exampleLinkId"); + questions.put(question); + + JSONArray responses = new JSONArray(); + JSONObject response = new JSONObject(); + responses.put(response); + + JSONObject extras = new JSONObject(); + + JSONArray updatedResponses = + QuestionnaireResponseGeneratorCommand.getAnswers(questions, responses, extras, false); + + assertNotNull(updatedResponses); + assertTrue(updatedResponses.getJSONObject(0).has("answer")); + assertTrue( + updatedResponses + .getJSONObject(0) + .getJSONArray("answer") + .getJSONObject(0) + .has("valueString")); + } + + @Test + void testGetAnswers_groupQuestion() { + JSONArray questions = new JSONArray(); + JSONObject groupQuestion = new JSONObject(); + groupQuestion.put("type", "group"); + groupQuestion.put("linkId", "group1"); + JSONArray nestedQuestions = new JSONArray(); + JSONObject nestedQuestion = new JSONObject(); + nestedQuestion.put("type", "integer"); + nestedQuestion.put("linkId", "nested1"); + nestedQuestions.put(nestedQuestion); + groupQuestion.put("item", nestedQuestions); + questions.put(groupQuestion); + + JSONArray responses = new JSONArray(); + JSONObject groupResponse = new JSONObject(); + groupResponse.put("item", new JSONArray().put(new JSONObject())); + responses.put(groupResponse); + + JSONObject extras = new JSONObject(); + + JSONArray updatedResponses = + QuestionnaireResponseGeneratorCommand.getAnswers(questions, responses, extras, false); + + assertNotNull(updatedResponses); + JSONObject nestedResponse = + updatedResponses.getJSONObject(0).getJSONArray("item").getJSONObject(0); + assertTrue(nestedResponse.has("answer")); + assertTrue(nestedResponse.getJSONArray("answer").getJSONObject(0).has("valueInteger")); + } + + @Test + void testGetAnswers_ignoreHiddenQuestions() { + JSONArray questions = new JSONArray(); + JSONObject hiddenQuestion = new JSONObject(); + hiddenQuestion.put("type", "boolean"); + hiddenQuestion.put("linkId", "hidden1"); + JSONArray extensions = new JSONArray(); + JSONObject hiddenExtension = new JSONObject(); + hiddenExtension.put("url", "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden"); + hiddenExtension.put("valueBoolean", true); + extensions.put(hiddenExtension); + hiddenQuestion.put("extension", extensions); + questions.put(hiddenQuestion); + + JSONArray responses = new JSONArray(); + responses.put(new JSONObject()); + + JSONObject extras = new JSONObject(); + + JSONArray updatedResponses = + QuestionnaireResponseGeneratorCommand.getAnswers(questions, responses, extras, true); + + assertNotNull(updatedResponses); + assertFalse(updatedResponses.getJSONObject(0).has("answer")); + } + + @Test + void testGetAnswers_includeHiddenQuestions() { + JSONArray questions = new JSONArray(); + JSONObject hiddenQuestion = new JSONObject(); + hiddenQuestion.put("type", "boolean"); + hiddenQuestion.put("linkId", "hidden1"); + JSONArray extensions = new JSONArray(); + JSONObject hiddenExtension = new JSONObject(); + hiddenExtension.put("url", "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden"); + hiddenExtension.put("valueBoolean", true); + extensions.put(hiddenExtension); + hiddenQuestion.put("extension", extensions); + questions.put(hiddenQuestion); + + JSONArray responses = new JSONArray(); + responses.put(new JSONObject()); + + JSONObject extras = new JSONObject(); + + JSONArray updatedResponses = + QuestionnaireResponseGeneratorCommand.getAnswers(questions, responses, extras, false); + + assertNotNull(updatedResponses); + assertTrue(updatedResponses.getJSONObject(0).has("answer")); + assertTrue( + updatedResponses + .getJSONObject(0) + .getJSONArray("answer") + .getJSONObject(0) + .has("valueBoolean")); + } + + @Test + void testGetAnswers_noExtras() { + JSONArray questions = new JSONArray(); + JSONObject question = new JSONObject(); + question.put("type", "decimal"); + question.put("linkId", "decimal1"); + questions.put(question); + + JSONArray responses = new JSONArray(); + responses.put(new JSONObject()); + + JSONArray updatedResponses = + QuestionnaireResponseGeneratorCommand.getAnswers(questions, responses, null, false); + + assertNotNull(updatedResponses); + assertTrue(updatedResponses.getJSONObject(0).has("answer")); + assertTrue( + updatedResponses + .getJSONObject(0) + .getJSONArray("answer") + .getJSONObject(0) + .has("valueDecimal")); + } + + @Test + void testGetAnswers_emptyQuestionnaire() { + JSONArray questions = new JSONArray(); + JSONArray responses = new JSONArray(); + JSONObject extras = new JSONObject(); + + JSONArray updatedResponses = + QuestionnaireResponseGeneratorCommand.getAnswers(questions, responses, extras, false); + + assertNotNull(updatedResponses); + assertEquals(0, updatedResponses.length()); + } + + @Test + void testGetAnswers_mismatchedQuestionsAndResponses() { + JSONArray questions = new JSONArray(); + JSONObject question = new JSONObject(); + question.put("type", "text"); + question.put("linkId", "text1"); + questions.put(question); + + JSONArray responses = new JSONArray(); + + JSONObject extras = new JSONObject(); + + assertThrows( + JSONException.class, + () -> + QuestionnaireResponseGeneratorCommand.getAnswers(questions, responses, extras, false)); + } } From 4ce042f8cfbb9c9d2703dad748033d915c875828 Mon Sep 17 00:00:00 2001 From: Hilary Baraka Egesa Date: Thu, 16 Jan 2025 04:00:48 +0300 Subject: [PATCH 3/3] Fix regression bugs in pr 214 (#228) * init multifactor authentication setup * Revert "init multifactor authentication setup" This reverts commit 826f31acf6b0f673bd1cf725b3ad08ccef7a1b06. * fix regression bugs introduced in PR 214 --- .../command/TranslateCommand.java | 72 ++++++++++++++++--- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/efsity-cli/src/main/java/org/smartregister/command/TranslateCommand.java b/efsity-cli/src/main/java/org/smartregister/command/TranslateCommand.java index 06ea0623..6758b00c 100644 --- a/efsity-cli/src/main/java/org/smartregister/command/TranslateCommand.java +++ b/efsity-cli/src/main/java/org/smartregister/command/TranslateCommand.java @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Properties; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; import org.smartregister.util.FCTConstants; import org.smartregister.util.FctUtils; import picocli.CommandLine; @@ -77,18 +78,26 @@ public void run() { long start = System.currentTimeMillis(); Path inputFilePath = Paths.get(resourceFile); - FctUtils.printInfo("Starting extraction"); FctUtils.printInfo(String.format("Input file \u001b[35m%s\u001b[0m", resourceFile)); try { - - if (Objects.equals(extractionType, "configs")) { - tempsConfig = Files.createTempDirectory("configs"); - } else tempsConfig = null; + if (Objects.equals(translationFile, null)) { + Path translationsDirectoryPath = getTranslationDirectoryPath(inputFilePath); + String defaultTranslationFile = translationsDirectoryPath + "/strings_default.properties"; + if (!Files.exists(translationsDirectoryPath)) { + Files.createDirectories(translationsDirectoryPath); + Files.createFile(Paths.get(defaultTranslationFile)); + } + translationFile = defaultTranslationFile; + } + tempsConfig = Files.createTempDirectory("configs"); // Check if the input path is a directory or a JSON file if (Files.isDirectory(inputFilePath)) { - if (Objects.equals(extractionType, "configs") || inputFilePath.endsWith("configs")) { + if ("configs".equals(extractionType) || inputFilePath.endsWith("configs")) { + // handle case where extractionType has not been given and inputFilePath ends with + // configs + if (Objects.equals(extractionType, null)) extractionType = "configs"; Set targetFields = FCTConstants.configTranslatables; copyDirectoryContent(inputFilePath, tempsConfig); extractContent(translationFile, inputFilePath, targetFields, extractionType); @@ -108,6 +117,7 @@ public void run() { if (Files.exists(configsPath) && Files.isDirectory(configsPath)) { extractionType = "configs"; + copyDirectoryContent(configsPath, tempsConfig); Set targetFields = FCTConstants.configTranslatables; extractContent(translationFile, configsPath, targetFields, extractionType); } else { @@ -201,6 +211,42 @@ public void run() { } } + /* + assuming the expected folder structure is always as follows + root/ + ├── configs/ + │ ├── translation/ + │ ├── file1.json + │ └── file2.json + ├── fhir_content/ + │ ├── translation/ + │ ├── example1.json + │ └── example2.json + ├── other_folder/ + │ ├── nested/ + │ │ | + │ │ ├── file3.json + │ │ └── file4.txt + └── translation/ + ├── another_file.json + + */ + @NotNull private static Path getTranslationDirectoryPath(@NotNull Path inputFilePath) { + Objects.requireNonNull(inputFilePath, "Input file path cannot be null"); + + if (inputFilePath.endsWith("configs") || inputFilePath.endsWith("fhir_content")) { + return inputFilePath.getParent().resolve("translation"); + } + if (inputFilePath.toString().endsWith(".json")) { + Path parent = inputFilePath.getParent(); + if (parent == null || parent.getParent() == null) { + throw new IllegalArgumentException("Invalid file path structure for: " + inputFilePath); + } + return parent.getParent().resolve("translation"); + } + return inputFilePath.resolve("translation"); + } + private static void mergeContent( Path inputFilePath, String translationFile, String locale, Set targetFields) throws IOException, NoSuchAlgorithmException { @@ -374,14 +420,18 @@ private void extractContent( } else if (Files.isDirectory(inputFilePath)) { // Handle the case where inputFilePath is a directory (folders may contain multiple JSON // files) + Path inputDir; + if (extractionType.equals("configs")) { + inputDir = tempsConfig; + } else inputDir = inputFilePath; - Files.walk(tempsConfig) + Files.walk(inputDir) .filter(Files::isRegularFile) .filter(file -> file.toString().endsWith(".json")) .forEach( file -> { try { - if (Objects.equals(extractionType, "configs")) { + if ("configs".equals(extractionType)) { // Extract and replace target fields with hashed values ObjectMapper objectMapper = new ObjectMapper(); JsonNode rootNode = @@ -398,7 +448,8 @@ private void extractContent( processJsonFile(file, textToHash, targetFields); } } catch (IOException | NoSuchAlgorithmException e) { - deleteDirectoryRecursively(tempsConfig); + if (tempsConfig != null && Files.exists(tempsConfig)) + deleteDirectoryRecursively(tempsConfig); throw new RuntimeException( "Error while reading file " + file.getFileName() + " " + e); } @@ -416,13 +467,14 @@ private void extractContent( } } + if (!Files.exists(propertiesFilePath)) Files.createFile(propertiesFilePath); Properties existingProperties = FctUtils.readPropertiesFile(propertiesFilePath.toString()); // Merge existing properties with new properties existingProperties.putAll(textToHash); writePropertiesFile(existingProperties, translationFile); FctUtils.printInfo(String.format("Translation file\u001b[36m %s \u001b[0m", translationFile)); - if (tempsConfig != null) deleteDirectoryRecursively(tempsConfig); + if (tempsConfig != null && Files.exists(tempsConfig)) deleteDirectoryRecursively(tempsConfig); } private static void processJsonFile(