Skip to content

Commit

Permalink
feat:use dhis2 expression parser
Browse files Browse the repository at this point in the history
  • Loading branch information
9sneha-n committed Sep 24, 2024
1 parent 2525238 commit 92247a3
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 32 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@dhis2/d2-i18n": "1.1.0",
"@dhis2/d2-i18n-extract": "1.0.8",
"@dhis2/d2-i18n-generate": "1.2.0",
"@dhis2/expression-parser": "^1.1.0",
"@dhis2/ui": "6.12.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
Expand Down
3 changes: 3 additions & 0 deletions src/data/utils/ruleHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,12 @@ export const getProgramRules = (
return {
id: id,
condition: attributeParsedCondition.replace(/d2:/g, "fn:"), //replace d2: with fn: to decouple entity from DHIS
d2Condition: attributeParsedCondition,
originalCondition: condition,
dataElementIds: _(dataElementIds).uniq().compact().value(),
teAttributeIds: _(teaIds).uniq().compact().value(),
actions: programRuleActions || [],
programRuleVariables: programRuleVariables || [],
};
}) || []
);
Expand Down
46 changes: 28 additions & 18 deletions src/domain/entities/Questionnaire/Questionnaire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
QuestionnaireQuestion,
isAntibioticQuestion,
} from "./QuestionnaireQuestion";
import { QuestionnaireRule, getApplicableRules } from "./QuestionnaireRules";
import { QuestionnaireRule } from "./QuestionnaireRules";
import { QuestionnaireSection, QuestionnaireSectionM } from "./QuestionnaireSection";

