Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

O3-614: Added support for AMPATH Forms Translations. #191

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The configuration folder is subdivided into _domain_ subfolders:
configuration/
├── addresshierarchy/
├── ampathforms/
├── ampathformstranslations/
├── appointmentspecialities/
├── appointmentservicedefinitions/
├── appointmentservicetypes/
Expand Down Expand Up @@ -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?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public enum Domain {
FHIR_CONCEPT_SOURCES,
FHIR_PATIENT_IDENTIFIER_SYSTEMS,
AMPATH_FORMS,
AMPATH_FORMS_TRANSLATIONS,
HTML_FORMS;

public int getOrder() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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.io.FilenameUtils;
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<String, Object> jsonTranslationsDefinition = new ObjectMapper().readValue(jsonTranslationsString, Map.class);

String formResourceUuid = (String) jsonTranslationsDefinition.get("uuid");
if (StringUtils.isBlank(formResourceUuid)) {
throw new Exception("Uuid is required for AMPATH forms translations loader");
}

String jsonTranlsationsFileName = FilenameUtils.removeExtension(file.getName());
Ruhanga marked this conversation as resolved.
Show resolved Hide resolved

String language = (String) jsonTranslationsDefinition.get("language");
if (StringUtils.isBlank(language)
&& !language.equalsIgnoreCase(jsonTranlsationsFileName.split("_translations_")[1])) {
throw new Exception(
"'language' property is required for AMPATH forms translations loader and should align with locale appended to the file name.");
Ruhanga marked this conversation as resolved.
Show resolved Hide resolved
}

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");
form = formService.getForm(formName);
if (form == null) {
throw new RuntimeException("No AMPATH form exists for AMPATH form tranlsations file: " + file.getName()
+ ". An existing form name should be specified on the 'form' property");
}
}

formResource.setForm(form);
formResource.setName(form.getName() + "_translations_" + language);
formResource.setDatatypeClassname("org.openmrs.customdatatype.datatype.LongFreeTextDatatype");
formResource.setValue(jsonTranslationsString);
formService.saveFormResource(formResource);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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.Assert;
import org.junit.FixMethodOrder;
import org.junit.runners.MethodSorters;
import org.junit.Test;
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;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class AmpathFormsTranslationsLoaderIntegrationTest extends DomainBaseModuleContextSensitiveTest {

private static final String RESOURCE_UUID = "c5bf3efe-3798-4052-8dcb-09aacfcbabdc";

@Autowired
private AmpathFormsTranslationsLoader ampathFormsTranslationsLoader;


@Autowired
private AmpathFormsLoader ampathFormsLoader;

@Autowired
private FormService formService;

@Test
public void load_shouldLoadAFormTranslationsFileWithAllAttributesSpecifiedAsFormResource() throws Exception {
// Setup
ampathFormsLoader.load();

// Replay
ampathFormsTranslationsLoader.load();

FormResource formResource = formService.getFormResourceByUuid(RESOURCE_UUID);
Ruhanga marked this conversation as resolved.
Show resolved Hide resolved

// Verify clob
Ruhanga marked this conversation as resolved.
Show resolved Hide resolved
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").toString());
Ruhanga marked this conversation as resolved.
Show resolved Hide resolved
Assert.assertEquals("\"French Translations\"", actualObj.get("description").toString());
Assert.assertEquals("\"fr\"", actualObj.get("language").toString());
Assert.assertEquals("\"Encontre\"", actualObj.get("translations").get("Encounter").toString());
Assert.assertEquals("\"Autre\"", actualObj.get("translations").get("Other").toString());
Assert.assertEquals("\"Enfant\"", actualObj.get("translations").get("Child").toString());

}

@Test
public void load_shouldLoadAndUpdateAFormTranslationsFileAsFormResource() throws Exception {
// Setup
ampathFormsLoader.load();

// Test that initial version loads in with expected values
// Replay
ampathFormsTranslationsLoader.load();

FormResource formResource = formService.getFormResourceByUuid(RESOURCE_UUID);

// Verify clob
Ruhanga marked this conversation as resolved.
Show resolved Hide resolved
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").toString());
Assert.assertEquals("\"French Translations\"", ampathTranslations.get("description").toString());
Assert.assertEquals("\"fr\"", ampathTranslations.get("language").toString());
Assert.assertEquals("\"Encontre\"", ampathTranslations.get("translations").get("Encounter").toString());
Assert.assertEquals("\"Autre\"", ampathTranslations.get("translations").get("Other").toString());
Assert.assertEquals("\"Enfant\"", ampathTranslations.get("translations").get("Child").toString());

String test_file_updated = "src/test/resources/testdata/testAmpathformstranslations/test_form_updated_translations_fr.json";
Ruhanga marked this conversation as resolved.
Show resolved Hide resolved
File srcFile = new File(test_file_updated);
File dstFile = new File(
ampathFormsTranslationsLoader.getDirUtil().getDomainDirPath() + "/test_form_translations_fr.json");

FileUtils.copyFile(srcFile, dstFile);

// Replay
ampathFormsTranslationsLoader.load();
FormResource formResourceUpdated = formService.getFormResourceByUuid(RESOURCE_UUID);

// Verify clob changed
Assert.assertNotNull(formResourceUpdated);
ObjectMapper mapperUpdated = new ObjectMapper();
JsonNode ampathTranslationsUpdated = mapperUpdated.readTree((String)formResourceUpdated.getValue());
Assert.assertEquals("\"c5bf3efe-3798-4052-8dcb-09aacfcbabdc\"", ampathTranslationsUpdated.get("uuid").toString());
Assert.assertEquals("\"French Translations Updated\"", ampathTranslationsUpdated.get("description").toString());
Assert.assertEquals("\"fr\"", ampathTranslationsUpdated.get("language").toString());
Assert.assertEquals("\"Tante\"", ampathTranslationsUpdated.get("translations").get("Aunt").toString());
Assert.assertEquals("\"Oncle\"", ampathTranslationsUpdated.get("translations").get("Uncle").toString());
Assert.assertEquals("\"Neveu\"", ampathTranslationsUpdated.get("translations").get("Nephew").toString());
}

}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Ruhanga marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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"
}
}
103 changes: 54 additions & 49 deletions readme/ampathforms.md
Original file line number Diff line number Diff line change
@@ -1,70 +1,75 @@
## 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",
"translations": {
"en": ["global-uuid-resource-en", "test-form-en"],
"fr": ["global-fr-uuid-resource", "test-form-fr"]
},
"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 by default a reference to the encounter type. 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.
Ruhanga marked this conversation as resolved.
Show resolved Hide resolved

**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).
36 changes: 36 additions & 0 deletions readme/ampathformstranslations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## 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 `<form_name>_translations_<locale>`.
Ruhanga marked this conversation as resolved.
Show resolved Hide resolved
* The UUID must match the identifiers specified in the form's schema, as shown [here](../readme/ampathforms.md) in the translations property, eg:
```json
"translations": {
"en": ["global-uuid-resource-en", "test-form-en"],
"fr": ["global-fr-uuid-resource-fr", "test-form-fr"]
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need that even when using the form resource?
I was under the impression that saving the translations as form resources would avoid having to specify the said translation file UUID in the form.

Copy link
Member

@Ruhanga Ruhanga Dec 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct @rbuisson. Specifying this form translation uuid is not a requirement. To make this non umbigious, let me remove this section.

```

#### Further examples:
Please look at the test configuration folder for sample import files for all domains, see [here](../api/src/test/resources/testAppDataDir/configuration).