-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add script to recode boolean data values in events
- Loading branch information
Showing
7 changed files
with
305 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,29 @@ | ||
import { Id } from "./Base"; | ||
import { Maybe } from "utils/ts-utils"; | ||
import { Id, NamedRef } from "./Base"; | ||
|
||
export type ProgramType = "WITH_REGISTRATION" | "WITHOUT_REGISTRATION"; | ||
|
||
export interface Program { | ||
id: Id; | ||
name: string; | ||
programType: ProgramType; | ||
programStages: ProgramStage[]; | ||
} | ||
|
||
type ProgramStage = { | ||
id: Id; | ||
programStageDataElements: ProgramStageDataElement[]; | ||
}; | ||
|
||
type ProgramStageDataElement = { | ||
dataElement: DataElement; | ||
displayInReports: boolean; | ||
}; | ||
|
||
type DataElement = { | ||
id: Id; | ||
name: string; | ||
code: string; | ||
valueType: string; | ||
optionSet: Maybe<NamedRef>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
228 changes: 228 additions & 0 deletions
228
src/domain/usecases/RecodeBooleanDataValuesInEventsUseCase.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
import fs from "fs"; | ||
import _ from "lodash"; | ||
import { promiseMap } from "data/dhis2-utils"; | ||
import { getId, Id, Ref } from "domain/entities/Base"; | ||
import { D2Api } from "types/d2-api"; | ||
import logger from "utils/log"; | ||
import { D2TrackerEvent } from "@eyeseetea/d2-api/api/trackerEvents"; | ||
import { ProgramEventsRepository } from "domain/repositories/ProgramEventsRepository"; | ||
import { ProgramsRepository } from "domain/repositories/ProgramsRepository"; | ||
import { Program } from "domain/entities/Program"; | ||
import { Maybe } from "utils/ts-utils"; | ||
|
||
type Options = { | ||
programId: Id; | ||
ternaryOptionSetId: Id; | ||
post: boolean; | ||
}; | ||
|
||
export class RecodeBooleanDataValuesInEventsUseCase { | ||
pageSize = 1000; | ||
|
||
constructor( | ||
private api: D2Api, | ||
private programsRepository: ProgramsRepository, | ||
private eventsRepository: ProgramEventsRepository | ||
) {} | ||
|
||
async execute(options: Options) { | ||
const program = await this.getProgram(options.programId); | ||
await this.fixEventsInProgram({ ...options, program: program }); | ||
} | ||
|
||
async getProgram(id: Id): Promise<Program> { | ||
const programs = await this.programsRepository.get({ ids: [id] }); | ||
const program = programs[0]; | ||
if (!program) { | ||
throw new Error(`Program not found: ${id}`); | ||
} | ||
return program; | ||
} | ||
|
||
async fixEventsInProgram(options: { program: Program; post: boolean; ternaryOptionSetId: Id }) { | ||
const pageCount = await this.getPageCount(options); | ||
|
||
await promiseMap(_.range(1, pageCount + 1), async page => { | ||
await this.fixEventsForPage({ | ||
...options, | ||
page: page, | ||
pageCount: pageCount, | ||
ternaryOptionSetId: options.ternaryOptionSetId, | ||
}); | ||
}); | ||
} | ||
|
||
private async getPageCount(options: { program: Program }) { | ||
const response = await this.api.tracker.events | ||
.get({ | ||
...params, | ||
page: 1, | ||
pageSize: 0, | ||
totalPages: true, | ||
program: options.program.id, | ||
}) | ||
.getData(); | ||
|
||
const pageCount = Math.ceil((response.total || 0) / this.pageSize); | ||
logger.info(`Total: ${response.total} -> pages: ${pageCount} (pageSize: ${this.pageSize})`); | ||
|
||
return pageCount; | ||
} | ||
|
||
async fixEventsForPage(options: { | ||
program: Program; | ||
page: number; | ||
pageCount: number; | ||
post: boolean; | ||
ternaryOptionSetId: Id; | ||
}) { | ||
const events = await this.getEvents(options); | ||
const recodedEvents = this.getRecodedEvents({ | ||
program: options.program, | ||
events: events, | ||
ternaryOptionSetId: options.ternaryOptionSetId, | ||
}); | ||
logger.info(`Events to recode: ${recodedEvents.length}`); | ||
|
||
if (_(recodedEvents).isEmpty()) { | ||
return; | ||
} else if (!options.post) { | ||
logger.info(`Add --post to update events`); | ||
} else { | ||
await this.saveEvents(recodedEvents); | ||
} | ||
} | ||
|
||
getRecodedEvents(options: { | ||
program: Program; | ||
events: D2TrackerEvent[]; | ||
ternaryOptionSetId: Id; | ||
}): D2TrackerEvent[] { | ||
const dataElementIdsWithTernary = this.getDataElementIdsWithTernaryOptionSet(options); | ||
|
||
return _(options.events) | ||
.map((event): Maybe<typeof event> => { | ||
return this.fixEvent(event, dataElementIdsWithTernary, options); | ||
}) | ||
.compact() | ||
.value(); | ||
} | ||
|
||
private fixEvent( | ||
event: D2TrackerEvent, | ||
dataElementIdsWithTernary: Set<string>, | ||
options: { program: Program } | ||
): Maybe<D2TrackerEvent> { | ||
const updatedDataValues = this.recodeEvent(event, dataElementIdsWithTernary); | ||
if (_.isEqual(event.dataValues, updatedDataValues)) return undefined; | ||
|
||
const dataElementIdsUsedInDataValue = _(event.dataValues) | ||
.map(dv => dv.dataElement) | ||
.uniq() | ||
.value(); | ||
|
||
const dataElementIdsInProgramStage = _(options.program.programStages) | ||
.filter(programStage => programStage.id === event.programStage) | ||
.flatMap(programStage => programStage.programStageDataElements) | ||
.map(psde => psde.dataElement.id) | ||
.uniq() | ||
.value(); | ||
|
||
const dataElementIdsUsedAndNotCurrentlyAssigned = _.difference( | ||
dataElementIdsUsedInDataValue, | ||
dataElementIdsInProgramStage | ||
); | ||
|
||
if (!_(dataElementIdsUsedAndNotCurrentlyAssigned).isEmpty()) { | ||
const tail = dataElementIdsUsedAndNotCurrentlyAssigned.join(" "); | ||
const head = `[skip] event.id=${event.event}`; | ||
const msg = `${head} [programStage.id=${event.programStage}] has unassigned dataElements: ${tail}`; | ||
logger.error(msg); | ||
return undefined; | ||
} else { | ||
return { ...event, dataValues: updatedDataValues }; | ||
} | ||
} | ||
|
||
private recodeEvent(event: D2TrackerEvent, dataElementIdsWithTernary: Set<string>) { | ||
return _(event.dataValues) | ||
.map((dataValue): typeof dataValue => { | ||
if (dataElementIdsWithTernary.has(dataValue.dataElement)) { | ||
// TODO: User options from optionSet | ||
const newValue = ["true", "Yes"].includes(dataValue.value) ? "Yes" : "No"; | ||
return { ...dataValue, value: newValue }; | ||
} else { | ||
return dataValue; | ||
} | ||
}) | ||
.value(); | ||
} | ||
|
||
private getDataElementIdsWithTernaryOptionSet(options: { | ||
program: Program; | ||
events: D2TrackerEvent[]; | ||
ternaryOptionSetId: Id; | ||
}) { | ||
const dataElements = _(options.program.programStages).flatMap(programStage => { | ||
return programStage.programStageDataElements.map(programStageDataElement => { | ||
return programStageDataElement.dataElement; | ||
}); | ||
}); | ||
|
||
const dataElementIdsWithTernaryOptions = new Set( | ||
dataElements | ||
.filter(dataElement => dataElement.optionSet?.id === options.ternaryOptionSetId) | ||
.map(getId) | ||
.value() | ||
); | ||
return dataElementIdsWithTernaryOptions; | ||
} | ||
|
||
private async saveEvents(events: D2TrackerEvent[]) { | ||
logger.info(`Post events: ${events.length}`); | ||
fs.writeFileSync("events.json", JSON.stringify(events, null, 2)); | ||
|
||
const response = await this.api.tracker | ||
.post( | ||
{ | ||
async: false, | ||
skipPatternValidation: true, | ||
skipSideEffects: true, | ||
skipRuleEngine: true, | ||
importMode: "COMMIT", | ||
}, | ||
{ events: events } | ||
) | ||
.getData(); | ||
|
||
logger.info(`Post result: ${JSON.stringify(response.stats)}`); | ||
} | ||
|
||
private async getEvents(options: { | ||
program: Ref; | ||
page: number; | ||
post: boolean; | ||
pageCount: number; | ||
}): Promise<D2TrackerEvent[]> { | ||
logger.info(`Get events: page ${options.page} of ${options.pageCount}`); | ||
|
||
const response = await this.api.tracker.events | ||
.get({ | ||
...params, | ||
page: options.page, | ||
pageSize: this.pageSize, | ||
program: options.program.id, | ||
}) | ||
.getData(); | ||
|
||
logger.info(`Events: ${response.instances.length}`); | ||
|
||
return response.instances; | ||
} | ||
} | ||
|
||
const params = { | ||
fields: { | ||
$all: true, | ||
}, | ||
} as const; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters