allPossibleStates;
+
+ public ProgramConfiguration() {
+
+ }
+
+ public ProgramConfiguration(String programRef, String workflowRef, String stateRef) {
+ this.programRef = programRef;
+ this.workflowRef = workflowRef;
+ this.stateRef = stateRef;
+ }
+
+ public Program getProgram() {
+ if (resolvedConfig == null) {
+ resolvedConfig = getResolvedConfig();
+ }
+ return resolvedConfig.getProgram();
+ }
+
+ public ProgramWorkflow getWorkflow() {
+ if (resolvedConfig == null) {
+ resolvedConfig = getResolvedConfig();
+ }
+ return resolvedConfig.getWorkflow();
+ }
+
+ public ProgramWorkflowState getState() {
+ if (resolvedConfig == null) {
+ resolvedConfig = getResolvedConfig();
+ }
+ return resolvedConfig.getState();
+ }
+
+ /**
+ * Checks whether the underlying {@code program}, {@code workflow} or {@code state} are under the same program tree
+ *
+ *
+ * For instance if a {@link ProgramConfiguration} has a {@code resolvedConfig} with a {@code program}, {@code workflow}
+ * and {@code state}; this method determines whether the {@code state} is associated with the {@code workflow} and if
+ * the {@code workflow} is also associated with the {@code program}.
+ *
+ * @return {@code true} if a valid program tree was found
+ */
+ public boolean hasValidProgramTree() {
+ Program program = getProgram();
+ ProgramWorkflow workflow = getWorkflow();
+ ProgramWorkflowState state = getState();
+ // Only program was specified
+ if (program != null && workflow == null && state == null) {
+ return true;
+ }
+ // Only workflow was specified
+ if (program == null && workflow != null && state == null) {
+ return true;
+ }
+ // Only state was specified
+ if (program == null && workflow == null && state != null) {
+ return true;
+ }
+ // For cases where only workflow and state were specified, be sure the state belongs to the specified workflow
+ if (program == null && workflow != null && state != null) {
+ return workflow.getStates().contains(state);
+ }
+ // For cases where only program and state were specified, be sure the state belongs to a workflow associated with program
+ if (program != null && workflow == null && state != null) {
+ boolean stateInProgramTree = false;
+ for (ProgramWorkflow candidate : program.getAllWorkflows()) {
+ if (candidate.getStates().contains(state)) {
+ stateInProgramTree = true;
+ }
+ }
+ return stateInProgramTree;
+ }
+ // For cases where a workflow and program were specified, be sure the workflow belongs to the specified program
+ if (program != null && workflow != null) {
+ boolean programHasWorkflow = program.getAllWorkflows().contains(workflow);
+ if (state != null) {
+ // For cases where a workflow, program and state were specified
+ return programHasWorkflow && workflow.getStates().contains(state);
+ }
+ return programHasWorkflow;
+ }
+ return false;
+ }
+
+ protected ResolvedConfiguration getResolvedConfig() {
+ if (resolvedConfig != null) {
+ return resolvedConfig;
+ }
+ ResolvedConfiguration ret = new ResolvedConfiguration();
+ if (StringUtils.isNotBlank(programRef)) {
+ List programs = getAllPossiblePrograms();
+ if (programs.size() == 1) {
+ ret.setProgram(programs.get(0));
+ } else if (programs.size() > 1) {
+ ret.setProgram(programWithBestWorflowAndStateCombination(programs));
+ }
+ }
+ if (StringUtils.isNotBlank(workflowRef)) {
+ List workflows = getAllPossibleWorkflows();
+ if (workflows.size() == 1) {
+ ret.setWorkflow(workflows.get(0));
+ } else if (workflows.size() > 1) {
+ ret.setWorkflow(workflowWithBestProgramAndStateCombination(workflows, ret.getProgram()));
+ }
+ }
+ if (StringUtils.isNotBlank(stateRef)) {
+ List states = getAllPossibleStates();
+ if (states.size() == 1) {
+ ret.setState(states.get(0));
+ } else if (states.size() > 1) {
+ ret.setState(stateWithBestProgramAndWorkflowCombination(states, ret.getProgram(), ret.getWorkflow()));
+ }
+ }
+ return ret;
+ }
+
+ protected Program programWithBestWorflowAndStateCombination(List programsSharingConcept) {
+ Set candidates = new HashSet();
+ for (Program program : programsSharingConcept) {
+ if (StringUtils.isNotBlank(workflowRef) && allPossibleWorkflows == null) {
+ allPossibleWorkflows = getAllPossibleWorkflows();
+ }
+ if (CollectionUtils.isNotEmpty(allPossibleWorkflows)) {
+ for (ProgramWorkflow workflow : allPossibleWorkflows) {
+ if (program.getAllWorkflows().contains(workflow)) {
+ candidates.add(program);
+ }
+ }
+ }
+ }
+ if (candidates.size() == 1) {
+ return candidates.iterator().next();
+ }
+ if (StringUtils.isNotBlank(stateRef) && candidates.size() > 1) {
+ programsSharingConcept.clear();
+ programsSharingConcept.addAll(candidates);
+ candidates.clear();
+ for (Program candidate : programsSharingConcept) {
+ if (StringUtils.isNotBlank(stateRef) && allPossibleStates == null) {
+ allPossibleStates = getAllPossibleStates();
+ }
+ for (ProgramWorkflowState state : allPossibleStates) {
+ for (ProgramWorkflow workflow : candidate.getAllWorkflows()) {
+ if (workflow.getSortedStates().contains(state)) {
+ candidates.add(candidate);
+ }
+ }
+ }
+ }
+ if (candidates.size() == 1) {
+ return candidates.iterator().next();
+ }
+ }
+ // If we didn't get any qualifying candidate or got more than one
+ throw new APIException("Could not choose the intended program out of the many programs identified by concept: " + programRef);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected ProgramWorkflow workflowWithBestProgramAndStateCombination(List workflowsSharingConcept,
+ Program underlyingProgram) {
+ Collection candidates = new HashSet();
+ // Look at the states and try to find out the best combination(s)
+ if (StringUtils.isNotBlank(stateRef)) {
+ for (ProgramWorkflow candidate : workflowsSharingConcept) {
+ if (allPossibleStates == null) {
+ allPossibleStates = getAllPossibleStates();
+ }
+ for (ProgramWorkflowState state : allPossibleStates) {
+ if (candidate.getSortedStates().contains(state)) {
+ candidates.add(candidate);
+ }
+ }
+ }
+ }
+ if (candidates.size() == 1) {
+ return candidates.iterator().next();
+ } else if (candidates.size() > 1) {
+ // Move up to the underlying program and see whether we can have an outstanding combination
+ if (underlyingProgram != null) {
+ candidates = CollectionUtils.intersection(candidates, underlyingProgram.getAllWorkflows());
+ if (candidates.size() == 1) {
+ return candidates.iterator().next();
+ }
+ }
+ }
+ // If we didn't get any qualifying candidate or got more than one
+ throw new APIException("Could not choose the intended workflow out of the many workflows identified by concept: " + workflowRef);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected ProgramWorkflowState stateWithBestProgramAndWorkflowCombination(List statesSharingConcept,
+ Program underlyingProgram, ProgramWorkflow underlyingWorkflow) {
+ Collection candidates = new HashSet();
+ // Try combining with the underlying workflow
+ if (underlyingWorkflow != null) {
+ candidates = CollectionUtils.intersection(statesSharingConcept, underlyingWorkflow.getSortedStates());
+ if (candidates.size() == 1) {
+ return candidates.iterator().next();
+ }
+ }
+ // Try combining with the underlying program
+ if (underlyingProgram != null) {
+ Set allProgramStates = new HashSet();
+ for (ProgramWorkflow workflow : underlyingProgram.getAllWorkflows()) {
+ allProgramStates.addAll(workflow.getSortedStates());
+ }
+ if (CollectionUtils.isNotEmpty(candidates)) {
+ candidates = CollectionUtils.intersection(candidates, allProgramStates);
+ } else {
+ candidates = CollectionUtils.intersection(statesSharingConcept, allProgramStates);
+ }
+ if (candidates.size() == 1) {
+ return candidates.iterator().next();
+ }
+ }
+ // If we didn't get any qualifying candidate or got more than one
+ throw new APIException("Could not choose the intended state out of the many states identified by concept: " + stateRef);
+ }
+
+ /**
+ * Gets program(s) identified by the {@link #programRef}
+ *
+ * @should get program by it's {@code id}
+ * @should get program by it's {@code uuid}
+ * @should get program(s) by the associated {@code concept}
+ */
+ private List getAllPossiblePrograms() {
+ try {
+ Integer programId = Integer.parseInt(programRef);
+ Program program = Context.getProgramWorkflowService().getProgram(programId);
+ if (program != null) {
+ return Arrays.asList(program);
+ }
+ } catch (NumberFormatException e) {
+ // try with the uuid
+ Program program = Context.getProgramWorkflowService().getProgramByUuid(programRef);
+ if (program != null) {
+ return Arrays.asList(program);
+ }
+ }
+ // try using the concept
+ Concept concept = AppFrameworkUtil.getConcept(programRef);
+ if (concept == null) {
+ throw new APIException("Could not find concept identified by: " + programRef);
+ }
+ List programs = AppFrameworkUtil.getProgramsByConcept(concept);
+ if (programs.isEmpty()) {
+ throw new APIException("Could not find program(s) identified by concept: " + concept);
+ }
+ return programs;
+ }
+
+ /**
+ * Gets workflow(s) identified by the {@link #workflowRef}
+ *
+ * @should get workflow by it's {@code id}
+ * @should get workflow by it's {@code uuid}
+ * @should get workflow(s) by the associated {@code concept}
+ */
+ private List getAllPossibleWorkflows() {
+ try {
+ Integer workflowId = Integer.parseInt(workflowRef);
+ ProgramWorkflow workflow = Context.getProgramWorkflowService().getWorkflow(workflowId);
+ if (workflow != null) {
+ return Arrays.asList(workflow);
+ }
+ } catch (NumberFormatException e) {
+ // try with the uuid
+ ProgramWorkflow workflow = Context.getProgramWorkflowService().getWorkflowByUuid(workflowRef);
+ if (workflow != null) {
+ return Arrays.asList(workflow);
+ }
+ }
+ Concept workflowConcept = AppFrameworkUtil.getConcept(workflowRef);
+ if (workflowConcept == null) {
+ throw new APIException("Could not find concept identified by: " + workflowRef);
+ }
+ List workflows = AppFrameworkUtil.getWorkflowsByConcept(workflowConcept);
+ if (CollectionUtils.isEmpty(workflows)) {
+ throw new APIException("Could not find workflow(s) identified by concept: " + workflowConcept);
+ }
+ return workflows;
+ }
+
+ /**
+ * Gets state(s) identified by the {@link #stateRef}
+ *
+ * @should get state by it's {@code id}
+ * @should get state by it's {@code uuid}
+ * @should get state(s) by the associated {@code concept}
+ */
+ private List getAllPossibleStates() {
+ try {
+ Integer stateId = Integer.parseInt(stateRef);
+ ProgramWorkflowState state = Context.getProgramWorkflowService().getState(stateId);
+ if (state != null) {
+ return Arrays.asList(state);
+ }
+ } catch (NumberFormatException e) {
+ // try with the uuid
+ ProgramWorkflowState state = Context.getProgramWorkflowService().getStateByUuid(stateRef);
+ if (state != null) {
+ return Arrays.asList(state);
+ }
+ }
+ Concept stateConcept = AppFrameworkUtil.getConcept(stateRef);
+ if (stateConcept == null) {
+ throw new APIException("Could not find concept identified by: " + stateRef);
+ }
+ List states = AppFrameworkUtil.getStatesByConcept(stateConcept);
+ if (CollectionUtils.isEmpty(states)) {
+ throw new APIException("Could not find state(s) identified by concept: " + stateConcept);
+ }
+ return states;
+ }
+
+ public void setResolvedConfig(ResolvedConfiguration resolvedConfig) {
+ this.resolvedConfig = resolvedConfig;
+ }
+
+ public boolean hasProgram() {
+ return getResolvedConfig().getProgram() != null;
+ }
+
+ public boolean hasWorkflow() {
+ return getResolvedConfig().getWorkflow() != null;
+ }
+
+ public boolean hasState() {
+ return getResolvedConfig().getState() != null;
+ }
+
+ public boolean hasAll() {
+ return hasProgram() && hasWorkflow() && hasState();
+ }
+
+ public static class ResolvedConfiguration {
+
+ private Program program;
+
+ private ProgramWorkflow workflow;
+
+ private ProgramWorkflowState state;
+
+ public ResolvedConfiguration() {
+
+ }
+
+ public ResolvedConfiguration(Program program, ProgramWorkflow workflow, ProgramWorkflowState state) {
+ this.program = program;
+ this.workflow = workflow;
+ this.state = state;
+ }
+
+ public Program getProgram() {
+ return program;
+ }
+
+ public void setProgram(Program program) {
+ this.program = program;
+ }
+
+ public ProgramWorkflow getWorkflow() {
+ return workflow;
+ }
+
+ public void setWorkflow(ProgramWorkflow workflow) {
+ this.workflow = workflow;
+ }
+
+ public ProgramWorkflowState getState() {
+ return state;
+ }
+
+ public void setState(ProgramWorkflowState state) {
+ this.state = state;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/openmrs/module/appframework/domain/Extension.java b/api/src/main/java/org/openmrs/module/appframework/domain/Extension.java
index f35afa3..c9c926f 100644
--- a/api/src/main/java/org/openmrs/module/appframework/domain/Extension.java
+++ b/api/src/main/java/org/openmrs/module/appframework/domain/Extension.java
@@ -5,9 +5,11 @@
import org.codehaus.jackson.annotate.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import org.openmrs.api.context.Context;
+import org.openmrs.module.appframework.context.ProgramConfiguration;
import org.openmrs.module.appframework.domain.validators.ValidationErrorMessages;
import org.openmrs.module.appframework.template.TemplateFactory;
+import java.util.List;
import java.util.Map;
public class Extension implements Comparable {
@@ -52,6 +54,9 @@ public class Extension implements Comparable {
@JsonProperty
protected Map extensionParams;
+ @JsonProperty
+ protected List requiredPrograms;
+
/**
* Will be set by {@link org.openmrs.module.appframework.AppFrameworkActivator} if this extension is defined within
* an app
@@ -280,4 +285,12 @@ TemplateFactory getTemplateFactory() {
return Context.getRegisteredComponent("appframeworkTemplateFactory", TemplateFactory.class);
}
-}
+ public List getRequiredPrograms() {
+ return requiredPrograms;
+ }
+
+ public void setRequiredPrograms(List requiredPrograms) {
+ this.requiredPrograms = requiredPrograms;
+ }
+
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java b/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java
index 21e609c..fed4008 100644
--- a/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java
+++ b/api/src/main/java/org/openmrs/module/appframework/service/AppFrameworkServiceImpl.java
@@ -18,6 +18,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.script.Bindings;
import javax.script.ScriptContext;
@@ -33,6 +34,13 @@
import org.codehaus.jackson.map.ObjectMapper;
import org.openmrs.Location;
import org.openmrs.LocationTag;
+import org.openmrs.Patient;
+import org.openmrs.PatientProgram;
+import org.openmrs.PatientState;
+import org.openmrs.Program;
+import org.openmrs.ProgramWorkflow;
+import org.openmrs.ProgramWorkflowState;
+import org.openmrs.api.APIException;
import org.openmrs.api.LocationService;
import org.openmrs.api.context.Context;
import org.openmrs.api.context.UserContext;
@@ -42,6 +50,7 @@
import org.openmrs.module.appframework.LoginLocationFilter;
import org.openmrs.module.appframework.config.AppFrameworkConfig;
import org.openmrs.module.appframework.context.AppContextModel;
+import org.openmrs.module.appframework.context.ProgramConfiguration;
import org.openmrs.module.appframework.domain.AppDescriptor;
import org.openmrs.module.appframework.domain.AppTemplate;
import org.openmrs.module.appframework.domain.ComponentState;
@@ -294,20 +303,22 @@ public List getExtensionsForCurrentUser(String extensionPointId) {
@Override
public List getExtensionsForCurrentUser(String extensionPointId, AppContextModel contextModel) {
- List extensions = new ArrayList();
- UserContext userContext = Context.getUserContext();
-
- for (Extension candidate : getAllEnabledExtensions(extensionPointId)) {
- if ((candidate.getBelongsTo() == null
- || hasPrivilege(userContext, candidate.getBelongsTo().getRequiredPrivilege()))
- && hasPrivilege(userContext, candidate.getRequiredPrivilege())) {
- if (contextModel == null || checkRequireExpression(candidate, contextModel)) {
- extensions.add(candidate);
- }
- }
- }
-
- return extensions;
+ List extensions = new ArrayList();
+ UserContext userContext = Context.getUserContext();
+
+ for (Extension candidate : getAllEnabledExtensions(extensionPointId)) {
+ if ((candidate.getBelongsTo() == null || hasPrivilege(userContext, candidate.getBelongsTo().getRequiredPrivilege()))
+ && hasPrivilege(userContext, candidate.getRequiredPrivilege()) ) {
+ if (contextModel == null) {
+ extensions.add(candidate);
+
+ } else if (checkRequireExpression(candidate, contextModel) && checkRequiredProgramsOnCurrentPatient(candidate, contextModel)) {
+ extensions.add(candidate);
+ }
+ }
+ }
+
+ return extensions;
}
// making it public is a hack so we can test this directly in the appui module
@@ -364,8 +375,129 @@ public boolean checkRequireExpression(Extension candidate, AppContextModel conte
return false;
}
}
+
+ /**
+ * Gets the patient's uuid
from a given contextModel
.
+ *
+ * @param contextModel
+ */
+ protected String getPatientUuid(AppContextModel contextModel) {
+ Bindings bindings = new SimpleBindings();
+ for (Map.Entry e : contextModel.entrySet()) {
+ bindings.put(e.getKey(), e.getValue());
+ }
+ javascriptEngine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
+ try {
+ String uuid = (String) javascriptEngine.eval("patient.uuid;");
+ if (StringUtils.isBlank(uuid)) {
+ throw new APIException("Patient uuid cannot be empty");
+ }
+ return uuid;
+ } catch (ScriptException e) {
+ throw new APIException("Failed to get patient uuid", e);
+ }
+ }
+
+ /**
+ * Determines whether a given extension
requires a program
that's associated with
+ * the patient
in the current visit.
+ *
+ *
+ * For instance if an extension
had such a program configuration:
+ *
+ * {
+ * "programRef" : "CIEL:123",
+ * "workflowRef" : "CIEL:124",
+ * "stateRef" : "CIEL:125"
+ * }
+ *
+ * This method would return true
if patient is enrolled to program("CIEL:123") with
+ * workflow("CIEL:124") and in state("CIEL:125")
+ *
+ *
+ */
+ protected boolean checkRequiredProgramsOnCurrentPatient(Extension extension, AppContextModel contextModel) {
+ List requiredPrograms = extension.getRequiredPrograms();
+ if (CollectionUtils.isEmpty(requiredPrograms)) {
+ return true;
+ }
+ Patient patient = Context.getPatientService().getPatientByUuid(getPatientUuid(contextModel));
+
+ List patientPrograms = Context.getProgramWorkflowService().getPatientPrograms(patient, null, null,
+ null, null, null, false);
+ if (CollectionUtils.isEmpty(patientPrograms)) {
+ // This patient isn't enrolled to any program
+ return false;
+ }
+
+ for (ProgramConfiguration configuration : requiredPrograms) {
+ boolean configAssignableToPatient = hasProgramAssignableToConfiguration(patientPrograms, configuration);
+ if (configAssignableToPatient) {
+ // Return early
+ return configAssignableToPatient;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determines whether a given {@link ProgramConfiguration} is assignable to any of the
+ * patientPrograms
+ *
+ * @param patientPrograms
+ * @param config
+ * @return true
if config
is assignable to any of the patientPrograms
+ */
+ protected boolean hasProgramAssignableToConfiguration(List patientPrograms,
+ ProgramConfiguration config) {
+ if (!config.hasValidProgramTree()) {
+ throw new APIException("ProgramConfiguration has an invalid program tree");
+ }
+ Program program = config.getProgram();
+ ProgramWorkflow workflow = config.getWorkflow();
+ ProgramWorkflowState state = config.getState();
+
+ List programs = new ArrayList();
+ List allWorkflows = new ArrayList();
+ List patientCurrentStates = new ArrayList();
+ for (PatientProgram patientProgram : patientPrograms) {
+ Set patientStates = patientProgram.getCurrentStates();
+ for (PatientState patientState : patientStates) {
+ patientCurrentStates.add(patientState.getState());
+ }
+ programs.add(patientProgram.getProgram());
+ allWorkflows.addAll(patientProgram.getProgram().getAllWorkflows());
+ }
+ if (config.hasProgram() && !config.hasWorkflow() && !config.hasState()) {
+ return programs.contains(config.getProgram());
+ }
+ if (!config.hasProgram() && config.hasWorkflow() && !config.hasState()) {
+ return allWorkflows.contains(workflow);
+ }
+ if (!config.hasProgram() && !config.hasWorkflow() && config.hasState()) {
+ return patientCurrentStates.contains(state);
+ }
+ if (config.hasAll()) {
+ return programs.contains(config.getProgram()) &&
+ allWorkflows.contains(workflow) &&
+ patientCurrentStates.contains(state);
+ }
+ if (config.hasWorkflow() && config.hasState()) {
+ return allWorkflows.contains(workflow) &&
+ patientCurrentStates.contains(state);
+ }
+ if (config.hasProgram() && config.hasWorkflow()) {
+ return programs.contains(program) &&
+ allWorkflows.contains(workflow);
+ }
+ if (config.hasProgram() && config.hasState()) {
+ return programs.contains(program) &&
+ patientCurrentStates.contains(state);
+ }
+ return false;
+ }
- @Override
+ @Override
public List getAppsForCurrentUser() {
List userApps = new ArrayList();
UserContext userContext = Context.getUserContext();
diff --git a/api/src/test/java/org/openmrs/module/appframework/AppFrameworkUtilTest.java b/api/src/test/java/org/openmrs/module/appframework/AppFrameworkUtilTest.java
new file mode 100644
index 0000000..15f68e9
--- /dev/null
+++ b/api/src/test/java/org/openmrs/module/appframework/AppFrameworkUtilTest.java
@@ -0,0 +1,124 @@
+package org.openmrs.module.appframework;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.openmrs.Concept;
+import org.openmrs.Program;
+import org.openmrs.ProgramWorkflow;
+import org.openmrs.ProgramWorkflowState;
+import org.openmrs.api.ConceptService;
+import org.openmrs.api.ProgramWorkflowService;
+import org.openmrs.api.context.Context;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(Context.class)
+public class AppFrameworkUtilTest {
+
+ @Mock
+ ConceptService conceptService;
+
+ @Mock
+ ProgramWorkflowService programWorkflowService;
+
+ Concept concept;
+
+ @Before
+ public void setup() {
+ concept = new Concept(1);
+
+ mockStatic(Context.class);
+ when(Context.getConceptService()).thenReturn(conceptService);
+ when(Context.getProgramWorkflowService()).thenReturn(programWorkflowService);
+ when(programWorkflowService.getAllPrograms(false)).thenReturn(setupPrograms());
+
+ }
+
+ @Test
+ public void getProgramsByConcept_shouldGetAllProgramsIdentifiedByGivenConcept() {
+ // Replay
+ List programsIdentifiedByConcept = AppFrameworkUtil.getProgramsByConcept(concept);
+
+ // Verify
+ Assert.assertThat(programsIdentifiedByConcept.size(), is(2));
+
+ }
+
+ @Test
+ public void getAllWorkflows_shouldReturnAllWorkflows() {
+ // Replay
+ List workflows = AppFrameworkUtil.getAllWorkflows();
+
+ // Verify
+ Assert.assertThat(workflows.size(), is(4));
+ }
+
+ @Test
+ public void getWorkflowsByConcept_shouldGetAllWorkflowsIdentifiedByConcept() {
+ // Replay
+ List workflows = AppFrameworkUtil.getWorkflowsByConcept(concept);
+
+ // Verify
+ Assert.assertThat(workflows.size(), is(2));
+ }
+
+ @Test
+ public void getStatesByConcept_shouldGetAllStatesIdentifiedByConcept() {
+ // Replay
+ List states = AppFrameworkUtil.getStatesByConcept(concept);
+
+ // Verify
+ Assert.assertThat(states.size(), is(1));
+ }
+
+ private List setupPrograms() {
+ Program program1 = new Program();
+ program1.setConcept(new Concept());
+ Program program2 = new Program();
+ program2.setConcept(concept);
+ Program program3 = new Program();
+ program3.setConcept(concept);
+
+ ProgramWorkflow workflow1 = new ProgramWorkflow();
+ ProgramWorkflowState state1 = new ProgramWorkflowState();
+ state1.setConcept(new Concept(8));
+ workflow1.addState(state1);
+ workflow1.setConcept(concept);
+ program1.addWorkflow(workflow1);
+
+ ProgramWorkflow workflow2 = new ProgramWorkflow();
+ ProgramWorkflowState state2 = new ProgramWorkflowState();
+ state2.setConcept(concept);
+ workflow2.addState(state2);
+ workflow2.setConcept(new Concept(3));
+ program1.addWorkflow(workflow2);
+
+ ProgramWorkflow workflow3 = new ProgramWorkflow();
+ ProgramWorkflowState state3 = new ProgramWorkflowState();
+ state3.setConcept(new Concept(9));
+ workflow3.addState(state3);
+ workflow3.setConcept(new Concept(7));
+ program3.addWorkflow(workflow3);
+
+ ProgramWorkflow workflow4 = new ProgramWorkflow();
+ ProgramWorkflowState state4 = new ProgramWorkflowState();
+ state4.setConcept(new Concept(10));
+ workflow4.addState(state4);
+ workflow4.setConcept(concept);
+ program3.addWorkflow(workflow4);
+
+ return Arrays.asList(program1, program2, program3);
+ }
+
+}
diff --git a/api/src/test/java/org/openmrs/module/appframework/context/ProgramConfigurationTest.java b/api/src/test/java/org/openmrs/module/appframework/context/ProgramConfigurationTest.java
new file mode 100644
index 0000000..5b23b97
--- /dev/null
+++ b/api/src/test/java/org/openmrs/module/appframework/context/ProgramConfigurationTest.java
@@ -0,0 +1,463 @@
+package org.openmrs.module.appframework.context;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.openmrs.Concept;
+import org.openmrs.ConceptName;
+import org.openmrs.Program;
+import org.openmrs.ProgramWorkflow;
+import org.openmrs.ProgramWorkflowState;
+import org.openmrs.api.APIException;
+import org.openmrs.api.ProgramWorkflowService;
+import org.openmrs.api.context.Context;
+import org.openmrs.module.appframework.AppFrameworkUtil;
+import org.openmrs.module.appframework.context.ProgramConfiguration.ResolvedConfiguration;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({Context.class, AppFrameworkUtil.class})
+public class ProgramConfigurationTest {
+
+ ProgramConfiguration configuration;
+
+ // Concept associated with programs
+ Concept concept1;
+
+ // Concept associated with workflows
+ Concept concept2;
+
+ // Concepts associated with states
+ @Mock
+ Concept concept3;
+
+ @Mock
+ Concept concept4;
+
+ @Mock
+ ProgramWorkflowService service;
+
+ Program program1;
+
+ Program program2;
+
+ ProgramWorkflow workflow1;
+
+ ProgramWorkflow workflow2;
+
+ ProgramWorkflowState state1;
+
+ ProgramWorkflowState state2;
+
+ List programsSharingConcept;
+
+ List workflowsSharingConcept;
+
+ List statesSharingConcept;
+
+ private static final String PROGRAM_CONCEPT = "CIEL:123";
+
+ private static final String WORKFLOW_CONCEPT = "CIEL:124";
+
+ private static final String STATE_CONCEPT = "CIEL:125";
+
+ @Before
+ public void setup() {
+ setupPrograms();
+ configuration = new ProgramConfiguration(PROGRAM_CONCEPT, WORKFLOW_CONCEPT, STATE_CONCEPT);
+ mockStatic(AppFrameworkUtil.class);
+ mockStatic(Context.class);
+
+ when(AppFrameworkUtil.getConcept(PROGRAM_CONCEPT)).thenReturn(concept1);
+ when(AppFrameworkUtil.getConcept(WORKFLOW_CONCEPT)).thenReturn(concept2);
+ when(AppFrameworkUtil.getConcept(STATE_CONCEPT)).thenReturn(concept3);
+ when(Context.getProgramWorkflowService()).thenReturn(service);
+ when(service.getProgram(any(Integer.class))).thenReturn(null);
+ when(service.getProgramByUuid(any(String.class))).thenReturn(null);
+ when(service.getWorkflow(any(Integer.class))).thenReturn(null);
+ when(service.getWorkflowByUuid(any(String.class))).thenReturn(null);
+ when(service.getState(any(Integer.class))).thenReturn(null);
+ when(service.getStateByUuid(any(String.class))).thenReturn(null);
+
+ programsSharingConcept = new ArrayList(Arrays.asList(program1, program2));
+ workflowsSharingConcept = new ArrayList(Arrays.asList(workflow1, workflow2));
+ statesSharingConcept = new ArrayList(Arrays.asList(state1, state2));
+
+ when(AppFrameworkUtil.getProgramsByConcept(concept1)).thenReturn(programsSharingConcept);
+ when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(workflowsSharingConcept);
+ when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(statesSharingConcept);
+
+ }
+
+ @Test
+ public void getProgram_shouldHandleCasesWhereUnderlyingProgramIsObvious() {
+ // Setup
+ when(AppFrameworkUtil.getProgramsByConcept(concept1)).thenReturn(Arrays.asList(program1));
+
+ // Replay
+ Program program = configuration.getProgram();
+
+ // Verify
+ Assert.assertEquals(program1, program);
+ }
+
+ @Test
+ public void getProgram_shouldHandleCasesWhereUnderlyingProgramIsNotObvious() {
+ // Setup
+ when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1));
+
+ // Replay
+ Program program = configuration.getProgram();
+
+ // Verify
+ Assert.assertEquals(program1, program);
+ }
+
+ @Test
+ public void getProgram_shouldFailIfProgramCanNotBeChose() {
+ try {
+ // Replay
+ configuration.getProgram();
+ fail("Should have failed since we have two programs, states and workflows identified by the smae concept");
+ } catch(APIException e) {
+ // Verify
+ Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: "
+ + PROGRAM_CONCEPT, e.getMessage());
+ }
+ }
+
+ @Test
+ public void getWorkflow_shouldHandleCasesWhereUnderlyingWorkflowIsObvious() {
+ // Setup
+ when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(Arrays.asList(workflow1));
+
+ // Replay
+ ProgramWorkflow worklow = configuration.getWorkflow();
+
+ // Verify
+ Assert.assertEquals(workflow1, worklow);
+ }
+
+ @Test
+ public void getWorkflow_shouldHandleCasesWhereUnderlyingWorkflowIsNotObvious() {
+ // Setup
+ when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1));
+
+ // Replay
+ ProgramWorkflow worklow = configuration.getWorkflow();
+
+ // Verify
+ Assert.assertEquals(workflow1, worklow);
+ }
+
+ @Test
+ public void getWorkflow_shouldFailIfWorkflowCanNotBeChose() {
+ try {
+ // Replay
+ configuration.getWorkflow();
+ fail("Should have failed at the program level since we have more than one program, workflow and states identified by the"
+ + " same concept.");
+ } catch(APIException e) {
+ // Verify
+ Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: "
+ + PROGRAM_CONCEPT, e.getMessage());
+ }
+
+ // Re-setup
+ configuration = new ProgramConfiguration(null, WORKFLOW_CONCEPT, null);
+ try {
+ // Replay
+ configuration.getWorkflow();
+ fail("Should have failed at the workflow level or base level since we have more than one workflow identified by the same"
+ + " concept yet this config has no program or state specified.");
+ } catch(APIException e) {
+ // Verify
+ Assert.assertEquals("Could not choose the intended workflow out of the many workflows identified by concept: "
+ + WORKFLOW_CONCEPT, e.getMessage());
+ }
+ }
+
+ @Test
+ public void getState_shouldHandleCasesWhereUnderlyingStateIsObvious() {
+ // Setup
+ when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1));
+
+ // Replay
+ ProgramWorkflowState state = configuration.getState();
+
+ // Verify
+ Assert.assertEquals(state1, state);
+ }
+
+ @Test
+ public void getState_shouldHandleCasesWhereUnderlyingStateIsNotObvious() {
+ // Setup
+ when(AppFrameworkUtil.getProgramsByConcept(concept1)).thenReturn(Arrays.asList(program1));
+
+ // Replay
+ ProgramWorkflowState state = configuration.getState();
+
+ // Verify
+ Assert.assertEquals(state1, state);
+ }
+
+ @Test
+ public void getState_shouldFailIfStateCanNotBeChose() {
+ try {
+ // Replay
+ configuration.getState();
+ fail("Should have failed at the program level since we have more than one program, workflow and states "
+ + "identified by the same concept");
+ } catch(APIException e) {
+ // Verify
+ Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: "
+ + PROGRAM_CONCEPT, e.getMessage());
+ }
+
+ // Re-setup
+ configuration = new ProgramConfiguration(null, null, STATE_CONCEPT);
+
+ try {
+ // Replay
+ configuration.getState();
+ fail("Should have faied at the state level or base level since we have more than one state identified "
+ + "by the same concept yet this config has no program or workflow specified.");
+ } catch(APIException e) {
+ // Verify
+ Assert.assertEquals("Could not choose the intended state out of the many states identified by concept: "
+ + STATE_CONCEPT, e.getMessage());
+ }
+ }
+
+ @Test
+ public void programWithBestWorflowAndStateCombination_shouldUseUniqueStateToDetermineTargetProgram() {
+ // setup
+ when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1));
+
+ // replay
+ Program program = configuration.programWithBestWorflowAndStateCombination(
+ programsSharingConcept);
+
+ // verify
+ Assert.assertEquals(program1, program);
+ }
+
+ @Test
+ public void programWithBestWorflowAndStateCombination_shouldUseUniqueWorkflowToDetermineTargetProgram() {
+ // setup
+ when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(Arrays.asList(workflow1));
+
+ // replay
+ Program program = configuration.programWithBestWorflowAndStateCombination(
+ programsSharingConcept);
+
+ // verify
+ Assert.assertEquals(program1, program);
+ }
+
+ @Test
+ public void programWithBestWorflowAndStateCombination_shouldFailInAmbigiousConditions() {
+ try {
+ // replay
+ configuration.programWithBestWorflowAndStateCombination(programsSharingConcept);
+ fail("Should have failed since we have more than one program, workflow and states "
+ + "identified by the same concept");
+ } catch(APIException e) {
+ // Verify
+ Assert.assertEquals("Could not choose the intended program out of the many programs identified by concept: "
+ + PROGRAM_CONCEPT, e.getMessage());
+ }
+ }
+
+ @Test
+ public void workflowWithBestProgramAndStateCombination_shouldUseUniqueStateToDetermineTargetWorkflow() {
+ // setup
+ when(AppFrameworkUtil.getStatesByConcept(concept3)).thenReturn(Arrays.asList(state1));
+
+ // replay
+ ProgramWorkflow workflow = configuration.workflowWithBestProgramAndStateCombination(workflowsSharingConcept, null);
+
+ // verify
+ Assert.assertEquals(workflow1, workflow);
+ }
+
+ @Test
+ public void workflowWithBestProgramAndStateCombination_shouldUseUniqueProgramToDetermineTargetWorkflow() {
+ // replay
+ ProgramWorkflow workflow = configuration.workflowWithBestProgramAndStateCombination(workflowsSharingConcept, program1);
+
+ // verify
+ Assert.assertEquals(workflow1, workflow);
+ }
+
+ @Test
+ public void workflowWithBestProgramAndStateCombination_shouldFailInAmbigiousConditions() {
+ try {
+ // replay
+ configuration.workflowWithBestProgramAndStateCombination(workflowsSharingConcept, null);
+ fail("Should have failed at the program level since we have more than one program, workflow and states "
+ + "identified by the same concept");
+ } catch (APIException e) {
+ // Verify
+ Assert.assertEquals("Could not choose the intended workflow out of the many workflows identified by concept: "
+ + WORKFLOW_CONCEPT, e.getMessage());
+ }
+ }
+
+ @Test
+ public void stateWithBestProgramAndWorkflowCombination_shouldUseUniqueWorkflowToDetermineTargetState() {
+ // setup
+ when(AppFrameworkUtil.getWorkflowsByConcept(concept2)).thenReturn(Arrays.asList(workflow1));
+
+ // replay
+ ProgramWorkflowState state = configuration.stateWithBestProgramAndWorkflowCombination(statesSharingConcept, null, workflow1);
+
+ // verify
+ Assert.assertEquals(state1, state);
+ }
+
+ @Test
+ public void stateWithBestProgramAndWorkflowCombination_shouldUseUniqueProgramToDetermineTargetState() {
+ // replay
+ ProgramWorkflowState state = configuration.stateWithBestProgramAndWorkflowCombination(statesSharingConcept, program1, null);
+
+ // verify
+ Assert.assertEquals(state1, state);
+ }
+
+ @Test
+ public void stateWithBestProgramAndWorkflowCombination_shouldFailInAmbigiousConditions() {
+ try {
+ // replay
+ configuration.stateWithBestProgramAndWorkflowCombination(statesSharingConcept, null, null);
+ fail("Should have failed at the program level since we have more than one program, workflow and states "
+ + "identified by the same concept");
+ } catch(APIException e) {
+ // Verify
+ Assert.assertEquals("Could not choose the intended state out of the many states identified by concept: "
+ + STATE_CONCEPT, e.getMessage());
+ }
+ }
+
+ @Test
+ public void hasValidProgramTree_shouldReturnTrueIfTreeIsValid() {
+ // setup
+ // program + workflow + state
+ ResolvedConfiguration resolvedConfig = new ResolvedConfiguration(program1, workflow1, state1);
+ configuration.setResolvedConfig(resolvedConfig);
+
+ // replay and verify
+ Assert.assertTrue(configuration.hasValidProgramTree());
+
+ // re-setup
+ // program + state
+ resolvedConfig = new ResolvedConfiguration(program1, null, state1);
+
+ // replay and verify
+ Assert.assertTrue(configuration.hasValidProgramTree());
+
+ // re-setup
+ // program + workflow
+ resolvedConfig = new ResolvedConfiguration(program1, workflow1, null);
+
+ // replay and verify
+ Assert.assertTrue(configuration.hasValidProgramTree());
+ }
+
+ @Test
+ public void hasValidProgramTree_shouldReturnTrueIfWeHaveNoTree() {
+ // setup
+ // program only
+ ResolvedConfiguration resolvedConfig = new ResolvedConfiguration(program1, null, null);
+ configuration.setResolvedConfig(resolvedConfig);
+
+ // replay and verify
+ Assert.assertTrue(configuration.hasValidProgramTree());
+
+ // setup
+ // workflow only
+ resolvedConfig = new ResolvedConfiguration(null, workflow1, null);
+ configuration.setResolvedConfig(resolvedConfig);
+
+ // replay and verify
+ Assert.assertTrue(configuration.hasValidProgramTree());
+
+ // setup
+ // state only
+ resolvedConfig = new ResolvedConfiguration(null, null, state1);
+ configuration.setResolvedConfig(resolvedConfig);
+
+ // replay and verify
+ Assert.assertTrue(configuration.hasValidProgramTree());
+ }
+
+ @Test
+ public void hasValidProgramTree_shouldReturnFalseIfTreeIsInvalid() {
+ // setup
+ // program + workflow of different program + state
+ ResolvedConfiguration resolvedConfig = new ResolvedConfiguration(program1, workflow2, state1);
+ configuration.setResolvedConfig(resolvedConfig);
+
+ // replay and verify
+ Assert.assertFalse(configuration.hasValidProgramTree());
+
+ // re-setup
+ // program + workflow + state of different program/workflow
+ resolvedConfig = new ResolvedConfiguration(program1, workflow1, state2);
+ configuration.setResolvedConfig(resolvedConfig);
+
+ // replay and verify
+ Assert.assertFalse(configuration.hasValidProgramTree());
+
+ // re-setup
+ // program + state of different program
+ resolvedConfig = new ResolvedConfiguration(program1, null, state2);
+ configuration.setResolvedConfig(resolvedConfig);
+
+ // replay and verify
+ Assert.assertFalse(configuration.hasValidProgramTree());
+ }
+
+ private void setupPrograms() {
+ setupConcepts();
+
+ state1 = new ProgramWorkflowState(1);
+ state2 = new ProgramWorkflowState(2);
+
+ state1.setConcept(concept3);
+ state2.setConcept(concept4);
+
+ workflow1 = new ProgramWorkflow(1);
+ workflow2 = new ProgramWorkflow(2);
+
+ program1 = new Program(1);
+ program2 = new Program(2);
+
+ program1.addWorkflow(workflow1);
+ program2.addWorkflow(workflow2);
+
+ workflow1.addState(state1);
+ workflow2.addState(state2);
+ }
+
+ private void setupConcepts() {
+ concept1 = new Concept(1);
+ concept2 = new Concept(2);
+ when(concept3.getName()).thenReturn(new ConceptName("State One", null));
+ when(concept4.getName()).thenReturn(new ConceptName("State Two", null));
+
+ }
+}
diff --git a/api/src/test/java/org/openmrs/module/appframework/factory/AppConfigurationLoaderFactoryTest.java b/api/src/test/java/org/openmrs/module/appframework/factory/AppConfigurationLoaderFactoryTest.java
index ad0438d..063f71e 100644
--- a/api/src/test/java/org/openmrs/module/appframework/factory/AppConfigurationLoaderFactoryTest.java
+++ b/api/src/test/java/org/openmrs/module/appframework/factory/AppConfigurationLoaderFactoryTest.java
@@ -32,8 +32,8 @@ public void testConfigurationLoad() throws IOException {
List appDescriptors = appConfigurationLoaderFactory.getAppDescriptors();
List extensions = appConfigurationLoaderFactory.getExtensions();
- assertEquals(5, appDescriptors.size());
- assertEquals(4, extensions.size());
+ assertEquals(6, appDescriptors.size());
+ assertEquals(5, extensions.size());
}
}
diff --git a/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceImplTest.java b/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceImplTest.java
index 95ec71a..f6af2ce 100644
--- a/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceImplTest.java
+++ b/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceImplTest.java
@@ -3,18 +3,22 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import javax.script.ScriptException;
import javax.validation.Validator;
import org.hibernate.Criteria;
@@ -22,19 +26,31 @@
import org.hibernate.classic.Session;
import org.junit.After;
import org.junit.Before;
-import org.junit.runner.RunWith;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.openmrs.Location;
+import org.openmrs.Patient;
+import org.openmrs.PatientProgram;
+import org.openmrs.PatientState;
+import org.openmrs.Program;
+import org.openmrs.ProgramWorkflow;
+import org.openmrs.ProgramWorkflowState;
+import org.openmrs.api.APIException;
+import org.openmrs.api.PatientService;
+import org.openmrs.api.ProgramWorkflowService;
import org.openmrs.api.context.Context;
import org.openmrs.api.db.hibernate.DbSessionFactory;
-import org.openmrs.Location;
+import org.openmrs.module.appframework.LoginLocationFilter;
import org.openmrs.module.appframework.config.AppFrameworkConfig;
import org.openmrs.module.appframework.context.AppContextModel;
+import org.openmrs.module.appframework.context.ProgramConfiguration;
+import org.openmrs.module.appframework.context.ProgramConfiguration.ResolvedConfiguration;
import org.openmrs.module.appframework.domain.AppDescriptor;
import org.openmrs.module.appframework.domain.Extension;
import org.openmrs.module.appframework.domain.ExtensionPoint;
import org.openmrs.module.appframework.feature.FeatureToggleProperties;
import org.openmrs.module.appframework.feature.TestFeatureTogglePropertiesFactory;
-import org.openmrs.module.appframework.LoginLocationFilter;
import org.openmrs.module.appframework.repository.AllAppDescriptors;
import org.openmrs.module.appframework.repository.AllComponentsState;
import org.openmrs.module.appframework.repository.AllFreeStandingExtensions;
@@ -47,11 +63,12 @@
@PrepareForTest(Context.class)
public class AppFrameworkServiceImplTest {
- private Validator validator = mock(Validator.class);
+ @Mock()
+ private Validator validator;
- private AllAppDescriptors allAppDescriptors = new AllAppDescriptors(validator);
+ private AllAppDescriptors allAppDescriptors;
- private AllFreeStandingExtensions allFreeStandingExtensions = new AllFreeStandingExtensions(validator);
+ private AllFreeStandingExtensions allFreeStandingExtensions;
private AllComponentsState allComponentsState = new AllComponentsState();
@@ -60,7 +77,7 @@ public class AppFrameworkServiceImplTest {
private FeatureToggleProperties featureToggles = TestFeatureTogglePropertiesFactory.get();
private AppFrameworkConfig appFrameworkConfig = new AppFrameworkConfig();
-
+
private AppDescriptor app1;
private AppDescriptor app2;
@@ -82,6 +99,17 @@ public class AppFrameworkServiceImplTest {
private AppFrameworkServiceImpl service;
private LoginLocationFilter loginLocationFilter;
+
+ private PatientProgram patientProgram;
+
+ private Program program;
+
+ private ProgramWorkflowState state;
+
+ private ProgramWorkflow workflow;
+
+ private static final String PATIENT_UUID = "0cbe2ed3-cd5f-4f46-9459-26127c9265ab";
+
@Before
public void setUp() throws Exception {
@@ -95,6 +123,11 @@ public boolean accept(Location location) {
PowerMockito.mockStatic(Context.class);
when(Context.getRegisteredComponents(eq(LoginLocationFilter.class)))
.thenReturn(Arrays.asList(loginLocationFilter));
+
+ setUpPatientProgram();
+
+ allAppDescriptors = new AllAppDescriptors(validator);
+ allFreeStandingExtensions = new AllFreeStandingExtensions(validator);
featureToggles.setPropertiesFile(new File(this.getClass().getResource("/" + FeatureToggleProperties.FEATURE_TOGGLE_PROPERTIES_FILE_NAME).getFile()));
@@ -279,12 +312,134 @@ public void testGetLoginLocationsShouldReturnAllLoginLocations() throws Exceptio
assertEquals(loginLocations.get(1).getUuid(), actualLoginLocations.get(1).getUuid());
}
+ public void testGetPatientUuid() throws ScriptException {
+ // Setup
+ AppContextModel contextModel = new AppContextModel();
+ contextModel.put("patient", new PatientContextModel(PATIENT_UUID));
+
+ // Replay
+ String uuid = service.getPatientUuid(contextModel);
+
+ // Verify
+ assertEquals(PATIENT_UUID, uuid);
+ }
+
+ @Test
+ public void testCheckRequiredProgramsOnCurrentPatient() {
+ // Setup
+ mockStatic(Context.class);
+ Patient patient = new Patient();
+ PatientService patientService = mock(PatientService.class);
+ ProgramWorkflowService workflowService = mock(ProgramWorkflowService.class);
+
+ when(patientService.getPatientByUuid(PATIENT_UUID)).thenReturn(patient);
+ when(workflowService.getPatientPrograms(patient, null, null,
+ null, null, null, false)).thenReturn(Arrays.asList(patientProgram));
+ when(Context.getPatientService()).thenReturn(patientService);
+ when(Context.getProgramWorkflowService()).thenReturn(workflowService);
+
+ AppContextModel contextModel = new AppContextModel();
+ contextModel.put("patient", new PatientContextModel(PATIENT_UUID));
+ ProgramConfiguration config = new ProgramConfiguration();
+ config.setResolvedConfig(new ResolvedConfiguration(program, workflow, state));
+
+ // Replay
+ boolean hasRequiredPrograms = service.checkRequiredProgramsOnCurrentPatient(extensionRequiringProgramConfigurations(Arrays.asList(config)), contextModel);
+
+ // Verify
+ assertTrue(hasRequiredPrograms);
+
+ // Re-setup
+ config.setResolvedConfig(new ResolvedConfiguration(null, workflow, state));
+
+ // Replay
+ hasRequiredPrograms = service.checkRequiredProgramsOnCurrentPatient(extensionRequiringProgramConfigurations(Arrays.asList(config)), contextModel);
+
+ // Verify
+ assertTrue(hasRequiredPrograms);
+
+ // Re-setup
+ // Configuration with a state that's not a member of the patient's current states
+ ProgramWorkflowState pastState = new ProgramWorkflowState();
+ workflow.addState(pastState);
+ config.setResolvedConfig(new ResolvedConfiguration(null, null, pastState));
+
+ // Replay
+ hasRequiredPrograms = service.checkRequiredProgramsOnCurrentPatient(extensionRequiringProgramConfigurations(Arrays.asList(config)), contextModel);
+
+ // Verify
+ assertFalse(hasRequiredPrograms);
+ }
+
+ @Test
+ public void hasProgramAssignableToConfiguration_shouldReturnTrueIfConfigIsAssignableToAnyOfThePatientPrograms() throws ScriptException {
+ // Setup
+ PatientProgram emptyPatientProgram = new PatientProgram();
+ emptyPatientProgram.setProgram(new Program());
+ List patientPrograms = Arrays.asList(emptyPatientProgram, patientProgram);
+
+ ProgramConfiguration config = new ProgramConfiguration();
+ config.setResolvedConfig(new ResolvedConfiguration(program, workflow, state));
+
+ // Replay
+ boolean isAssignable = service.hasProgramAssignableToConfiguration(patientPrograms, config);
+
+ // Verify
+ assertTrue(isAssignable);
+ }
+
+ @Test
+ public void hasProgramAssignableToConfiguration_shouldReturnFalseIfConfigIsNotAssignableToAnyOfThePatientPrograms() throws ScriptException {
+ // Setup
+ PatientProgram emptyPatientProgram = new PatientProgram();
+ emptyPatientProgram.setProgram(new Program());
+ List patientPrograms = Arrays.asList(emptyPatientProgram, patientProgram);
+
+ ProgramWorkflowState pastState = new ProgramWorkflowState();
+ workflow.addState(pastState);
+
+ // Configuration with a state that's not a member of the patient's current states
+ ProgramConfiguration config = new ProgramConfiguration();
+ config.setResolvedConfig(new ResolvedConfiguration(program, workflow, pastState));
+
+ // Replay
+ boolean isAssignable = service.hasProgramAssignableToConfiguration(patientPrograms, config);
+
+ // Verify
+ assertFalse(isAssignable);
+ }
+
+ @Test
+ public void hasProgramAssignableToConfiguration_shouldFailWithAnExceptionIfConfigurationHasAnInvalidProgramTree() {
+ // Setup
+
+ // Configuration with invalid program tree
+ ProgramConfiguration config = new ProgramConfiguration();
+ config.setResolvedConfig(new ResolvedConfiguration(program, workflow, new ProgramWorkflowState()));
+
+ try {
+ // Replay
+ service.hasProgramAssignableToConfiguration(Arrays.asList(patientProgram), config);
+ fail("Should throw exception if configuration has an invalid program tree");
+ } catch(APIException e) {
+ // Verify
+ assertEquals("ProgramConfiguration has an invalid program tree", e.getMessage());
+ }
+ }
+
private Extension extensionRequiring(String requires) {
Extension extension = new Extension();
extension.setRequire(requires);
return extension;
}
-
+
+ private Extension extensionRequiringProgramConfigurations(List programConfigurations) {
+ Extension extension = new Extension();
+ extension.setRequiredPrograms(programConfigurations);
+ return extension;
+ }
+
+
public class VisitStatus {
public boolean active;
public boolean admitted;
@@ -293,4 +448,32 @@ public VisitStatus(boolean active, boolean admitted) {
this.admitted = admitted;
}
}
+
+ public static class PatientContextModel {
+ public String uuid;
+ public Patient patient;
+ public PatientContextModel(String uuid) {
+ this.uuid = uuid;
+ }
+ }
+
+ private void setUpPatientProgram() {
+ program = new Program();
+ program.setUuid("588a31bb-7923-4ef8-a6fc-b8f2ae5d1343");
+
+ state = new ProgramWorkflowState();
+ state.setUuid("588a31bb-7923-4ef8-a6fc-b8f2ae5d1344");
+
+ workflow = new ProgramWorkflow();
+ workflow.setUuid("588a31bb-7923-4ef8-a6fc-b8f2ae5d1345");
+
+ workflow.addState(state);
+ program.addWorkflow(workflow);
+
+ patientProgram = mock(PatientProgram.class);
+ when(patientProgram.getProgram()).thenReturn(program);
+ PatientState currentState = new PatientState();
+ currentState.setState(state);
+ when(patientProgram.getCurrentStates()).thenReturn(new HashSet(Arrays.asList(currentState)));
+ }
}
diff --git a/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java b/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java
index 3125aa2..64ae839 100644
--- a/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java
+++ b/api/src/test/java/org/openmrs/module/appframework/service/AppFrameworkServiceTest.java
@@ -68,10 +68,14 @@ public class AppFrameworkServiceTest extends BaseModuleContextSensitiveTest {
@Autowired
private AppFrameworkService appFrameworkService;
-
- @Autowired
- private AppFrameworkConfig appFrameworkConfig;
-
+
+ @Autowired
+ private AppFrameworkConfig appFrameworkConfig;
+
+ private static String UUID_OF_PATIENT_NOT_ENROLLED_IN_ANY_PROGRAM = "0cbe2ed3-cd5x-4f46-9459-26127c226b9i";
+
+ private static String UUID_OF_PATIENT_ENROLLED_TO_PROGRAMS = "0cbe2ed3-cd5f-4f46-9459-26127c9265ab";
+
@Before
public void setup() throws IOException {
//trigger loading of the apps
@@ -113,17 +117,38 @@ else if (p4.getPrivilege().equals(privilegeToAssign))
return us.createUser(u, "Openmr5xy");
}
+ private Role viewPatientProgramRole() {
+ UserService us = Context.getUserService();
+ Role role = new Role("View patient program", "Set of privileges required to view a Patient and their programs");
+ Privilege p1 = new Privilege("View Patients", "description");
+ us.savePrivilege(p1);
+ Privilege p2 = new Privilege("View Patient Programs", "description");
+ us.savePrivilege(p2);
+ Privilege p3 = new Privilege("View Concepts", "description");
+ us.savePrivilege(p3);
+ Privilege p4 = new Privilege("View Programs", "description");
+ us.savePrivilege(p4);
+
+ role.addPrivilege(p1);
+ role.addPrivilege(p2);
+ role.addPrivilege(p3);
+ role.addPrivilege(p4);
+ return us.saveRole(role);
+ }
+
/**
* @see {@link AppFrameworkService#getAllEnabledExtensions()}
*/
@Test
public void getAllEnabledExtensions_shouldGetAllEnabledExtensions() throws Exception {
List visitExts = appFrameworkService.getAllEnabledExtensions();
- assertEquals(4, visitExts.size());
- assertThat(visitExts,
- containsInAnyOrder(hasProperty("id", is("registerOutpatientHomepageLink")),
- hasProperty("id", is("orderXrayExtension")), hasProperty("id", is("gotoPatientExtension")),
- hasProperty("id", is("gotoArchives"))));
+ assertEquals(5, visitExts.size());
+ assertThat(visitExts, containsInAnyOrder(
+ hasProperty("id", is("registerOutpatientHomepageLink")),
+ hasProperty("id", is("orderXrayExtension")),
+ hasProperty("id", is("gotoPatientExtension")),
+ hasProperty("id", is("gotoArchives")),
+ hasProperty("id", is("gotoProgramSection"))));
}
/**
@@ -172,7 +197,7 @@ public void getExtensionsForCurrentUser_shouldGetAllEnabledExtensionsForTheCurre
assertEquals(user, Context.getAuthenticatedUser());
List userExts = appFrameworkService.getExtensionsForCurrentUser();
- assertEquals(2, userExts.size());
+ assertEquals(3, userExts.size());
List userAppIds = new ArrayList();
for (Extension ext : userExts) {
userAppIds.add(ext.getId());
@@ -218,8 +243,9 @@ public void getExtensionsForCurrentUser_shouldReturnExtensionsWithNoRequiredPriv
if (Context.getAuthenticatedUser() != null)
Context.logout();
assertNull(Context.getAuthenticatedUser());
- assertThat(appFrameworkService.getExtensionsForCurrentUser(), hasSize(1));
- assertThat(appFrameworkService.getExtensionsForCurrentUser().get(0).getId(), is("gotoArchives"));
+
+ assertThat(appFrameworkService.getExtensionsForCurrentUser(), hasSize(2));
+ assertThat(appFrameworkService.getExtensionsForCurrentUser().get(0).getId(), is("gotoArchives"));
}
/**
@@ -300,29 +326,24 @@ public void getExtensionsForCurrentUser_shouldGetAllEnabledExtensionsForTheSuper
*/
@Test
public void getExtensionsForCurrentUser_shouldGetEnabledExtensionsForTheCurrentUserByRequireProperty() throws Exception {
+ executeDataSet("AppFrameworkServiceImplTest-createPatientProgram.xml");
User user = setupPrivilegesRolesAndUser("Some Random Privilege");
+ user.addRole(viewPatientProgramRole());
Context.authenticate(user.getUsername(), "Openmr5xy");
assertEquals(user, Context.getAuthenticatedUser());
- AppContextModel contextModel = setupContextModel(true);
+ AppContextModel contextModel = setupContextModel(true, UUID_OF_PATIENT_NOT_ENROLLED_IN_ANY_PROGRAM);
List extensions = appFrameworkService.getExtensionsForCurrentUser(null, contextModel);
assertEquals(1, extensions.size());
assertEquals("orderXrayExtension", extensions.get(0).getId());
- contextModel = setupContextModel(false);
+ contextModel = setupContextModel(false, UUID_OF_PATIENT_NOT_ENROLLED_IN_ANY_PROGRAM);
extensions = appFrameworkService.getExtensionsForCurrentUser(null, contextModel);
assertEquals(1, extensions.size());
assertEquals("gotoArchives", extensions.get(0).getId());
}
-
- private AppContextModel setupContextModel(boolean isVisitActive) {
- AppContextModel bindings = new AppContextModel();
- bindings.put("patientId", 7);
- bindings.put("visit", new VisitStatus(isVisitActive));
- return bindings;
- }
-
+
@Test
public void getAllAppTemplates_shouldGetAppTemplates() throws Exception {
List actual = appFrameworkService.getAllAppTemplates();
@@ -354,7 +375,7 @@ public void testInheritingConfigurationFromAppTemplate() {
@Verifies(value = "should get all enabled apps", method = "getAllEnabledApps()")
public void getAllEnabledApps_shouldGetAllEnabledApps() throws Exception {
List apps = appFrameworkService.getAllEnabledApps();
- assertEquals(4, apps.size());//should include the app with that requires no privilege
+ assertEquals(5, apps.size());//should include the app with that requires no privilege
List appIds = new ArrayList();
for (AppDescriptor app : apps) {
appIds.add(app.getId());
@@ -475,4 +496,30 @@ public VisitStatus(boolean visitActive) {
this.active = visitActive;
}
}
+
+ @Test
+ public void getExtensionsForCurrentUser_shouldGetEnabledExtensionsForTheCurrentUserByProgramConfiguration() throws Exception {
+ executeDataSet("AppFrameworkServiceImplTest-createPatientProgram.xml");
+ User user = setupPrivilegesRolesAndUser(null);
+ user.addRole(viewPatientProgramRole());
+ Context.authenticate(user.getUsername(), "Openmr5xy");
+
+ AppContextModel contextModel = setupContextModel(true, UUID_OF_PATIENT_NOT_ENROLLED_IN_ANY_PROGRAM);
+ List extensions = appFrameworkService.getExtensionsForCurrentUser(null, contextModel);
+
+ assertEquals(0, extensions.size());
+
+ contextModel = setupContextModel(true, UUID_OF_PATIENT_ENROLLED_TO_PROGRAMS);
+ extensions = appFrameworkService.getExtensionsForCurrentUser(null, contextModel);
+
+ assertEquals(1, extensions.size());
+ assertEquals("gotoProgramSection", extensions.get(0).getId());
+ }
+
+ private AppContextModel setupContextModel(boolean isVisitActive, String patientUuid) {
+ AppContextModel bindings = new AppContextModel();
+ bindings.put("patient", new AppFrameworkServiceImplTest.PatientContextModel(patientUuid));
+ bindings.put("visit", new VisitStatus(isVisitActive));
+ return bindings;
+ }
}
diff --git a/api/src/test/resources/AppFrameworkServiceImplTest-createPatientProgram.xml b/api/src/test/resources/AppFrameworkServiceImplTest-createPatientProgram.xml
new file mode 100644
index 0000000..e4f0dba
--- /dev/null
+++ b/api/src/test/resources/AppFrameworkServiceImplTest-createPatientProgram.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appsfortesting/src/main/resources/apps/appset1_app.json b/appsfortesting/src/main/resources/apps/appset1_app.json
index 1242ffa..8271175 100644
--- a/appsfortesting/src/main/resources/apps/appset1_app.json
+++ b/appsfortesting/src/main/resources/apps/appset1_app.json
@@ -24,5 +24,16 @@
}
],
"contextModel": [ "xrayConceptId", "patientId" ]
+ },
+ {
+ "id": "programApp",
+ "description": "Program Application",
+ "requiredPrivilege": "Manage Programs",
+ "extensionPoints": [
+ {
+ "id": "programActions"
+ }
+ ],
+ "contextModel": null
}
]
\ No newline at end of file
diff --git a/appsfortesting/src/main/resources/apps/extensionset1_extension.json b/appsfortesting/src/main/resources/apps/extensionset1_extension.json
index ff5e4d3..3fefed8 100644
--- a/appsfortesting/src/main/resources/apps/extensionset1_extension.json
+++ b/appsfortesting/src/main/resources/apps/extensionset1_extension.json
@@ -29,5 +29,20 @@
"label": "Go to Archives",
"url": "/archives.page?patientId={{patientId}}",
"require": "!visit.active"
+ },
+ {
+ "id": "gotoProgramSection",
+ "appId": "programApp",
+ "extensionPointId": "programActions",
+ "type": "link",
+ "label": "Go to Program",
+ "url": "/program.page?patientId={{patientId}}",
+ "requiredPrograms": [
+ {
+ "programRef": "CIEL:123",
+ "workflowRef": "1743",
+ "stateRef": "CIEL:124"
+ }
+ ]
}
]
\ No newline at end of file