diff --git a/src/api/analytics/Analytics.js b/src/api/analytics/Analytics.js index d08872524..0e0ddba2f 100644 --- a/src/api/analytics/Analytics.js +++ b/src/api/analytics/Analytics.js @@ -6,12 +6,15 @@ import AnalyticsEnrollments from './AnalyticsEnrollments.js' import AnalyticsEvents from './AnalyticsEvents.js' import AnalyticsRequest from './AnalyticsRequest.js' import AnalyticsResponse from './AnalyticsResponse.js' +import AnalyticsTrackedEntities from './AnalyticsTrackedEntities.js' /** * @description * Analytics class used to request analytics data from Web API. * * @requires analytics.AnalyticsAggregate + * @requires analytics.AnalyticsTrackedEntities + * @requires analytics.AnalyticsEnrollments * @requires analytics.AnalyticsEvents * @requires analytics.AnalyticsRequest * @requires analytics.AnalyticsResponse @@ -31,13 +34,22 @@ import AnalyticsResponse from './AnalyticsResponse.js' class Analytics { /** * @param {!module:analytics.AnalyticsAggregate} analyticsAggregate The AnalyticsAggregate instance + * @param {!module:analytics.AnalyticsTrackedEntities} analyticsTrackedEntities The AnalyticsTrackedEntities instance * @param {!module:analytics.AnalyticsEnrollments} analyticsEnrollments The AnalyticsEnrollments instance * @param {!module:analytics.AnalyticsEvents} analyticsEvents The AnalyticsEvents instance * @param {!module:analytics.AnalyticsRequest} analyticsRequest The AnalyticsRequest class * @param {!module:analytics.AnalyticsResponse} analyticsResponse The AnalyticsResponse class */ - constructor({ aggregate, enrollments, events, request, response }) { + constructor({ + aggregate, + trackedEntities, + enrollments, + events, + request, + response, + }) { this.aggregate = aggregate + this.trackedEntities = trackedEntities this.enrollments = enrollments this.events = events this.request = request @@ -60,6 +72,7 @@ class Analytics { if (!Analytics.getAnalytics.analytics) { Analytics.getAnalytics.analytics = new Analytics({ aggregate: new AnalyticsAggregate(dataEngine), + trackedEntities: new AnalyticsTrackedEntities(dataEngine), enrollments: new AnalyticsEnrollments(dataEngine), events: new AnalyticsEvents(dataEngine), request: AnalyticsRequest, diff --git a/src/api/analytics/AnalyticsBase.js b/src/api/analytics/AnalyticsBase.js index 70d2c2b5b..9d5d82cff 100644 --- a/src/api/analytics/AnalyticsBase.js +++ b/src/api/analytics/AnalyticsBase.js @@ -1,11 +1,15 @@ import sortBy from 'lodash/sortBy' import AnalyticsRequest from './AnalyticsRequest.js' +import { formatRequestPath } from './utils.js' const analyticsQuery = { resource: 'analytics', - id: ({ path, program }) => { - return [path, program].filter(Boolean).join('/') - }, + id: ({ path, program, trackedEntityType }) => + formatRequestPath({ + path, + program, + trackedEntityType, + }), params: ({ dimensions, filters, parameters }) => ({ dimension: dimensions.length ? dimensions : undefined, filter: filters.length ? filters : undefined, @@ -15,9 +19,12 @@ const analyticsQuery = { const analyticsDataQuery = { resource: 'analytics', - id: ({ path, program }) => { - return [path, program].filter(Boolean).join('/') - }, + id: ({ path, program, trackedEntityType }) => + formatRequestPath({ + path, + program, + trackedEntityType, + }), params: ({ dimensions, filters, parameters }) => { return { dimension: dimensions.length ? dimensions : undefined, @@ -31,9 +38,12 @@ const analyticsDataQuery = { const analyticsMetaDataQuery = { resource: 'analytics', - id: ({ path, program }) => { - return [path, program].filter(Boolean).join('/') - }, + id: ({ path, program, trackedEntityType }) => + formatRequestPath({ + path, + program, + trackedEntityType, + }), params: ({ dimensions, filters, parameters }) => ({ dimension: dimensions.length ? dimensions : undefined, filter: filters.length ? filters : undefined, @@ -120,6 +130,7 @@ class AnalyticsBase { variables: { path: req.path, program: req.program, + trackedEntityType: req.trackedEntityType, dimensions: generateDimensionStrings(req.dimensions), filters: generateDimensionStrings(req.filters), parameters: req.parameters, @@ -164,6 +175,7 @@ class AnalyticsBase { variables: { path: req.path, program: req.program, + trackedEntityType: req.trackedEntityType, dimensions: generateDimensionStrings( req.dimensions, options diff --git a/src/api/analytics/AnalyticsRequest.js b/src/api/analytics/AnalyticsRequest.js index 5b7d4b70e..b3ff28ce4 100644 --- a/src/api/analytics/AnalyticsRequest.js +++ b/src/api/analytics/AnalyticsRequest.js @@ -4,6 +4,7 @@ import AnalyticsRequestBase from './AnalyticsRequestBase.js' import AnalyticsRequestDimensionsMixin from './AnalyticsRequestDimensionsMixin.js' import AnalyticsRequestFiltersMixin from './AnalyticsRequestFiltersMixin.js' import AnalyticsRequestPropertiesMixin from './AnalyticsRequestPropertiesMixin.js' +import { formatDimension } from './utils.js' /** * @description @@ -45,6 +46,8 @@ class AnalyticsRequest extends AnalyticsRequestDimensionsMixin( fromVisualization(visualization, passFilterAsDimension = false) { let request = this + const outputType = visualization.outputType + // extract dimensions from visualization const columns = visualization.columns || [] const rows = visualization.rows || [] @@ -56,23 +59,31 @@ class AnalyticsRequest extends AnalyticsRequestDimensionsMixin( dimension += `-${d.legendSet.id}` } - if (d.programStage?.id) { - dimension = `${d.programStage.id}.${dimension}` - } - if (d.filter) { dimension += `:${d.filter}` } + const programStageId = d.programStage?.id + if (d.repetition?.indexes?.length) { d.repetition.indexes.forEach((index) => { request = request.addDimension( - dimension.replace(/\./, `[${index}].`) + formatDimension({ + programId: d.program?.id, + programStageId: `${programStageId}[${index}]`, + dimension, + outputType, + }) ) }) } else { request = request.addDimension( - dimension, + formatDimension({ + programId: d.program?.id, + programStageId, + dimension, + outputType, + }), d.items?.map((item) => item.id) ) } @@ -91,23 +102,33 @@ class AnalyticsRequest extends AnalyticsRequestDimensionsMixin( f.items?.map((item) => item.id) ) } else { - let filterString = f.programStage?.id - ? `${f.programStage.id}.${f.dimension}` - : f.dimension + let filterString = f.dimension if (f.filter) { filterString += `:${f.filter}` } + const programStageId = f.programStage?.id + if (f.repetition?.indexes?.length) { f.repetition.indexes.forEach((index) => { request = request.addFilter( - filterString.replace(/\./, `[${index}].`) + formatDimension({ + programId: f.program?.id, + programStageId: `${programStageId}[${index}]`, + dimension: filterString, + outputType, + }) ) }) } else { request = request.addFilter( - filterString, + formatDimension({ + programId: f.program?.id, + programStageId, + dimension: filterString, + outputType, + }), f.items?.map((item) => item.id) ) } diff --git a/src/api/analytics/AnalyticsRequestBase.js b/src/api/analytics/AnalyticsRequestBase.js index 2bc24b108..ba317ae38 100644 --- a/src/api/analytics/AnalyticsRequestBase.js +++ b/src/api/analytics/AnalyticsRequestBase.js @@ -17,6 +17,7 @@ class AnalyticsRequestBase { format = 'json', path, program, + trackedEntityType, dimensions = [], filters = [], parameters = {}, @@ -25,6 +26,7 @@ class AnalyticsRequestBase { this.format = format.toLowerCase() this.path = path this.program = program + this.trackedEntityType = trackedEntityType this.dimensions = dimensions this.filters = filters @@ -63,8 +65,13 @@ class AnalyticsRequestBase { return dimension }) - const endPoint = [this.endPoint, this.path, this.program] - .filter((e) => !!e) + const endPoint = [ + this.endPoint, + this.path, + this.program, + this.trackedEntityType, + ] + .filter(Boolean) .join('/') let url = `${endPoint}.${this.format}` diff --git a/src/api/analytics/AnalyticsRequestPropertiesMixin.js b/src/api/analytics/AnalyticsRequestPropertiesMixin.js index 6dd0f8cec..7d5ff0531 100644 --- a/src/api/analytics/AnalyticsRequestPropertiesMixin.js +++ b/src/api/analytics/AnalyticsRequestPropertiesMixin.js @@ -514,6 +514,26 @@ const AnalyticsRequestPropertiesMixin = (base) => return new AnalyticsRequest(this) } + /** + * Sets the tracked entity type for the request. + * It appends the tracked entity type id to the request's path. + * + * @param {!String} trackedEntityType The tracked entity type id + * + * @returns {AnalyticsRequest} A new instance of the class for chaining purposes + * + * @example + * const req = new analytics.request() + * .withTrackedEntityType('nEenWmSyUEp'); + */ + withTrackedEntityType(trackedEntityType) { + if (trackedEntityType) { + this.trackedEntityType = trackedEntityType + } + + return new AnalyticsRequest(this) + } + /** * Sets the program for the request. * It appends the program id to the request's path. diff --git a/src/api/analytics/AnalyticsTrackedEntities.js b/src/api/analytics/AnalyticsTrackedEntities.js new file mode 100644 index 000000000..3756be0e8 --- /dev/null +++ b/src/api/analytics/AnalyticsTrackedEntities.js @@ -0,0 +1,25 @@ +import AnalyticsBase from './AnalyticsBase.js' + +/** + * @extends module:analytics.AnalyticsBase + * + * @description + * Analytics tracked entities class used to request analytics tracked entities data from Web API. + * + * @memberof module:analytics + */ +class AnalyticsTrackedEntities extends AnalyticsBase { + /** + * @param {!AnalyticsRequest} req Request object + * + * @returns {Promise} Promise that resolves with the analytics query data from the api. + * + * @example + // TODO: provide working example + */ + getQuery(req) { + return this.fetch(req.withPath('trackedEntities/query')) + } +} + +export default AnalyticsTrackedEntities diff --git a/src/api/analytics/utils.js b/src/api/analytics/utils.js index bf766d69c..bb15681d2 100644 --- a/src/api/analytics/utils.js +++ b/src/api/analytics/utils.js @@ -7,3 +7,20 @@ const whitelistRegExp = new RegExp(`(?:${whitelistURICodes.join('|')})`, 'g') export const customEncodeURIComponent = (uri) => encodeURIComponent(uri).replace(whitelistRegExp, decodeURIComponent) + +export const formatRequestPath = ({ path, program, trackedEntityType }) => + [path, program, trackedEntityType].filter(Boolean).join('/') + +export const formatDimension = ({ + outputType, + programId, + programStageId, + dimension, +}) => + [ + outputType === 'TRACKED_ENTITY_INSTANCE' ? programId : undefined, + programStageId, + dimension, + ] + .filter(Boolean) + .join('.') diff --git a/src/modules/layout/axisGetDimensionIds.js b/src/modules/layout/axisGetDimensionIds.js index 0c7023e45..3db877e8a 100644 --- a/src/modules/layout/axisGetDimensionIds.js +++ b/src/modules/layout/axisGetDimensionIds.js @@ -1,7 +1,7 @@ import { AXIS } from './axis.js' import { dimensionGetId } from './dimensionGetId.js' -export const axisGetDimensionIds = (axis) => +export const axisGetDimensionIds = (axis, outputType) => AXIS.isValid(axis) - ? axis.map((dimension) => dimensionGetId(dimension)) + ? axis.map((dimension) => dimensionGetId(dimension, outputType)) : AXIS.defaultValue diff --git a/src/modules/layout/dimension.js b/src/modules/layout/dimension.js index 5133b4759..fb6454467 100644 --- a/src/modules/layout/dimension.js +++ b/src/modules/layout/dimension.js @@ -37,6 +37,13 @@ export const DIMENSION_PROP_LEGEND_SET = { isValid: (prop) => isString(prop), } +export const DIMENSION_PROP_PROGRAM = { + name: 'program', + defaultValue: {}, + required: false, + isValid: (prop) => isObject(prop), +} + export const DIMENSION_PROP_PROGRAM_STAGE = { name: 'programStage', defaultValue: {}, @@ -56,6 +63,7 @@ export const DIMENSION_PROPS = [ DIMENSION_PROP_ITEMS, DIMENSION_PROP_FILTER, DIMENSION_PROP_LEGEND_SET, + DIMENSION_PROP_PROGRAM, DIMENSION_PROP_PROGRAM_STAGE, DIMENSION_PROP_REPETITION, ] diff --git a/src/modules/layout/dimensionCreate.js b/src/modules/layout/dimensionCreate.js index 7fb11b39c..261edeb3b 100644 --- a/src/modules/layout/dimensionCreate.js +++ b/src/modules/layout/dimensionCreate.js @@ -3,6 +3,7 @@ import { DIMENSION_PROP_ITEMS, DIMENSION_PROP_FILTER, DIMENSION_PROP_LEGEND_SET, + DIMENSION_PROP_PROGRAM, DIMENSION_PROP_PROGRAM_STAGE, DIMENSION_PROP_REPETITION, } from './dimension.js' @@ -17,6 +18,9 @@ export const dimensionCreate = (dimensionId, itemIds = [], args = {}) => { ...(args.legendSet && { [DIMENSION_PROP_LEGEND_SET.name]: args.legendSet, }), + ...(args.program && { + [DIMENSION_PROP_PROGRAM.name]: args.program, + }), ...(args.programStage && { [DIMENSION_PROP_PROGRAM_STAGE.name]: args.programStage, }), diff --git a/src/modules/layout/dimensionGetId.js b/src/modules/layout/dimensionGetId.js index 2354a860d..f5d63b860 100644 --- a/src/modules/layout/dimensionGetId.js +++ b/src/modules/layout/dimensionGetId.js @@ -1,8 +1,14 @@ -import { DIMENSION_PROP_ID, DIMENSION_PROP_PROGRAM_STAGE } from './dimension.js' +import { formatDimension } from '../../api/analytics/utils.js' +import { + DIMENSION_PROP_ID, + DIMENSION_PROP_PROGRAM_STAGE, + DIMENSION_PROP_PROGRAM, +} from './dimension.js' -export const dimensionGetId = (dimension) => - dimension[DIMENSION_PROP_PROGRAM_STAGE.name]?.id - ? `${dimension[DIMENSION_PROP_PROGRAM_STAGE.name].id}.${ - dimension[DIMENSION_PROP_ID.name] - }` - : dimension[DIMENSION_PROP_ID.name] +export const dimensionGetId = (dimension, outputType) => + formatDimension({ + dimension: dimension[DIMENSION_PROP_ID.name], + programId: dimension[DIMENSION_PROP_PROGRAM.name]?.id, + programStageId: dimension[DIMENSION_PROP_PROGRAM_STAGE.name]?.id, + outputType, + }) diff --git a/src/modules/layout/layoutFilterDimensions.js b/src/modules/layout/layoutFilterDimensions.js index 0e9cd9250..ab8f9ff65 100644 --- a/src/modules/layout/layoutFilterDimensions.js +++ b/src/modules/layout/layoutFilterDimensions.js @@ -8,7 +8,10 @@ export const layoutFilterDimensions = (layout, dimensionIds) => { DEFAULT_AXIS_IDS.forEach((axisId) => { if (AXIS.isValid(filteredLayout[axisId])) { filteredLayout[axisId] = filteredLayout[axisId].filter( - (dimension) => !idArray.includes(dimensionGetId(dimension)) + (dimension) => + !idArray.includes( + dimensionGetId(dimension, layout.outputType) + ) ) } }) diff --git a/src/modules/layout/layoutGetAxisIdDimensionIdsObject.js b/src/modules/layout/layoutGetAxisIdDimensionIdsObject.js index f7ca0c8ad..c875b0567 100644 --- a/src/modules/layout/layoutGetAxisIdDimensionIdsObject.js +++ b/src/modules/layout/layoutGetAxisIdDimensionIdsObject.js @@ -4,7 +4,7 @@ import { axisGetDimensionIds } from './axisGetDimensionIds.js' export const layoutGetAxisIdDimensionIdsObject = (layout) => DEFAULT_AXIS_IDS.reduce((obj, axisId) => { if (AXIS.isValid(layout[axisId])) { - obj[axisId] = axisGetDimensionIds(layout[axisId]) + obj[axisId] = axisGetDimensionIds(layout[axisId], layout.outputType) } return obj diff --git a/src/modules/layout/layoutGetDimensionIdItemIdsObject.js b/src/modules/layout/layoutGetDimensionIdItemIdsObject.js index c98e1650e..0bb9e5724 100644 --- a/src/modules/layout/layoutGetDimensionIdItemIdsObject.js +++ b/src/modules/layout/layoutGetDimensionIdItemIdsObject.js @@ -4,6 +4,7 @@ import { layoutGetAllDimensions } from './layoutGetAllDimensions.js' export const layoutGetDimensionIdItemIdsObject = (layout) => layoutGetAllDimensions(layout).reduce((obj, dimension) => { - obj[dimensionGetId(dimension)] = dimensionGetItemIds(dimension) + obj[dimensionGetId(dimension, layout.outputType)] = + dimensionGetItemIds(dimension) return obj }, {}) diff --git a/src/modules/layout/layoutHasDynamicDimension.js b/src/modules/layout/layoutHasDynamicDimension.js index 586abdd1f..dc02b5f94 100644 --- a/src/modules/layout/layoutHasDynamicDimension.js +++ b/src/modules/layout/layoutHasDynamicDimension.js @@ -7,7 +7,8 @@ export const layoutHasDynamicDimension = (layout) => { return Boolean( layoutGetAllDimensions(layout).find( - (dimension) => !fixedIds.includes(dimensionGetId(dimension)) + (dimension) => + !fixedIds.includes(dimensionGetId(dimension, layout.outputType)) ) ) }