Skip to content

Commit

Permalink
chore: merge with development
Browse files Browse the repository at this point in the history
  • Loading branch information
9sneha-n committed Jan 19, 2025
2 parents 043d2d1 + cf693e2 commit 8e09d13
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 49 deletions.
1 change: 0 additions & 1 deletion i18n/es.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"

msgid "There was a problem with \"{{name}}\" - {{prop}} is not set"
msgstr ""

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "amr-surveys",
"description": "AMR Surveys App",
"version": "0.8.0",
"version": "0.9.0",
"license": "GPL-3.0",
"author": "EyeSeeTea team",
"homepage": ".",
Expand Down
40 changes: 30 additions & 10 deletions src/data/entities/D2ExpressionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,42 @@ export class D2ExpressionParser {
ruleCondition,
xp.ExpressionMode.RULE_ENGINE_CONDITION
);
const expressionData = this.getExpressionDataJs(expressionParser, variableValues);
const parsedResult: boolean = expressionParser.evaluate(() => {}, expressionData);
return Either.success(parsedResult);
} catch (error) {
return Either.error(error as Error);
}
}

const ruleVariables = this.mapProgramRuleVariables(expressionParser, variableValues);
const genericVariables = this.mapProgramVariables(expressionParser);
const variables = new Map([...ruleVariables, ...genericVariables]);

const expressionData = new xp.ExpressionDataJs(variables);

