diff --git a/README.md b/README.md index 7ecf62f8a..ae25585bf 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The configuration folder is subdivided into _domain_ subfolders: configuration/ ├── addresshierarchy/ ├── ampathforms/ + ├── ampathformstranslations/ ├── appointmentspecialities/ ├── appointmentservicedefinitions/ ├── appointmentservicetypes/ @@ -115,6 +116,7 @@ This is the list of currently supported domains in their loading order: 1. [FHIR Concept Sources (CSV files)](readme/fhir.md#domain-fhirconceptsources) 1. [FHIR Patient Identifier Systems (CSV Files)](readme/fhir.md#domain-fhirpatientidentifiersystems) 1. [AMPATH Forms (JSON files)](readme/ampathforms.md) +1. [AMPATH Forms Translations (JSON files)](readme/ampathformstranslations.md) 1. [HTML Forms (XML files)](readme/htmlforms.md) ### How to try it out? diff --git a/api/src/main/java/org/openmrs/module/initializer/Domain.java b/api/src/main/java/org/openmrs/module/initializer/Domain.java index 833abed01..e99a4f44d 100644 --- a/api/src/main/java/org/openmrs/module/initializer/Domain.java +++ b/api/src/main/java/org/openmrs/module/initializer/Domain.java @@ -44,6 +44,7 @@ public enum Domain { FHIR_CONCEPT_SOURCES, FHIR_PATIENT_IDENTIFIER_SYSTEMS, AMPATH_FORMS, + AMPATH_FORMS_TRANSLATIONS, HTML_FORMS; public int getOrder() { diff --git a/api/src/main/java/org/openmrs/module/initializer/api/loaders/AmpathFormsTranslationsLoader.java b/api/src/main/java/org/openmrs/module/initializer/api/loaders/AmpathFormsTranslationsLoader.java new file mode 100644 index 000000000..2c458fdd1 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/initializer/api/loaders/AmpathFormsTranslationsLoader.java @@ -0,0 +1,75 @@ +package org.openmrs.module.initializer.api.loaders; + +import java.io.File; +import java.util.Map; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.codehaus.jackson.map.ObjectMapper; +import org.openmrs.Form; +import org.openmrs.FormResource; +import org.openmrs.api.FormService; +import org.openmrs.api.context.Context; +import org.openmrs.module.initializer.Domain; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.nio.charset.StandardCharsets; + +@Component +public class AmpathFormsTranslationsLoader extends BaseFileLoader { + + @Autowired + private FormService formService; + + @Override + protected Domain getDomain() { + return Domain.AMPATH_FORMS_TRANSLATIONS; + } + + @Override + protected String getFileExtension() { + return "json"; + } + + @Override + protected void load(File file) throws Exception { + String jsonTranslationsString = FileUtils.readFileToString(file, StandardCharsets.UTF_8.toString()); + Map jsonTranslationsDefinition = new ObjectMapper().readValue(jsonTranslationsString, Map.class); + + String formResourceUuid = (String) jsonTranslationsDefinition.get("uuid"); + if (StringUtils.isBlank(formResourceUuid)) { + throw new IllegalArgumentException("Uuid is required for AMPATH forms translations loader."); + } + + String language = (String) jsonTranslationsDefinition.get("language"); + if (StringUtils.isBlank(language)) { + throw new IllegalArgumentException("'language' property is required for AMPATH forms translations loader."); + } + + Form form = null; + FormResource formResource = Context.getFormService().getFormResourceByUuid(formResourceUuid); + if (formResource == null) { + formResource = new FormResource(); + formResource.setUuid(formResourceUuid); + } else { + form = formResource.getForm(); + } + + if (form == null) { + String formName = (String) jsonTranslationsDefinition.get("form"); + if (formName == null) { + throw new IllegalArgumentException("'form' property is required for AMPATH forms translations loader."); + } + form = formService.getForm(formName); + if (form == null) { + throw new IllegalArgumentException( + "Could not find a form named '" + formName + "'. Please ensure an existing form is configured."); + } + } + + formResource.setForm(form); + formResource.setName(form.getName() + "_translations_" + language); + formResource.setDatatypeClassname("org.openmrs.customdatatype.datatype.LongFreeTextDatatype"); + formResource.setValue(jsonTranslationsString); + formService.saveFormResource(formResource); + } +} diff --git a/api/src/test/java/org/openmrs/module/initializer/api/form/AmpathFormsTranslationsLoaderIntegrationTest.java b/api/src/test/java/org/openmrs/module/initializer/api/form/AmpathFormsTranslationsLoaderIntegrationTest.java new file mode 100644 index 000000000..5eeb58eb5 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/initializer/api/form/AmpathFormsTranslationsLoaderIntegrationTest.java @@ -0,0 +1,186 @@ +package org.openmrs.module.initializer.api.form; + +import org.apache.commons.io.FileUtils; +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.map.ObjectMapper; +import org.junit.After; +import org.junit.Assert; +import org.junit.FixMethodOrder; +import org.junit.Rule; +import org.junit.runners.MethodSorters; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.openmrs.FormResource; +import org.openmrs.api.FormService; +import org.openmrs.module.initializer.DomainBaseModuleContextSensitiveTest; +import org.openmrs.module.initializer.api.loaders.AmpathFormsLoader; +import org.openmrs.module.initializer.api.loaders.AmpathFormsTranslationsLoader; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class AmpathFormsTranslationsLoaderIntegrationTest extends DomainBaseModuleContextSensitiveTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private static final String FORM_TRANSLATIONS_FOLDER_PATH = "src/test/resources/ampathformstranslations/"; + + private static final String RESOURCE_UUID = "c5bf3efe-3798-4052-8dcb-09aacfcbabdc"; + + @Autowired + private AmpathFormsTranslationsLoader ampathFormsTranslationsLoader; + + @Autowired + private AmpathFormsLoader ampathFormsLoader; + + @Autowired + private FormService formService; + + @After + public void clean() throws IOException { + + // Delete created form files + FileUtils.deleteDirectory(new File(FORM_TRANSLATIONS_FOLDER_PATH)); + FileUtils.deleteQuietly(new File( + ampathFormsTranslationsLoader.getDirUtil().getDomainDirPath() + "/test_ampath_translations_updated.json")); + } + + @Test + public void load_shouldLoadAFormTranslationsFileWithAllAttributesSpecifiedAsFormResource() throws Exception { + // Setup + ampathFormsLoader.load(); + + // Replay + ampathFormsTranslationsLoader.load(); + + // Verify + FormResource formResource = formService.getFormResourceByUuid(RESOURCE_UUID); + Assert.assertNotNull(formResource); + Assert.assertEquals("c5bf3efe-3798-4052-8dcb-09aacfcbabdc", formResource.getUuid()); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode actualObj = mapper.readTree((String) formResource.getValue()); + Assert.assertEquals("c5bf3efe-3798-4052-8dcb-09aacfcbabdc", actualObj.get("uuid").getTextValue()); + Assert.assertEquals("French Translations", actualObj.get("description").getTextValue()); + Assert.assertEquals("fr", actualObj.get("language").getTextValue()); + Assert.assertEquals("Encontre", actualObj.get("translations").get("Encounter").getTextValue()); + Assert.assertEquals("Autre", actualObj.get("translations").get("Other").getTextValue()); + Assert.assertEquals("Enfant", actualObj.get("translations").get("Child").getTextValue()); + + } + + @Test + public void load_shouldLoadAndUpdateAFormTranslationsFileAsFormResource() throws Exception { + // Setup + ampathFormsLoader.load(); + + // Replay + // Test that initial version loads in with expected values + ampathFormsTranslationsLoader.load(); + + FormResource formResource = formService.getFormResourceByUuid(RESOURCE_UUID); + + // Verify + Assert.assertNotNull(formResource); + Assert.assertEquals("c5bf3efe-3798-4052-8dcb-09aacfcbabdc", formResource.getUuid()); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode ampathTranslations = mapper.readTree((String) formResource.getValue()); + Assert.assertEquals("c5bf3efe-3798-4052-8dcb-09aacfcbabdc", ampathTranslations.get("uuid").getTextValue()); + Assert.assertEquals("French Translations", ampathTranslations.get("description").getTextValue()); + Assert.assertEquals("fr", ampathTranslations.get("language").getTextValue()); + Assert.assertEquals("Encontre", ampathTranslations.get("translations").get("Encounter").getTextValue()); + Assert.assertEquals("Autre", ampathTranslations.get("translations").get("Other").getTextValue()); + Assert.assertEquals("Enfant", ampathTranslations.get("translations").get("Child").getTextValue()); + + String test_file_updated = "src/test/resources/testdata/testAmpathformstranslations/test_form_updated_translations_fr.json"; + File srcFile = new File(test_file_updated); + File dstFile = new File( + ampathFormsTranslationsLoader.getDirUtil().getDomainDirPath() + "/test_form_translations_fr.json"); + + FileUtils.copyFile(srcFile, dstFile); + + // Replay + // Now load updated values + ampathFormsTranslationsLoader.load(); + FormResource formResourceUpdated = formService.getFormResourceByUuid(RESOURCE_UUID); + + // Verify + Assert.assertNotNull(formResourceUpdated); + ObjectMapper mapperUpdated = new ObjectMapper(); + JsonNode ampathTranslationsUpdated = mapperUpdated.readTree((String) formResourceUpdated.getValue()); + Assert.assertEquals("c5bf3efe-3798-4052-8dcb-09aacfcbabdc", ampathTranslationsUpdated.get("uuid").getTextValue()); + Assert.assertEquals("French Translations Updated", ampathTranslationsUpdated.get("description").getTextValue()); + Assert.assertEquals("fr", ampathTranslationsUpdated.get("language").getTextValue()); + Assert.assertEquals("Tante", ampathTranslationsUpdated.get("translations").get("Aunt").getTextValue()); + Assert.assertEquals("Oncle", ampathTranslationsUpdated.get("translations").get("Uncle").getTextValue()); + Assert.assertEquals("Neveu", ampathTranslationsUpdated.get("translations").get("Nephew").getTextValue()); + } + + @Test + public void load_shouldThrowGivenInvalidFormAssociatedWithFormTranslations() throws Exception { + // Setup + thrown.expectMessage( + "IllegalArgumentException: Could not find a form named 'Test Form 1'. Please ensure an existing form is configured."); + + // Replay + ampathFormsTranslationsLoader.loadUnsafe(Collections.emptyList(), true); + + } + + @Test + public void load_shouldThrowGivenMissingUuidPropertyInFormTranslationsDef() throws Exception { + // Setup + thrown.expectMessage("IllegalArgumentException: Uuid is required for AMPATH forms translations loader."); + + String missingUuidTranslationDefFile = "src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_uuid_translations_fr.json"; + File srcFile = new File(missingUuidTranslationDefFile); + File dstFile = new File( + ampathFormsTranslationsLoader.getDirUtil().getDomainDirPath() + "/test_form_translations_fr.json"); + + FileUtils.copyFile(srcFile, dstFile); + + // Replay + ampathFormsTranslationsLoader.loadUnsafe(Collections.emptyList(), true); + + } + + @Test + public void load_shouldThrowGivenMissingFormFieldInFormTranslationsDef() throws Exception { + // Setup + thrown.expectMessage("IllegalArgumentException: 'form' property is required for AMPATH forms translations loader."); + + String missingUuidTranslationDefFile = "src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_formName_translations_fr.json"; + File srcFile = new File(missingUuidTranslationDefFile); + File dstFile = new File( + ampathFormsTranslationsLoader.getDirUtil().getDomainDirPath() + "/test_form_translations_fr.json"); + + FileUtils.copyFile(srcFile, dstFile); + + // Replay + ampathFormsTranslationsLoader.loadUnsafe(Collections.emptyList(), true); + + } + + @Test + public void load_shouldThrowGivenMissingLanguageFieldInFormTranslationsDef() throws Exception { + // Setup + thrown.expectMessage( + "IllegalArgumentException: 'language' property is required for AMPATH forms translations loader."); + + String missingUuidTranslationDefFile = "src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_language_translations_fr.json"; + File srcFile = new File(missingUuidTranslationDefFile); + File dstFile = new File( + ampathFormsTranslationsLoader.getDirUtil().getDomainDirPath() + "/test_form_translations_fr.json"); + + FileUtils.copyFile(srcFile, dstFile); + + // Replay + ampathFormsTranslationsLoader.loadUnsafe(Collections.emptyList(), true); + + } +} diff --git a/api/src/test/resources/testAppDataDir/configuration/ampathformstranslations/test_form_translations_fr.json b/api/src/test/resources/testAppDataDir/configuration/ampathformstranslations/test_form_translations_fr.json new file mode 100644 index 000000000..6715b962f --- /dev/null +++ b/api/src/test/resources/testAppDataDir/configuration/ampathformstranslations/test_form_translations_fr.json @@ -0,0 +1,11 @@ +{ + "uuid" : "c5bf3efe-3798-4052-8dcb-09aacfcbabdc", + "form" : "Test Form 1", + "description" : "French Translations", + "language" : "fr", + "translations" : { + "Encounter" : "Encontre", + "Other" : "Autre", + "Child" : "Enfant" + } +} \ No newline at end of file diff --git a/api/src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_formName_translations_fr.json b/api/src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_formName_translations_fr.json new file mode 100644 index 000000000..0b27e1ed0 --- /dev/null +++ b/api/src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_formName_translations_fr.json @@ -0,0 +1,10 @@ +{ + "uuid" : "c5bf3efe-3798-4052-8dcb-09aacfcbabdc", + "description" : "French Translations", + "language" : "fr", + "translations" : { + "Encounter" : "Encontre", + "Other" : "Autre", + "Child" : "Enfant" + } +} \ No newline at end of file diff --git a/api/src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_language_translations_fr.json b/api/src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_language_translations_fr.json new file mode 100644 index 000000000..3f8c81784 --- /dev/null +++ b/api/src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_language_translations_fr.json @@ -0,0 +1,10 @@ +{ + "uuid" : "c5bf3efe-3798-4052-8dcb-09aacfcbabdc", + "form" : "Test Form 1", + "description" : "French Translations", + "translations" : { + "Encounter" : "Encontre", + "Other" : "Autre", + "Child" : "Enfant" + } +} \ No newline at end of file diff --git a/api/src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_uuid_translations_fr.json b/api/src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_uuid_translations_fr.json new file mode 100644 index 000000000..b43eee3e9 --- /dev/null +++ b/api/src/test/resources/testdata/testAmpathformstranslations/invalid_form_missing_uuid_translations_fr.json @@ -0,0 +1,10 @@ +{ + "form" : "Test Form 1", + "description" : "French Translations", + "language" : "fr", + "translations" : { + "Encounter" : "Encontre", + "Other" : "Autre", + "Child" : "Enfant" + } +} \ No newline at end of file diff --git a/api/src/test/resources/testdata/testAmpathformstranslations/test_form_updated_translations_fr.json b/api/src/test/resources/testdata/testAmpathformstranslations/test_form_updated_translations_fr.json new file mode 100644 index 000000000..8cb106bc2 --- /dev/null +++ b/api/src/test/resources/testdata/testAmpathformstranslations/test_form_updated_translations_fr.json @@ -0,0 +1,11 @@ +{ + "uuid" : "c5bf3efe-3798-4052-8dcb-09aacfcbabdc", + "form" : "Test Form 1", + "description" : "French Translations Updated", + "language" : "fr", + "translations" : { + "Aunt" : "Tante", + "Uncle" : "Oncle", + "Nephew" : "Neveu" + } +} \ No newline at end of file diff --git a/readme/ampathforms.md b/readme/ampathforms.md index a2b84aa4e..d73295686 100644 --- a/readme/ampathforms.md +++ b/readme/ampathforms.md @@ -1,70 +1,71 @@ ## Domain 'ampathforms' -The **ampathforms** subfolder contains [AMPATH Forms](https://ampath-forms.vercel.app/) JSON schema files. Each JSON file defines the schema for a different form. For example, +The **ampathforms** subfolder contains [AMPATH Forms](https://ampath-forms.vercel.app/) JSON schema files. Each JSON file defines the schema for a different form. This is an example of how this folder's content may look like for two forms being defined: ```bash ampathforms/ - └── form1.json + ├── form1.json └── form2.json ``` ###### JSON file example: + ```json { - "name" : "Test Form 1", - "description" : "Test 1 Description", - "version" : "1", - "published" : true, - "retired" : false, - "encounter" : "Emergency", - "pages" : [ + "name": "Test Form 1", + "description": "Test 1 Description", + "version": "1", + "published": true, + "retired": false, + "encounter": "Emergency", + "pages": [ + { + "label": "Page 1", + "sections": [ { - "label" : "Page 1", - "sections" : [ - { - "label" : "Section 1", - "isExpanded" : "true", - "questions" : [ - { - "label" : "Height", - "type" : "obs", - "questionOptions" : { - "rendering" : "number", - "concept" : "5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "max" : "", - "min" : "", - "showDate" : "", - "conceptMappings" : [ - { - "type" : "CIEL", - "value" : "5090" - }, - { - "type" : "AMPATH", - "value" : "5090" - }, - { - "type" : "PIH", - "value" : "5090" - } - ] - }, - "id" : "Ht" - } - ] - } - ] + "label": "Section 1", + "isExpanded": "true", + "questions": [ + { + "label": "Height", + "type": "obs", + "questionOptions": { + "rendering": "number", + "concept": "5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "max": "", + "min": "", + "showDate": "", + "conceptMappings": [ + { + "type": "CIEL", + "value": "5090" + }, + { + "type": "AMPATH", + "value": "5090" + }, + { + "type": "PIH", + "value": "5090" + } + ] + }, + "id": "Ht" + } + ] } - ], - "processor" : "EncounterFormProcessor", - "referencedForms" : [ ] + ] + } + ], + "processor": "EncounterFormProcessor", + "referencedForms": [] } ``` The JSON source for these forms can be obtained from [AMPATH Form Builder](https://openmrs-spa.org/formbuilder/) by downloading the form, although it can also be written by hand. -**Note** The [AMPATH Form Builder](https://openmrs-spa.org/formbuilder/) does not include a reference to the encounter type by default. For encounter-based forms, you must ensure that you specify an `"encounter"` field with the name of the encounter type for this form or else Initializer will not be able to properly load the form. +**NOTE:** The [AMPATH Form Builder](https://openmrs-spa.org/formbuilder/) does not include a reference to the encounter type by default. For encounter-based forms, it is important to specify an `"encounter"` field with the name of the encounter type associated with the form, or else Initializer will not be able to properly load the form. -**Note** Like other form engines (and as a result of the form tooling), the unique identifier for a form is its name. As a result, the `uuid` field provided by the Form Builder is usually "xxxx" and not used. Instead, the form UUID is determined based on the form name and the form version. Any previous version of the form with the same name, however, will also be replaced with whatever form is loaded by initializer. It is therefore not recommended to combine Initializer with another mechanism for loading forms into the system, e.g., by using the Form Builder directly. +**NOTE:** Like other form engines (and as a result of the form tooling), the unique identifier for a form is its name. As a result, the `uuid` field provided by the Form Builder is usually `"xxxx"` and not used. Instead, the form UUID is determined based on the form name and the form version. Any previous version of the form with the same name, however, will also be replaced with whatever form is loaded by Initializer. It is therefore not recommended to combine Initializer with another mechanism for loading forms into the system, e.g., by using the Form Builder directly. #### Further examples: Please look at the test configuration folder for sample import files for all domains, see [here](../api/src/test/resources/testAppDataDir/configuration). diff --git a/readme/ampathformstranslations.md b/readme/ampathformstranslations.md new file mode 100644 index 000000000..be244fc4b --- /dev/null +++ b/readme/ampathformstranslations.md @@ -0,0 +1,29 @@ +## Domain 'ampathformstranslations' +The **ampathtranslationsforms** subfolder contains AMPATH Translation Forms JSON schema files. Each JSON file defines the translations for forms. For example, + +```bash +ampathformstranslations/ + ├── form1_translations_fr.json + └── form2_translations_en.json +``` + +###### JSON file example: +```json +{ + "uuid": "c5bf3efe-3798-4052-8dcb-09aacfcbabdc", + "form": "Form 1", + "description": "French Translations for Form 1", + "language": "fr", + "translations": { + "Encounter": "Rencontre", + "Other": "Autre", + "Child": "Enfant" + } +} +``` + +**NOTE:** +* The `form` attribute must be provided with an existing form name for the translations to load successfully. The translations form resources get names following the following pattern `_translations_`. + +###### Further examples: +Please look at the test configuration folder for sample import files for all domains, see [here](../api/src/test/resources/testAppDataDir/configuration).