export interface QuestionnaireBase {
Expand Down Expand Up @@ -259,23 +259,33 @@ export class Questionnaire {
initialLoad = false
): Questionnaire {
//For the updated question, get all rules that are applicable
const allQsInQuestionnaireStages = questionnaire.stages.flatMap(
(stage: QuestionnaireStage) => {
return stage.sections.flatMap(section => {
return section.questions.map(question => question);
});
}
);

const allQsInQuestionnaire = [
...(questionnaire.entity?.questions || []),
...allQsInQuestionnaireStages,
];

const applicableRules = getApplicableRules(
updatedQuestion,
questionnaire.rules,
allQsInQuestionnaire
// const allQsInQuestionnaireStages = questionnaire.stages.flatMap(
// (stage: QuestionnaireStage) => {
// return stage.sections.flatMap(section => {
// return section.questions.map(question => question);
// });
// }
// );

// const allQsInQuestionnaire = [
// ...(questionnaire.entity?.questions || []),
// ...allQsInQuestionnaireStages,
// ];

// const applicableRules = getApplicableRules(
// updatedQuestion,
// questionnaire.rules,
// allQsInQuestionnaire
// );

const applicableRules = questionnaire.rules.filter(
rule =>
rule.dataElementIds.includes(updatedQuestion.id) ||
rule.teAttributeIds.includes(updatedQuestion.id) ||
rule.actions.some(action => action.dataElement?.id === updatedQuestion.id) ||
rule.actions.some(
action => action.trackedEntityAttribute?.id === updatedQuestion.id
)
);

if (initialLoad && applicableRules.length === 0) return questionnaire;
Expand Down
19 changes: 12 additions & 7 deletions src/domain/entities/Questionnaire/QuestionnaireQuestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export class QuestionnaireQuestion {
static updateQuestions(
questions: Question[],
updatedQuestion: Question,
rules: QuestionnaireRule[],
_rules: QuestionnaireRule[],
questionnaire: Questionnaire,
parentSectionHidden?: boolean
): Question[] {
Expand All @@ -180,7 +180,7 @@ export class QuestionnaireQuestion {

//Get list question ids that require update
const allQuestionsRequiringUpdate = _(
rules.flatMap(rule => {
questionnaire.rules.flatMap(rule => {
const actionUpdates = rule.actions.flatMap(
action => action?.dataElement?.id || action.trackedEntityAttribute?.id
);
Expand All @@ -196,10 +196,15 @@ export class QuestionnaireQuestion {
const parsedAndUpdatedQuestions = updatedQuestions.map(question => {
//Get the rules that are applicable for the current question
//this is done to take care of any "side-effects" of an updated question.
const rulesApplicableForCurrentQuestion =
question.id !== updatedQuestion.id
? getApplicableRules(question, questionnaire.rules, updatedQuestions)
: rules;
// const rulesApplicableForCurrentQuestion =
// question.id !== updatedQuestion.id
// ? getApplicableRules(question, questionnaire.rules, updatedQuestions)
// : rules;
const rulesApplicableForCurrentQuestion = getApplicableRules(
question,
questionnaire.rules,
updatedQuestions
);

//If the question is part of any of the rule actions, update the section
const parsedAndUpdatedQuestion =
Expand Down Expand Up @@ -227,7 +232,7 @@ export class QuestionnaireQuestion {
return this.updateQuestions(
acc,
{ ...hiddenQuestion, value: undefined },
rules,
questionnaire.rules,
questionnaire
);
}, parsedAndUpdatedQuestions);
Expand Down
126 changes: 123 additions & 3 deletions src/domain/entities/Questionnaire/QuestionnaireRules.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { D2ProgramRuleVariable } from "../../../data/entities/D2Program";
import { Id } from "../Ref";
import _ from "../generic/Collection";
import { Question } from "./QuestionnaireQuestion";

import * as xp from "@dhis2/expression-parser";

const RULE_FUNCTIONS = ["fn:hasValue", "fn:daysBetween", "fn:yearsBetween"];
const RULE_OPERATORS = [
">" as const,
Expand Down Expand Up @@ -46,10 +49,13 @@ export interface QuestionnaireRuleAction {
export interface QuestionnaireRule {
id: Id;
condition: string; //condition is parsed with dataelementId e.g: #{dataElementId} == 'Yes'
d2Condition: string; //SNEHA TO DO : remove above condition and use this condition after testing
originalCondition: string;
dataElementIds: Id[]; // all dataElements in condition (there could be mutiple conditions)
teAttributeIds: Id[]; // all trackedEntityAttributes in condition (there could be mutiple conditions)
actions: QuestionnaireRuleAction[];
parsedResult?: boolean; //calculate the condition and store the result
programRuleVariables: D2ProgramRuleVariable[] | undefined;
}

export const getApplicableRules = (
Expand All @@ -68,8 +74,24 @@ export const getApplicableRules = (

//2. Run the rule conditions and return rules with parsed results
const parsedApplicableRules = applicableRules.map(rule => {
const parsedResult = parseCondition(rule.condition, updatedQuestion, questions);
return { ...rule, parsedResult };
const customParserResult = parseCondition(rule.condition, updatedQuestion, questions);

const expressionParserResult = parseConditionWithExpressionParser(rule, questions);

//SNEHA DEBUG
if (customParserResult !== expressionParserResult) {
console.debug(
`custom parser and expression parser give diffrent results for rule : ${
rule.id
}, condition : ${
rule.originalCondition
}, custom parser : ${expressionParserResult}, expression parser : ${customParserResult}, value: ${
questions.find(q => q.id === rule.dataElementIds[0])?.value
}`
);
}

return { ...rule, parsedResult: expressionParserResult };
});

return parsedApplicableRules;
Expand Down Expand Up @@ -226,7 +248,7 @@ const parseAndEvaluateSubCondition = (
}
};

export const parseCondition = (
const parseCondition = (
condition: string,
updatedQuestion: Question,
questions: Question[]
Expand Down Expand Up @@ -270,3 +292,101 @@ export const parseCondition = (

return andConditions.every(condition => condition);
};

const parseConditionWithExpressionParser = (rule: QuestionnaireRule, questions: Question[]) => {
try {
const dataItemsExpressionParser = new xp.ExpressionJs(
rule.d2Condition,
xp.ExpressionMode.RULE_ENGINE_CONDITION
);

const dataItems = dataItemsExpressionParser.collectDataItems();
const dataItemValueMap = dataItems.map((dataItem: xp.DataItemJs) => {
const currentQuestion = questions.find(question => question.id === dataItem.uid0.value);
if (!currentQuestion) return { dataItem: dataItem, value: "" };

const value = getQuestionValueByType(currentQuestion);
return {
dataItem: dataItem,
value: value,
};
});
const dataItemsMap = new Map(dataItemValueMap.map(a => [a.dataItem, a.value]));

const programRuleVariableExpressionParser = new xp.ExpressionJs(
rule.originalCondition,
xp.ExpressionMode.RULE_ENGINE_CONDITION
);

const programRuleVariables =
programRuleVariableExpressionParser.collectProgramRuleVariableNames();
const programRuleVariablesValueMap = programRuleVariables.map(programRuleVariable => {
const currentProgramRuleVariable = rule.programRuleVariables?.find(
prv => prv.name === programRuleVariable
);
const currentQuestion = questions.find(
question =>
question.id === currentProgramRuleVariable?.dataElement?.id ||
question.id === currentProgramRuleVariable?.trackedEntityAttribute?.id
);

if (!currentQuestion)
return {
programRuleVariable: programRuleVariable,
value: new xp.VariableValueJs(xp.ValueType.STRING, null, [], null),
};
const value = getQuestionValueByType(currentQuestion);
//SNEHA DEBUG : This is my guess.
// const candidates = currentQuestion.type === "select" ? currentQuestion.options : [];
// const eventDate = new Date();

const variableValue = getVariableValueByType(
currentQuestion,
value === "" ? null : value
);

return {
programRuleVariable: programRuleVariable,
value: variableValue,
};
});
const programRuleVariablesMap = new Map(
programRuleVariablesValueMap.map(a => [a.programRuleVariable, a.value])
);

const expressionData = new xp.ExpressionDataJs(
programRuleVariablesMap,
undefined,
undefined,
dataItemsMap,
undefined
);
const parsedResult: boolean = programRuleVariableExpressionParser.evaluate(
a => console.debug("ABC" + a), //SNEGHA DEBUG : what is this?
expressionData
);
return parsedResult;
} catch (error) {
console.error(`Error parsing rule condition: ${rule.condition} with error : ${error}`);
return false;
}
};

const getVariableValueByType = (
question: Question,
stringValue: xp.Nullable<string>
): xp.VariableValueJs => {
switch (question.type) {
case "select":
case "text":
return new xp.VariableValueJs(xp.ValueType.STRING, stringValue, [], null);
case "boolean":
return new xp.VariableValueJs(xp.ValueType.BOOLEAN, stringValue, [], null);
case "date":
case "datetime":
return new xp.VariableValueJs(xp.ValueType.DATE, stringValue, [], null);

case "number":
return new xp.VariableValueJs(xp.ValueType.NUMBER, stringValue, [], null);
}
};
10 changes: 6 additions & 4 deletions src/domain/entities/Questionnaire/QuestionnaireSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,29 @@ export class QuestionnaireSectionM {
sections: QuestionnaireSection[],
updatedQuestion: Question,
questionnaire: Questionnaire,
rules: QuestionnaireRule[]
_rules: QuestionnaireRule[]
): QuestionnaireSection[] {
//Get all the sections that require update
const allSectionsRequiringUpdate = _(
rules.flatMap(rule => rule.actions.flatMap(action => action.programStageSection?.id))
questionnaire.rules.flatMap(rule =>
rule.actions.flatMap(action => action.programStageSection?.id)
)
)
.compact()
.value();

const updatedSections = sections.map(section => {
//If the section is part of any of the rule actions, update the section
const updatedSection = allSectionsRequiringUpdate.includes(section.code)
? this.updateSection(section, rules)
? this.updateSection(section, questionnaire.rules)
: section;

return {
...updatedSection,
questions: QuestionnaireQuestion.updateQuestions(
updatedSection.questions,
updatedQuestion,
rules,
questionnaire.rules,
questionnaire,
updatedSection.isVisible === false && section.isVisible === true
),
Expand Down
18 changes: 18 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2778,6 +2778,14 @@
material-ui "^0.20.0"
rxjs "^5.5.7"

"@dhis2/expression-parser@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@dhis2/expression-parser/-/expression-parser-1.1.0.tgz#fcb4405c3621b7f754fc5c639f1a9a5c987e200a"
integrity sha512-QR663COhDunSq0oOZNfx7e7NJWtPjByyh/ncU6Nr5q7s45mMy0vGSFV4reIRIwFqjYGEAXIh/YK/4DKHgL9lqA==
dependencies:
"@js-joda/core" "3.2.0"
format-util "^1.0.5"

"@dhis2/prop-types@^1.6.4":
version "1.6.4"
resolved "https://registry.yarnpkg.com/@dhis2/prop-types/-/prop-types-1.6.4.tgz#ec4d256c9440d4d00071524422a727c61ddaa6f6"
Expand Down Expand Up @@ -3613,6 +3621,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"

"@js-joda/[email protected]":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==

"@juggle/resize-observer@^3.3.1":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
Expand Down Expand Up @@ -6987,6 +7000,11 @@ form-data@^4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"

format-util@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271"
integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==

[email protected]:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
Expand Down

0 comments on commit 92247a3

Please sign in to comment.