const parsedResult: boolean = expressionParser.evaluate(
() => console.debug(""),
expressionData
public evaluateActionExpression(
expression: string,
variableValues: Map<ProgramRuleVariableName, ProgramRuleVariableValue>
): Either<Error, EvaluatedExpressionResult> {
try {
const expressionParser = new xp.ExpressionJs(
expression,
xp.ExpressionMode.RULE_ENGINE_ACTION
);

const expressionData = this.getExpressionDataJs(expressionParser, variableValues);
const parsedResult: EvaluatedExpressionResult = expressionParser.evaluate(() => {},
expressionData);
return Either.success(parsedResult);
} catch (error) {
return Either.error(error as Error);
}
}

private getExpressionDataJs = (
expressionParser: xp.ExpressionJs,
variableValues: Map<ProgramRuleVariableName, ProgramRuleVariableValue>
): xp.ExpressionDataJs => {
const ruleVariables = this.mapProgramRuleVariables(expressionParser, variableValues);
const genericVariables = this.mapProgramVariables(expressionParser);
const variables = new Map([...ruleVariables, ...genericVariables]);
return new xp.ExpressionDataJs(variables);
};

private getVariableValueByType = (
type: ProgramRuleVariableType,
stringValue: xp.Nullable<string>
Expand Down Expand Up @@ -106,3 +124,5 @@ const VariableValueTypeMap: Record<ProgramRuleVariableType, xp.ValueType> = {
date: xp.ValueType.DATE,
number: xp.ValueType.NUMBER,
};

export type EvaluatedExpressionResult = boolean | string | number | Date | null;
17 changes: 15 additions & 2 deletions src/data/utils/questionHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import _ from "../../domain/entities/generic/Collection";
import { D2TrackerEvent } from "@eyeseetea/d2-api/api/trackerEvents";
import { D2TrackerTrackedEntity as TrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities";
import { isValidDate } from "../../utils/dates";
import i18n from "../../utils/i18n";

const SPECIES_QUESTION_FORNAME = "Specify the specie";
Expand Down Expand Up @@ -182,7 +183,7 @@ export const getQuestion = (
const dateQ: DateQuestion = {
...base,
type: "date",
value: dataValue ? new Date(dataValue) : undefined,
value: parseQuestionDate(dataValue, base),
};
return dateQ;
}
Expand All @@ -191,13 +192,25 @@ export const getQuestion = (
const dateQ: DateTimeQuestion = {
...base,
type: "datetime",
value: dataValue ? new Date(dataValue) : undefined,
value: parseQuestionDate(dataValue, base),
};
return dateQ;
}
}
};

const parseQuestionDate = (
dateStr: string | undefined,
question: QuestionBase
): Date | undefined => {
if (!dateStr) return undefined;
const result = isValidDate(new Date(dateStr)) ? new Date(dateStr) : undefined;
if (dateStr && !result) {
console.debug(`Invalid date value: ${dateStr}`, question);
}
return result;
};

export const mapQuestionsToDataValues = (questions: Question[]): DataValue[] => {
const dataValues = _(
questions.map(question => {
Expand Down
33 changes: 26 additions & 7 deletions src/data/utils/surveyFormMappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
} from "@eyeseetea/d2-api/api/trackerEnrollments";
import { DataValue } from "@eyeseetea/d2-api";
import { generateUid } from "../../utils/uid";
import i18n from "../../utils/i18n";

const AntibioticTreatmentHospitalEpisodeSectionName =
`Antibiotic treatments during hospital episode`.toLowerCase();
Expand Down Expand Up @@ -300,7 +301,12 @@ export const mapQuestionnaireToEvent = (
stages.sections.flatMap(section => section.questions)
);

const dataValues = mapQuestionsToDataValues(questions);
let dataValues: DataValue[] = [];
try {
dataValues = mapQuestionsToDataValues(questions);
} catch (error) {
return Future.error(new Error(i18n.t("There was an error processing the form")));
}

if (eventId) {
return getEventProgramById(eventId, api).flatMap(event => {
Expand Down Expand Up @@ -329,13 +335,12 @@ export const mapQuestionnaireToEvent = (
}
};

export const mapQuestionnaireToTrackedEntities = (
const mapQuestionnaireToEventsByStage = (
questionnaire: Questionnaire,
orgUnitId: string,
programId: Id,
teiId: string | undefined = undefined
): FutureData<{ trackedEntities: TrackedEntity[] }> => {
const eventsByStage: D2TrackerEvent[] = questionnaire.stages.map(stage => {
programId: Id
): D2TrackerEvent[] => {
return questionnaire.stages.map(stage => {
const dataValuesByStage = stage.sections.flatMap(section => {
return mapQuestionsToDataValues(section.questions);
});
Expand All @@ -345,11 +350,25 @@ export const mapQuestionnaireToTrackedEntities = (
event: stage.instanceId ?? "",
programStage: stage.code,
orgUnit: orgUnitId,
dataValues: dataValuesByStage as DataValue[],
dataValues: dataValuesByStage,
occurredAt: new Date().getTime().toString(),
status: "ACTIVE",
};
});
};

export const mapQuestionnaireToTrackedEntities = (
questionnaire: Questionnaire,
orgUnitId: string,
programId: Id,
teiId: string | undefined = undefined
): FutureData<{ trackedEntities: TrackedEntity[] }> => {
let eventsByStage: D2TrackerEvent[] = [];
try {
eventsByStage = mapQuestionnaireToEventsByStage(questionnaire, orgUnitId, programId);
} catch (error) {
return Future.error(new Error(i18n.t("There was an error processing the form")));
}

const attributes: D2TrackerEnrollmentAttribute[] = questionnaire.entity
? questionnaire.entity.questions.map(question => {
Expand Down
63 changes: 60 additions & 3 deletions src/domain/entities/Questionnaire/QuestionnaireQuestion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Maybe, assertUnreachable } from "../../../utils/ts-utils";
import { Id, NamedRef } from "../Ref";
import { getApplicableRules, QuestionnaireRule } from "./QuestionnaireRules";
import {
getApplicableRules,
getQuestionValueFromEvaluatedExpression,
QuestionnaireRule,
} from "./QuestionnaireRules";
import _ from "../generic/Collection";
import { Questionnaire } from "./Questionnaire";

Expand Down Expand Up @@ -286,18 +290,71 @@ export class QuestionnaireQuestion {
return finalUpdatesWithSideEffects;
}

private static updateQuestion(question: Question, rules: QuestionnaireRule[]): Question {
private static updateQuestion<T extends Question>(question: T, rules: QuestionnaireRule[]): T {
const updatedIsVisible = this.isQuestionVisible(question, rules);
const updatedErrors = this.getQuestionWarningsAndErrors(question, rules);

const updatedIsDisabled = this.isQuestionDisabled(question, rules);
const updatedValue = this.getQuestionAssignValue(question, rules);
return {
...question,
isVisible: updatedIsVisible,
errors: updatedErrors,
disabled: updatedIsDisabled,
value: updatedValue,
...(question.isVisible !== updatedIsVisible ? { value: undefined } : {}),
};
}

private static getRulesWithAssignActionForQuestion(
question: Question,
rules: QuestionnaireRule[]
) {
return rules.filter(
rule =>
rule.parsedResult &&
rule.actions.filter(
action =>
action.programRuleActionType === "ASSIGN" &&
((action.dataElement && action.dataElement.id === question.id) ||
(action.trackedEntityAttribute &&
action.trackedEntityAttribute.id === question.id))
).length > 0
);
}

private static isQuestionDisabled(
question: Question,
rules: QuestionnaireRule[]
): boolean | undefined {
const applicableRules = this.getRulesWithAssignActionForQuestion(question, rules);
return applicableRules.length > 0 ? true : question.disabled;
}

private static getQuestionAssignValue(
question: Question,
rules: QuestionnaireRule[]
): Question["value"] {
const applicableActions = this.getRulesWithAssignActionForQuestion(question, rules).flatMap(
rule => rule.actions
);
if (applicableActions.length === 0) {
return question.value;
} else {
if (applicableActions.length > 1) {
console.warn(
"Multiple ASSIGN actions found for question: ",
question,
"Applying first rule:",
applicableActions[0]
);
}
return getQuestionValueFromEvaluatedExpression(
question,
applicableActions[0]?.dataEvaluated
);
}
}

private static isQuestionVisible(question: Question, rules: QuestionnaireRule[]): boolean {
//Check of there are any rules applicable to the current question
//with hide field action
Expand Down
72 changes: 69 additions & 3 deletions src/domain/entities/Questionnaire/QuestionnaireRules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
D2ExpressionParser,
EvaluatedExpressionResult,
ProgramRuleVariableName,
ProgramRuleVariableValue,
} from "../../../data/entities/D2ExpressionParser";
Expand Down Expand Up @@ -31,7 +32,8 @@ export interface QuestionnaireRuleAction {
trackedEntityAttribute?: {
id: Id | undefined; // to hide
};
data?: string; // to assign
data?: string; // to assign (expression raw value)
dataEvaluated?: EvaluatedExpressionResult; // to assign (calculated/evaluated value)
programStageSection?: {
id: Id | undefined; // to hide/show
};
Expand Down Expand Up @@ -65,10 +67,18 @@ export const getApplicableRules = (
);

//2. Run the rule conditions and return rules with parsed results
// Also augment rule actions with results of the `data` expression evaluation
const parsedApplicableRules = applicableRules.map(rule => {
const expressionParserResult = parseConditionWithExpressionParser(rule, questions);

return { ...rule, parsedResult: expressionParserResult };
const actionsWithEvaluatedDataExpressions = getActionsWithEvaluatedDataExpression(
rule,
questions
);
return {
...rule,
parsedResult: expressionParserResult,
actions: actionsWithEvaluatedDataExpressions,
};
});

return parsedApplicableRules;
Expand Down Expand Up @@ -101,6 +111,25 @@ export const getQuestionValueByType = (question: Question): string => {
}
};

export function getQuestionValueFromEvaluatedExpression(
question: Question,
dataEvaluated?: EvaluatedExpressionResult
): Question["value"] {
// TODO: handle possible mismatches between question value type and dataEvaluated type
// e.g. question.type is "date" but dataEvaluated is a number
if (dataEvaluated === null) {
return undefined;
} else if (question.type === "select") {
const option = question.options.find(option => option.code === dataEvaluated);
if (!option) console.warn("Option not found in question for code:", dataEvaluated);
return option;
} else if (typeof dataEvaluated === "number") {
return dataEvaluated.toString();
} else {
return dataEvaluated;
}
}

function getProgramRuleVariableValues(
programRuleVariables: Maybe<D2ProgramRuleVariable[]>,
questions: Question[]
Expand Down Expand Up @@ -154,3 +183,40 @@ const parseConditionWithExpressionParser = (
},
});
};

/**
* Get the actions from the rule, augmenting them with the `dataEvaluated` property - Only for ASSIGN actions
* `dataEvaluated` is set with the results of running the D2ExpressionParser evaluation
*/
const getActionsWithEvaluatedDataExpression = (
rule: QuestionnaireRule,
questions: Question[]
): QuestionnaireRuleAction[] => {
const programRuleVariableValues = getProgramRuleVariableValues(
rule.programRuleVariables,
questions
);
const parser = new D2ExpressionParser();

return rule.actions.map(action => {
if (!action.data || action.programRuleActionType !== "ASSIGN") {
return action;
}
return {
...action,
dataEvaluated: parser
.evaluateActionExpression(action.data, programRuleVariableValues)
.match({
success: evaluationResult => evaluationResult,
error: errMsg => {
console.error(
"Error evaluating ASSIGN data expression",
action.data,
errMsg
);
return null;
},
}),
};
});
};
9 changes: 5 additions & 4 deletions src/domain/usecases/GetSurveyUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,12 @@ export class GetSurveyUseCase {
sections: updatedSections,
};

const updatedStages = questionnaire.stages.map(stage =>
stage.id === updatedStage.id ? updatedStage : stage
);

return Future.success(
Questionnaire.updateQuestionnaireStages(questionnaire, [
...questionnaire.stages,
updatedStage,
])
Questionnaire.updateQuestionnaireStages(questionnaire, updatedStages)
);
} else {
return Future.success(questionnaire);
Expand Down
Loading

0 comments on commit 8e09d13

Please sign in to comment.