From a20a703df1ca62d838467486df5e50022bc0ae17 Mon Sep 17 00:00:00 2001 From: Luciano Fiandesio Date: Mon, 18 May 2026 14:17:39 +0200 Subject: [PATCH 1/2] fix: support PROGRAM_STATUS without filter as an aggregate analytics dimension (#23919) * fix: support PROGRAM_STATUS without filter as an aggregate analytics dimension * Remove debug code * Address sq warnings * Fix PROGRAM_STATUS query parsing regression --- .../org/hisp/dhis/common/DimensionType.java | 3 +- .../data/DefaultEventDataQueryService.java | 84 +- .../data/JdbcEnrollmentAnalyticsManager.java | 98 ++- .../event/data/JdbcEventAnalyticsManager.java | 12 +- .../tracker/MetadataItemsHandler.java | 36 +- .../DefaultEventDataQueryServiceTest.java | 68 +- .../tracker/MetadataItemsHandlerTest.java | 33 + .../EnrollmentsAggregate5AutoTest.java | 70 +- .../aggregate/EventsAggregate1AutoTest.java | 748 +++++++++++++----- .../scenarios/enroll-aggregated.json | 7 + .../generator/scenarios/event-aggregated.json | 21 + 11 files changed, 958 insertions(+), 222 deletions(-) diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionType.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionType.java index 88c17c57d32d..5332374e8eef 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionType.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionType.java @@ -50,7 +50,8 @@ public enum DimensionType { OPTION_GROUP_SET, VALIDATION_RULE, STATIC, - ORGANISATION_UNIT_LEVEL; + ORGANISATION_UNIT_LEVEL, + PROGRAM_STATUS; private static final int LAST_ORDER = 999; diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryService.java index a91ad08785ec..44722e990bb1 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryService.java @@ -77,6 +77,8 @@ import org.hisp.dhis.analytics.table.EnrollmentAnalyticsColumnName; import org.hisp.dhis.analytics.table.EventAnalyticsColumnName; import org.hisp.dhis.common.BaseDimensionalItemObject; +import org.hisp.dhis.common.BaseDimensionalObject; +import org.hisp.dhis.common.DimensionType; import org.hisp.dhis.common.DimensionalItemObject; import org.hisp.dhis.common.DimensionalObject; import org.hisp.dhis.common.EventAnalyticalObject; @@ -673,7 +675,12 @@ private void addFiltersToParams( continue; } if (isProgramStatusDimension(input.dimensionId())) { - enrollmentStatuses.addAll(parseEnrollmentStatuses(input.items(), input.rawDimension())); + if (isAggregateRequest(request)) { + requireNonEmptyStatusFilter(input.items(), input.rawDimension()); + params.addFilter(getProgramStatusDimension(input.items(), input.rawDimension())); + } else { + enrollmentStatuses.addAll(parseEnrollmentStatuses(input.items(), input.rawDimension())); + } continue; } @@ -718,7 +725,11 @@ private void addDimensionsToParams( continue; } if (isProgramStatusDimension(input.dimensionId())) { - enrollmentStatuses.addAll(parseEnrollmentStatuses(input.items(), input.rawDimension())); + if (isAggregateRequest(request)) { + params.addDimension(getProgramStatusDimension(input.items(), input.rawDimension())); + } else { + enrollmentStatuses.addAll(parseEnrollmentStatuses(input.items(), input.rawDimension())); + } continue; } @@ -992,27 +1003,66 @@ private boolean isProgramStatusDimension(String dimensionId) { || ColumnHeader.PROGRAM_STATUS.getItem().equalsIgnoreCase(dimensionId); } - private Set parseEnrollmentStatuses( - List statusItems, String dimensionString) { - if (statusItems == null || statusItems.isEmpty()) { - throwIllegalQueryEx(ErrorCode.E7222, dimensionString); + private boolean isAggregateRequest(EventDataQueryRequest request) { + return request.getEndpointAction() == RequestTypeAware.EndpointAction.AGGREGATE; + } + + /** + * Builds the {@link DimensionalObject} for {@code PROGRAM_STATUS}. The dimension identifier + * ({@code "programstatus"}) drives the response header; {@code dimensionName} ({@code + * "enrollmentstatus"}) is the underlying analytics column referenced by {@code quoteAlias} in the + * SQL builder. An empty item list is permitted on the aggregate dimension path — it means + * "include the column as a group-by without filtering rows." + */ + private DimensionalObject getProgramStatusDimension( + List rawItems, String dimensionString) { + return new BaseDimensionalObject( + ColumnHeader.PROGRAM_STATUS.getItem(), + DimensionType.PROGRAM_STATUS, + EventAnalyticsColumnName.ENROLLMENT_STATUS_COLUMN_NAME, + ColumnHeader.PROGRAM_STATUS.getName(), + parseStatusItems(rawItems, dimensionString)); + } + + private static Set parseEnrollmentStatuses( + List rawItems, String dimensionString) { + requireNonEmptyStatusFilter(rawItems, dimensionString); + + return rawItems.stream() + .map(raw -> parseEnrollmentStatus(raw, dimensionString)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private static List parseStatusItems( + List rawItems, String dimensionString) { + if (rawItems == null || rawItems.isEmpty()) { + return List.of(); } - Set statuses = new LinkedHashSet<>(); + return rawItems.stream() + .map(raw -> parseEnrollmentStatus(raw, dimensionString)) + .distinct() + .map(status -> new BaseDimensionalItemObject(status.name())) + .toList(); + } - for (String statusItem : statusItems) { - if (StringUtils.isBlank(statusItem)) { - throwIllegalQueryEx(ErrorCode.E7222, dimensionString); - } + private static EnrollmentStatus parseEnrollmentStatus(String raw, String dimensionString) { + if (StringUtils.isBlank(raw)) { + throwIllegalQueryEx(ErrorCode.E7222, dimensionString); + } - try { - statuses.add(EnrollmentStatus.valueOf(statusItem.trim().toUpperCase())); - } catch (IllegalArgumentException ex) { - throwIllegalQueryEx(ErrorCode.E7222, dimensionString); - } + try { + return EnrollmentStatus.valueOf(raw.trim().toUpperCase(java.util.Locale.ROOT)); + } catch (IllegalArgumentException ex) { + throwIllegalQueryEx(ErrorCode.E7222, dimensionString); + throw ex; // unreachable } + } - return statuses; + private static void requireNonEmptyStatusFilter(List rawItems, String dimensionString) { + if (rawItems == null || rawItems.isEmpty()) { + throwIllegalQueryEx(ErrorCode.E7222, dimensionString); + } } /** diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java index ebbdd867507d..68a5be5e78ab 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java @@ -195,6 +195,13 @@ public JdbcEnrollmentAnalyticsManager( this.timeFieldSqlRenderer = timeFieldSqlRenderer; } + private enum AggregateEnrollmentHeaderType { + VALUE, + ORG_UNIT, + PERIOD, + OTHER + } + @Override public void getEnrollments(EventQueryParams params, Grid grid, int maxLimit) { String sql; @@ -460,13 +467,22 @@ protected String getWhereClause(EventQueryParams params) { List dynamicDimensions = params.getDimensionsAndFilters( - Sets.newHashSet(DimensionType.CATEGORY, DimensionType.CATEGORY_OPTION_GROUP_SET)); + Sets.newHashSet( + DimensionType.CATEGORY, + DimensionType.CATEGORY_OPTION_GROUP_SET, + DimensionType.PROGRAM_STATUS)); for (DimensionalObject dim : dynamicDimensions) { // Skip attribute categories for enrollments if (dim.getDimensionType() == DimensionType.CATEGORY && isAttributeCategory(dim)) { continue; } + // PROGRAM_STATUS without items means group-by only — the column comes from the generic + // dimension SELECT loop; there is no IN-list to filter on. + DimensionType type = dim.getDimensionType(); + if (type == DimensionType.PROGRAM_STATUS && dim.getItems().isEmpty()) { + continue; + } String dimName = dim.getDimensionName(); String col = @@ -1277,15 +1293,8 @@ private String buildAggregatedEnrollmentQueryWithCte( // 3.1: Append the WITH clause if needed addCteClause(sb, cteContext); - // 3.2: Append SELECT columns in following order: - // 1) count(eb.enrollment) as value - // 2) org unit columns (orgColumns) - // 3) period columns (periodColumns) - // 4) header columns (headerColumns) - sb.addColumn("count(eb.enrollment) as value"); - addOrgUnitAggregateColumns(sb, params); - addPeriodAggregateColumns(params, sb, periodProjections); - addHeaderAggregateColumns(headers, params, cteContext, sb, periodProjections); + addAggregateEnrollmentSelectColumnsInHeaderOrder( + sb, headers, params, cteContext, periodProjections); // 3.3: Append the FROM clause (the main enrollment analytics table) sb.from(ENROLLMENT_AGGR_BASE, ENROLLMENT_AGGR_BASE_ALIAS); @@ -1303,6 +1312,75 @@ private String buildAggregatedEnrollmentQueryWithCte( return sb.build(); } + private void addAggregateEnrollmentSelectColumnsInHeaderOrder( + SelectBuilder sb, + List headers, + EventQueryParams params, + CteContext cteContext, + List periodProjections) { + if (headers.isEmpty()) { + addAggregatedColumns(sb); + addOrgUnitAggregateColumns(sb, params); + addPeriodAggregateColumns(params, sb, periodProjections); + addHeaderAggregateColumns(headers, params, cteContext, sb, periodProjections); + return; + } + + Set periodHeaderNames = + periodProjections.stream() + .map(PeriodProjection::responseKey) + .collect(Collectors.toCollection(LinkedHashSet::new)); + periodHeaderNames.add(PERIOD_DIM_ID); + periodHeaderNames.addAll(collectPeriodDateFieldKeys(params)); + + Set addedInfrastructureColumns = + EnumSet.noneOf(AggregateEnrollmentHeaderType.class); + + for (GridHeader header : headers) { + String headerName = header.getName(); + AggregateEnrollmentHeaderType headerType = + classifyAggregateEnrollmentHeader(headerName, periodHeaderNames); + + switch (headerType) { + case VALUE -> { + if (addedInfrastructureColumns.add(headerType)) { + addAggregatedColumns(sb); + } + } + case ORG_UNIT -> { + if (addedInfrastructureColumns.add(headerType)) { + addOrgUnitAggregateColumns(sb, params); + } + } + case PERIOD -> { + if (addedInfrastructureColumns.add(headerType)) { + addPeriodAggregateColumns(params, sb, periodProjections); + } + } + case OTHER -> + addHeaderAggregateColumns( + List.of(header), params, cteContext, sb, periodProjections); + } + } + } + + private AggregateEnrollmentHeaderType classifyAggregateEnrollmentHeader( + String headerName, Set periodHeaderNames) { + if (COL_VALUE.equalsIgnoreCase(headerName)) { + return AggregateEnrollmentHeaderType.VALUE; + } + + if (ORGUNIT_DIM_ID.equalsIgnoreCase(headerName)) { + return AggregateEnrollmentHeaderType.ORG_UNIT; + } + + if (periodHeaderNames.stream().anyMatch(headerName::equalsIgnoreCase)) { + return AggregateEnrollmentHeaderType.PERIOD; + } + + return AggregateEnrollmentHeaderType.OTHER; + } + private boolean useItemUidForTable(CteDefinition cteDefinition) { return cteDefinition.getCteType() == CteDefinition.CteType.PROGRAM_INDICATOR_ENROLLMENT || cteDefinition.getCteType() == CteDefinition.CteType.PROGRAM_INDICATOR_EVENT diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEventAnalyticsManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEventAnalyticsManager.java index 16afbee9662f..25c1ee0edd14 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEventAnalyticsManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEventAnalyticsManager.java @@ -634,9 +634,19 @@ protected String getWhereClause(EventQueryParams params) { List dynamicDimensions = params.getDimensionsAndFilters( - Set.of(DimensionType.CATEGORY, DimensionType.CATEGORY_OPTION_GROUP_SET)); + Set.of( + DimensionType.CATEGORY, + DimensionType.CATEGORY_OPTION_GROUP_SET, + DimensionType.PROGRAM_STATUS)); for (DimensionalObject dim : dynamicDimensions) { + // PROGRAM_STATUS without items means group-by only — the column comes from the generic + // dimension SELECT loop; there is no IN-list to filter on. + DimensionType type = dim.getDimensionType(); + if (type == DimensionType.PROGRAM_STATUS && dim.getItems().isEmpty()) { + continue; + } + String dimName = dim.getDimensionName(); String col = params.isPiDisagDimension(dimName) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/tracker/MetadataItemsHandler.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/tracker/MetadataItemsHandler.java index ddd67cfee90d..7b3f4239478e 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/tracker/MetadataItemsHandler.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/tracker/MetadataItemsHandler.java @@ -75,6 +75,7 @@ import org.hisp.dhis.analytics.util.AnalyticsUtils; import org.hisp.dhis.calendar.Calendar; import org.hisp.dhis.common.DimensionItemKeywords.Keyword; +import org.hisp.dhis.common.DimensionType; import org.hisp.dhis.common.DimensionalItemObject; import org.hisp.dhis.common.DimensionalObject; import org.hisp.dhis.common.DisplayProperty; @@ -595,16 +596,37 @@ private String getEnrollmentOuDisplayName() { private void addProgramStatusMetadata( Map metadataItemMap, EventQueryParams params) { - if (!params.hasEnrollmentStatuses()) { - return; - } + if (params.hasEnrollmentStatuses()) { + metadataItemMap.putIfAbsent( + PROGRAM_STATUS.getItem(), new MetadataItem(PROGRAM_STATUS.getName())); - metadataItemMap.putIfAbsent( - PROGRAM_STATUS.getItem(), new MetadataItem(PROGRAM_STATUS.getName())); + for (EnrollmentStatus status : params.getEnrollmentStatus()) { + metadataItemMap.put( + status.name(), new MetadataItem(getEnrollmentStatusDisplayName(status))); + } + } - for (EnrollmentStatus status : params.getEnrollmentStatus()) { + for (DimensionalObject dim : params.getDimensionsAndFilters()) { + if (dim.getDimensionType() != DimensionType.PROGRAM_STATUS) { + continue; + } metadataItemMap.putIfAbsent( - status.name(), new MetadataItem(getEnrollmentStatusDisplayName(status))); + PROGRAM_STATUS.getItem(), new MetadataItem(PROGRAM_STATUS.getName())); + for (DimensionalItemObject item : dim.getItems()) { + EnrollmentStatus status = parseEnrollmentStatus(item.getUid()); + if (status != null) { + metadataItemMap.put( + status.name(), new MetadataItem(getEnrollmentStatusDisplayName(status))); + } + } + } + } + + private static EnrollmentStatus parseEnrollmentStatus(String value) { + try { + return EnrollmentStatus.valueOf(value); + } catch (IllegalArgumentException | NullPointerException ex) { + return null; } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryServiceTest.java index da1c349d21e8..f0a50163b1be 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryServiceTest.java @@ -354,13 +354,14 @@ void getFromRequestStillNormalizesIsoPeriodForEnrollmentDate() { } @Test - void getFromRequestParsesProgramStatusDimensionForEventQuery() { + void getFromRequestParsesProgramStatusDimensionForEventQueryAsEnrollmentStatusFilter() { EventDataQueryRequest request = baseRequestBuilder(QUERY, EVENT).dimension(Set.of(Set.of("PROGRAM_STATUS:ACTIVE"))).build(); EventQueryParams params = subject.getFromRequest(request); assertEquals(Set.of(EnrollmentStatus.ACTIVE), params.getEnrollmentStatus()); + assertNoProgramStatusDimension(params); } @Test @@ -372,13 +373,11 @@ void getFromRequestParsesMultipleProgramStatusesFromDimensionForEventAggregate() EventQueryParams params = subject.getFromRequest(request); - assertEquals( - Set.of(EnrollmentStatus.ACTIVE, EnrollmentStatus.COMPLETED, EnrollmentStatus.CANCELLED), - params.getEnrollmentStatus()); + assertProgramStatusDimension(params, "ACTIVE", "COMPLETED", "CANCELLED"); } @Test - void getFromRequestMergesProgramStatusDimensionWithProgramStatusRequestParam() { + void getFromRequestMergesProgramStatusDimensionWithProgramStatusRequestParamForQuery() { EventDataQueryRequest request = baseRequestBuilder(QUERY, EVENT) .dimension(Set.of(Set.of("PROGRAM_STATUS:ACTIVE"))) @@ -389,6 +388,7 @@ void getFromRequestMergesProgramStatusDimensionWithProgramStatusRequestParam() { assertEquals( Set.of(EnrollmentStatus.ACTIVE, EnrollmentStatus.COMPLETED), params.getEnrollmentStatus()); + assertNoProgramStatusDimension(params); } @Test @@ -402,7 +402,30 @@ void getFromRequestRejectsInvalidProgramStatusDimensionValue() { } @Test - void getFromRequestParsesProgramStatusDimensionForEnrollmentEndpoint() { + void getFromRequestAcceptsProgramStatusDimensionWithoutValue() { + EventDataQueryRequest request = + baseRequestBuilder(AGGREGATE, ENROLLMENT) + .dimension(Set.of(Set.of("PROGRAM_STATUS"))) + .build(); + + EventQueryParams params = subject.getFromRequest(request); + + DimensionalObject dim = findStatusDimension(params, DimensionType.PROGRAM_STATUS); + assertEquals("programstatus", dim.getDimension()); + assertEquals(EventAnalyticsColumnName.ENROLLMENT_STATUS_COLUMN_NAME, dim.getDimensionName()); + assertTrue(dim.getItems().isEmpty(), "Dimension items should be empty for group-by only"); + } + + @Test + void getFromRequestRejectsProgramStatusFilterWithoutValue() { + EventDataQueryRequest request = + baseRequestBuilder(AGGREGATE, ENROLLMENT).filter(Set.of(Set.of("PROGRAM_STATUS"))).build(); + + assertThrows(IllegalQueryException.class, () -> subject.getFromRequest(request)); + } + + @Test + void getFromRequestParsesProgramStatusDimensionForEnrollmentQueryAsEnrollmentStatusFilter() { EventDataQueryRequest request = baseRequestBuilder(QUERY, ENROLLMENT) .dimension(Set.of(Set.of("PROGRAM_STATUS:ACTIVE"))) @@ -411,10 +434,11 @@ void getFromRequestParsesProgramStatusDimensionForEnrollmentEndpoint() { EventQueryParams params = subject.getFromRequest(request); assertEquals(Set.of(EnrollmentStatus.ACTIVE), params.getEnrollmentStatus()); + assertNoProgramStatusDimension(params); } @Test - void getFromRequestParsesProgramStatusFilterForEnrollmentEndpoint() { + void getFromRequestParsesProgramStatusFilterForEnrollmentQueryAsEnrollmentStatusFilter() { EventDataQueryRequest request = baseRequestBuilder(QUERY, ENROLLMENT) .filter(Set.of(Set.of("PROGRAM_STATUS:ACTIVE;COMPLETED"))) @@ -424,6 +448,36 @@ void getFromRequestParsesProgramStatusFilterForEnrollmentEndpoint() { assertEquals( Set.of(EnrollmentStatus.ACTIVE, EnrollmentStatus.COMPLETED), params.getEnrollmentStatus()); + assertNoProgramStatusFilter(params); + } + + private DimensionalObject findStatusDimension(EventQueryParams params, DimensionType type) { + return params.getDimensions().stream() + .filter(d -> d.getDimensionType() == type) + .findFirst() + .orElseThrow(() -> new AssertionError("No dimension of type " + type + " in params")); + } + + private void assertProgramStatusDimension(EventQueryParams params, String... expectedUids) { + DimensionalObject dim = findStatusDimension(params, DimensionType.PROGRAM_STATUS); + Set actual = + dim.getItems().stream() + .map(BaseDimensionalItemObject.class::cast) + .map(BaseDimensionalItemObject::getUid) + .collect(java.util.stream.Collectors.toSet()); + assertEquals(Set.of(expectedUids), actual); + } + + private void assertNoProgramStatusDimension(EventQueryParams params) { + assertTrue( + params.getDimensions().stream() + .noneMatch(d -> d.getDimensionType() == DimensionType.PROGRAM_STATUS)); + } + + private void assertNoProgramStatusFilter(EventQueryParams params) { + assertTrue( + params.getFilters().stream() + .noneMatch(d -> d.getDimensionType() == DimensionType.PROGRAM_STATUS)); } @Test diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/tracker/MetadataItemsHandlerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/tracker/MetadataItemsHandlerTest.java index e06687d5ef28..47366f0be018 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/tracker/MetadataItemsHandlerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/tracker/MetadataItemsHandlerTest.java @@ -1786,6 +1786,39 @@ void shouldIncludeProgramStatusItemsMetadata() { assertFalse(items.containsKey("CANCELLED")); } + @Test + @DisplayName("should use display names for generic programstatus dimension item metadata") + void shouldUseDisplayNamesForGenericProgramStatusDimensionItemsMetadata() { + Grid grid = new ListGrid(); + + BaseDimensionalObject programStatusDimension = + new BaseDimensionalObject( + "programstatus", + DimensionType.PROGRAM_STATUS, + "enrollmentstatus", + "Program status", + List.of(new BaseDimensionalItemObject("ACTIVE"))); + + EventQueryParams params = + new EventQueryParams.Builder() + .withProgram(programA) + .withSkipMeta(false) + .withEndpointAction(AGGREGATE) + .addDimension(programStatusDimension) + .build(); + + when(userService.getUserByUsername(anyString())).thenReturn(null); + + metadataItemsHandler.addMetadata(grid, params, List.of()); + + @SuppressWarnings("unchecked") + Map items = (Map) grid.getMetaData().get(ITEMS.getKey()); + + assertNotNull(items); + assertEquals("Program status", ((MetadataItem) items.get("programstatus")).getName()); + assertEquals("Active", ((MetadataItem) items.get("ACTIVE")).getName()); + } + @Test @DisplayName("should include programstatus dimensions and items for event query endpoint") void shouldIncludeProgramStatusMetadataForEventQueryEndpoint() { diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/enrollment/aggregate/EnrollmentsAggregate5AutoTest.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/enrollment/aggregate/EnrollmentsAggregate5AutoTest.java index e5efc89db5c8..b0e2c563c3c6 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/enrollment/aggregate/EnrollmentsAggregate5AutoTest.java +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/enrollment/aggregate/EnrollmentsAggregate5AutoTest.java @@ -104,6 +104,75 @@ public void queryAggregatedenrollmentsmacase5() throws JSONException { validateRow(response, List.of("178", "ImspTQPwCqd", "202212")); } + @Test + public void withProgramStatusNoFilter() throws JSONException { + // Read the 'expect.postgis' system property at runtime to adapt assertions. + boolean expectPostgis = isPostgres(); + + // Given + QueryParamsBuilder params = + new QueryParamsBuilder().add("dimension=ou:USER_ORGUNIT,PROGRAM_STATUS,pe:202211"); + + // When + ApiResponse response = actions.aggregate().get("IpHINAT79UW", JSON, JSON, params); + + // Then + // 1. Validate Response Structure (Counts, Headers, Height/Width) + // This helper checks basic counts and dimensions, adapting based on the runtime + // 'expectPostgis' flag. + validateResponseStructure( + response, + expectPostgis, + 1, + 4, + 4); // Pass runtime flag, row count, and expected header counts + + // 2. Extract Headers into a List of Maps for easy access by name + List> actualHeaders = + response.extractList("headers", Map.class).stream() + .map(obj -> (Map) obj) // Ensure correct type + .collect(Collectors.toList()); + + // 3. Assert metaData. + String expectedMetaData = + "{\"pager\":{\"page\":1,\"pageSize\":50,\"isLastPage\":true},\"items\":{\"202211\":{\"name\":\"November 2022\"},\"ImspTQPwCqd\":{\"name\":\"Sierra Leone\"},\"programstatus\":{\"name\":\"Program status\"},\"pe\":{},\"IpHINAT79UW\":{\"name\":\"Child Programme\"},\"ZzYYXq4fJie\":{\"name\":\"Baby Postnatal\"},\"USER_ORGUNIT\":{\"organisationUnits\":[\"ImspTQPwCqd\"]},\"ou\":{\"name\":\"Organisation unit\"},\"A03MvHHogjR\":{\"name\":\"Birth\"}},\"dimensions\":{\"pe\":[\"202211\"],\"programstatus\":[],\"ou\":[\"ImspTQPwCqd\"]}}"; + String actualMetaData = new JSONObject((Map) response.extract("metaData")).toString(); + assertEquals(expectedMetaData, actualMetaData, false); + + // 4. Validate Headers By Name (conditionally checking PostGIS headers). + validateHeaderPropertiesByName( + response, actualHeaders, "value", "Value", "NUMBER", "java.lang.Double", false, false); + validateHeaderPropertiesByName( + response, + actualHeaders, + "programstatus", + "Program status", + "TEXT", + "java.lang.String", + false, + true); + validateHeaderPropertiesByName( + response, + actualHeaders, + "ou", + "Organisation unit", + "TEXT", + "java.lang.String", + false, + true); + validateHeaderPropertiesByName( + response, actualHeaders, "pe", "Period", "TEXT", "java.lang.String", false, true); + + // rowContext not found or empty in the response, skipping assertions. + + // 7. Assert row existence by value (unsorted results - validates all columns). + // Validate row exists with values from original row index 0 + validateRowExists( + response, + actualHeaders, + Map.of("value", "933", "programstatus", "ACTIVE", "ou", "ImspTQPwCqd", "pe", "202211")); + } + @Test public void withStatusActiveOnly() throws JSONException { // Read the 'expect.postgis' system property at runtime to adapt assertions. @@ -115,7 +184,6 @@ public void withStatusActiveOnly() throws JSONException { // When ApiResponse response = actions.aggregate().get("IpHINAT79UW", JSON, JSON, params); - // Then // 1. Validate Response Structure (Counts, Headers, Height/Width) // This helper checks basic counts and dimensions, adapting based on the runtime diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/event/aggregate/EventsAggregate1AutoTest.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/event/aggregate/EventsAggregate1AutoTest.java index d01259fe0223..77009fe45f54 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/event/aggregate/EventsAggregate1AutoTest.java +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/event/aggregate/EventsAggregate1AutoTest.java @@ -33,11 +33,15 @@ import static org.hamcrest.Matchers.hasSize; import static org.hisp.dhis.analytics.ValidationHelper.assertJsonMetadata; import static org.hisp.dhis.analytics.ValidationHelper.validateHeader; +import static org.hisp.dhis.analytics.ValidationHelper.validateHeaderPropertiesByName; +import static org.hisp.dhis.analytics.ValidationHelper.validateResponseStructure; import static org.hisp.dhis.analytics.ValidationHelper.validateRow; +import static org.hisp.dhis.analytics.ValidationHelper.validateRowExists; import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.hisp.dhis.AnalyticsApiTest; import org.hisp.dhis.test.e2e.actions.analytics.AnalyticsEventActions; import org.hisp.dhis.test.e2e.dto.ApiResponse; @@ -253,7 +257,10 @@ public void queryActiveStatusCasesByGenderLast12Months() throws JSONException { } @Test - public void queryActiveStatusCasesByGenderLast12MonthsDim() { + public void queryActiveStatusCasesByGenderLast12MonthsDim() throws JSONException { + // Read the 'expect.postgis' system property at runtime to adapt assertions. + boolean expectPostgis = isPostgres(); + // Given QueryParamsBuilder params = new QueryParamsBuilder() @@ -264,194 +271,579 @@ public void queryActiveStatusCasesByGenderLast12MonthsDim() { .add("outputType=EVENT") .add( "dimension=PROGRAM_STATUS:ACTIVE,pe:LAST_12_MONTHS,cejWyOfXge6,ou:O6uvpzGd5pu;fdc6uOvgoji;lc3eMKXaEfw;jUb8gELQApl;PMa2VCrupOd;kJq2mPyFEHo;qhqAxPSTUXp;Vth0fbpFcsO;jmIPBj66vD6;TEQlaapDQoK;bL4ooGhyHRQ;eIQbndfxQMb;at6UHUQatSo") - // .add("programStatus=ACTIVE") .add("relativePeriodDate=2023-07-01"); // When ApiResponse response = actions.aggregate().get("IpHINAT79UW", JSON, JSON, params); // Then - response - .validate() - .statusCode(200) - .body("headers", hasSize(equalTo(5))) - .body("rows", hasSize(equalTo(156))) - .body("height", equalTo(156)) - .body("width", equalTo(5)) - .body("headerWidth", equalTo(5)); + // 1. Validate Response Structure (Counts, Headers, Height/Width) + // This helper checks basic counts and dimensions, adapting based on the runtime + // 'expectPostgis' flag. + validateResponseStructure( + response, + expectPostgis, + 156, + 5, + 5); // Pass runtime flag, row count, and expected header counts - // Assert metaData. + // 2. Extract Headers into a List of Maps for easy access by name + List> actualHeaders = + response.extractList("headers", Map.class).stream() + .map(obj -> (Map) obj) // Ensure correct type + .collect(Collectors.toList()); + + // 3. Assert metaData. String expectedMetaData = - "{\"items\":{\"jUb8gELQApl\":{\"name\":\"Kailahun\"},\"eIQbndfxQMb\":{\"name\":\"Tonkolili\"},\"ACTIVE\":{\"name\":\"Active\"},\"Vth0fbpFcsO\":{\"name\":\"Kono\"},\"202208\":{\"name\":\"August 2022\"},\"202209\":{\"name\":\"September 2022\"},\"O6uvpzGd5pu\":{\"name\":\"Bo\"},\"bL4ooGhyHRQ\":{\"name\":\"Pujehun\"},\"202305\":{\"name\":\"May 2023\"},\"kJq2mPyFEHo\":{\"name\":\"Kenema\"},\"202207\":{\"name\":\"July 2022\"},\"202306\":{\"name\":\"June 2023\"},\"202303\":{\"name\":\"March 2023\"},\"202304\":{\"name\":\"April 2023\"},\"202301\":{\"name\":\"January 2023\"},\"202302\":{\"name\":\"February 2023\"},\"LAST_12_MONTHS\":{\"name\":\"Last 12 months\"},\"202211\":{\"name\":\"November 2022\"},\"202212\":{\"name\":\"December 2022\"},\"202210\":{\"name\":\"October 2022\"},\"at6UHUQatSo\":{\"name\":\"Western Area\"},\"A03MvHHogjR\":{\"name\":\"Birth\"},\"Mnp3oXrpAbK\":{\"code\":\"Female\",\"name\":\"Female\"},\"TEQlaapDQoK\":{\"name\":\"Port Loko\"},\"rBvjJYbMCVx\":{\"code\":\"Male\",\"name\":\"Male\"},\"IpHINAT79UW\":{\"name\":\"Child Programme\"},\"PMa2VCrupOd\":{\"name\":\"Kambia\"},\"ou\":{\"name\":\"Organisation unit\"},\"fdc6uOvgoji\":{\"name\":\"Bombali\"},\"cejWyOfXge6\":{\"name\":\"Gender\"},\"programstatus\":{\"name\":\"Program status\"},\"pe\":{\"name\":\"Period\"},\"lc3eMKXaEfw\":{\"name\":\"Bonthe\"},\"qhqAxPSTUXp\":{\"name\":\"Koinadugu\"},\"jmIPBj66vD6\":{\"name\":\"Moyamba\"}},\"dimensions\":{\"programstatus\":[\"ACTIVE\"],\"pe\":[\"202207\",\"202208\",\"202209\",\"202210\",\"202211\",\"202212\",\"202301\",\"202302\",\"202303\",\"202304\",\"202305\",\"202306\"],\"ou\":[\"O6uvpzGd5pu\",\"fdc6uOvgoji\",\"lc3eMKXaEfw\",\"jUb8gELQApl\",\"PMa2VCrupOd\",\"kJq2mPyFEHo\",\"qhqAxPSTUXp\",\"Vth0fbpFcsO\",\"jmIPBj66vD6\",\"TEQlaapDQoK\",\"bL4ooGhyHRQ\",\"eIQbndfxQMb\",\"at6UHUQatSo\"],\"cejWyOfXge6\":[\"rBvjJYbMCVx\",\"Mnp3oXrpAbK\"]}}"; + "{\"items\":{\"ACTIVE\":{\"name\":\"Active\"},\"jUb8gELQApl\":{\"name\":\"Kailahun\"},\"eIQbndfxQMb\":{\"name\":\"Tonkolili\"},\"Vth0fbpFcsO\":{\"name\":\"Kono\"},\"202208\":{\"name\":\"August 2022\"},\"202209\":{\"name\":\"September 2022\"},\"O6uvpzGd5pu\":{\"name\":\"Bo\"},\"bL4ooGhyHRQ\":{\"name\":\"Pujehun\"},\"202305\":{\"name\":\"May 2023\"},\"kJq2mPyFEHo\":{\"name\":\"Kenema\"},\"202207\":{\"name\":\"July 2022\"},\"202306\":{\"name\":\"June 2023\"},\"202303\":{\"name\":\"March 2023\"},\"202304\":{\"name\":\"April 2023\"},\"202301\":{\"name\":\"January 2023\"},\"202302\":{\"name\":\"February 2023\"},\"LAST_12_MONTHS\":{\"name\":\"Last 12 months\"},\"202211\":{\"name\":\"November 2022\"},\"202212\":{\"name\":\"December 2022\"},\"202210\":{\"name\":\"October 2022\"},\"at6UHUQatSo\":{\"name\":\"Western Area\"},\"A03MvHHogjR\":{\"name\":\"Birth\"},\"Mnp3oXrpAbK\":{\"code\":\"Female\",\"name\":\"Female\"},\"TEQlaapDQoK\":{\"name\":\"Port Loko\"},\"rBvjJYbMCVx\":{\"code\":\"Male\",\"name\":\"Male\"},\"IpHINAT79UW\":{\"name\":\"Child Programme\"},\"PMa2VCrupOd\":{\"name\":\"Kambia\"},\"ou\":{\"name\":\"Organisation unit\"},\"fdc6uOvgoji\":{\"name\":\"Bombali\"},\"cejWyOfXge6\":{\"name\":\"Gender\"},\"programstatus\":{\"name\":\"Program status\"},\"pe\":{\"name\":\"Period\"},\"lc3eMKXaEfw\":{\"name\":\"Bonthe\"},\"qhqAxPSTUXp\":{\"name\":\"Koinadugu\"},\"jmIPBj66vD6\":{\"name\":\"Moyamba\"}},\"dimensions\":{\"programstatus\":[\"ACTIVE\"],\"pe\":[\"202207\",\"202208\",\"202209\",\"202210\",\"202211\",\"202212\",\"202301\",\"202302\",\"202303\",\"202304\",\"202305\",\"202306\"],\"ou\":[\"O6uvpzGd5pu\",\"fdc6uOvgoji\",\"lc3eMKXaEfw\",\"jUb8gELQApl\",\"PMa2VCrupOd\",\"kJq2mPyFEHo\",\"qhqAxPSTUXp\",\"Vth0fbpFcsO\",\"jmIPBj66vD6\",\"TEQlaapDQoK\",\"bL4ooGhyHRQ\",\"eIQbndfxQMb\",\"at6UHUQatSo\"],\"cejWyOfXge6\":[\"rBvjJYbMCVx\",\"Mnp3oXrpAbK\"]}}"; String actualMetaData = new JSONObject((Map) response.extract("metaData")).toString(); - // assertEquals(expectedMetaData, actualMetaData, false); - assertJsonMetadata(expectedMetaData, actualMetaData); + assertEquals(expectedMetaData, actualMetaData, false); - // Assert headers. - validateHeader(response, 0, "cejWyOfXge6", "Gender", "TEXT", "java.lang.String", false, true); - validateHeader(response, 1, "pe", "Period", "TEXT", "java.lang.String", false, true); - validateHeader(response, 2, "ou", "Organisation unit", "TEXT", "java.lang.String", false, true); - validateHeader( - response, 3, "programstatus", "Program status", "TEXT", "java.lang.String", false, true); - validateHeader(response, 4, "value", "Value", "NUMBER", "java.lang.Double", false, false); + // 4. Validate Headers By Name (conditionally checking PostGIS headers). + validateHeaderPropertiesByName( + response, actualHeaders, "cejWyOfXge6", "Gender", "TEXT", "java.lang.String", false, true); + validateHeaderPropertiesByName( + response, + actualHeaders, + "programstatus", + "Program status", + "TEXT", + "java.lang.String", + false, + true); + validateHeaderPropertiesByName( + response, actualHeaders, "pe", "Period", "TEXT", "java.lang.String", false, true); + validateHeaderPropertiesByName( + response, + actualHeaders, + "ou", + "Organisation unit", + "TEXT", + "java.lang.String", + false, + true); + validateHeaderPropertiesByName( + response, actualHeaders, "value", "Value", "NUMBER", "java.lang.Double", false, false); - // Assert rows. - validateRow(response, List.of("Male", "202210", "TEQlaapDQoK", "ACTIVE", "51")); - validateRow(response, List.of("Male", "202212", "at6UHUQatSo", "ACTIVE", "46")); - validateRow(response, List.of("Male", "202212", "kJq2mPyFEHo", "ACTIVE", "45")); - validateRow(response, List.of("Male", "202208", "TEQlaapDQoK", "ACTIVE", "45")); - validateRow(response, List.of("Male", "202207", "kJq2mPyFEHo", "ACTIVE", "43")); - validateRow(response, List.of("Male", "202210", "kJq2mPyFEHo", "ACTIVE", "41")); - validateRow(response, List.of("Male", "202212", "O6uvpzGd5pu", "ACTIVE", "39")); - validateRow(response, List.of("Male", "202209", "O6uvpzGd5pu", "ACTIVE", "37")); - validateRow(response, List.of("Male", "202207", "O6uvpzGd5pu", "ACTIVE", "37")); - validateRow(response, List.of("Male", "202211", "O6uvpzGd5pu", "ACTIVE", "37")); - validateRow(response, List.of("Male", "202211", "kJq2mPyFEHo", "ACTIVE", "36")); - validateRow(response, List.of("Male", "202208", "O6uvpzGd5pu", "ACTIVE", "35")); - validateRow(response, List.of("Male", "202208", "kJq2mPyFEHo", "ACTIVE", "35")); - validateRow(response, List.of("Male", "202207", "at6UHUQatSo", "ACTIVE", "35")); - validateRow(response, List.of("Male", "202207", "TEQlaapDQoK", "ACTIVE", "35")); - validateRow(response, List.of("Male", "202210", "eIQbndfxQMb", "ACTIVE", "35")); - validateRow(response, List.of("Male", "202211", "TEQlaapDQoK", "ACTIVE", "34")); - validateRow(response, List.of("Male", "202209", "kJq2mPyFEHo", "ACTIVE", "33")); - validateRow(response, List.of("Male", "202210", "O6uvpzGd5pu", "ACTIVE", "32")); - validateRow(response, List.of("Male", "202209", "eIQbndfxQMb", "ACTIVE", "31")); - validateRow(response, List.of("Male", "202209", "TEQlaapDQoK", "ACTIVE", "31")); - validateRow(response, List.of("Male", "202211", "eIQbndfxQMb", "ACTIVE", "30")); - validateRow(response, List.of("Male", "202210", "jmIPBj66vD6", "ACTIVE", "30")); - validateRow(response, List.of("Male", "202207", "Vth0fbpFcsO", "ACTIVE", "29")); - validateRow(response, List.of("Male", "202207", "bL4ooGhyHRQ", "ACTIVE", "28")); - validateRow(response, List.of("Male", "202208", "jmIPBj66vD6", "ACTIVE", "28")); - validateRow(response, List.of("Male", "202208", "eIQbndfxQMb", "ACTIVE", "28")); - validateRow(response, List.of("Male", "202208", "at6UHUQatSo", "ACTIVE", "28")); - validateRow(response, List.of("Male", "202209", "fdc6uOvgoji", "ACTIVE", "27")); - validateRow(response, List.of("Male", "202211", "jmIPBj66vD6", "ACTIVE", "27")); - validateRow(response, List.of("Male", "202212", "jUb8gELQApl", "ACTIVE", "27")); - validateRow(response, List.of("Male", "202209", "Vth0fbpFcsO", "ACTIVE", "27")); - validateRow(response, List.of("Male", "202208", "Vth0fbpFcsO", "ACTIVE", "27")); - validateRow(response, List.of("Male", "202210", "at6UHUQatSo", "ACTIVE", "26")); - validateRow(response, List.of("Male", "202208", "jUb8gELQApl", "ACTIVE", "26")); - validateRow(response, List.of("Male", "202208", "qhqAxPSTUXp", "ACTIVE", "26")); - validateRow(response, List.of("Male", "202211", "fdc6uOvgoji", "ACTIVE", "25")); - validateRow(response, List.of("Male", "202212", "eIQbndfxQMb", "ACTIVE", "25")); - validateRow(response, List.of("Male", "202210", "qhqAxPSTUXp", "ACTIVE", "25")); - validateRow(response, List.of("Male", "202211", "at6UHUQatSo", "ACTIVE", "25")); - validateRow(response, List.of("Male", "202207", "jmIPBj66vD6", "ACTIVE", "25")); - validateRow(response, List.of("Male", "202212", "fdc6uOvgoji", "ACTIVE", "24")); - validateRow(response, List.of("Male", "202210", "Vth0fbpFcsO", "ACTIVE", "24")); - validateRow(response, List.of("Male", "202212", "qhqAxPSTUXp", "ACTIVE", "24")); - validateRow(response, List.of("Male", "202207", "eIQbndfxQMb", "ACTIVE", "24")); - validateRow(response, List.of("Male", "202209", "jmIPBj66vD6", "ACTIVE", "24")); - validateRow(response, List.of("Male", "202209", "at6UHUQatSo", "ACTIVE", "24")); - validateRow(response, List.of("Male", "202207", "fdc6uOvgoji", "ACTIVE", "23")); - validateRow(response, List.of("Male", "202212", "PMa2VCrupOd", "ACTIVE", "23")); - validateRow(response, List.of("Male", "202210", "bL4ooGhyHRQ", "ACTIVE", "23")); - validateRow(response, List.of("Male", "202208", "fdc6uOvgoji", "ACTIVE", "23")); - validateRow(response, List.of("Male", "202212", "lc3eMKXaEfw", "ACTIVE", "23")); - validateRow(response, List.of("Male", "202211", "jUb8gELQApl", "ACTIVE", "22")); - validateRow(response, List.of("Male", "202212", "jmIPBj66vD6", "ACTIVE", "22")); - validateRow(response, List.of("Male", "202211", "qhqAxPSTUXp", "ACTIVE", "22")); - validateRow(response, List.of("Male", "202212", "TEQlaapDQoK", "ACTIVE", "20")); - validateRow(response, List.of("Male", "202211", "bL4ooGhyHRQ", "ACTIVE", "20")); - validateRow(response, List.of("Male", "202210", "fdc6uOvgoji", "ACTIVE", "20")); - validateRow(response, List.of("Male", "202208", "bL4ooGhyHRQ", "ACTIVE", "20")); - validateRow(response, List.of("Male", "202207", "jUb8gELQApl", "ACTIVE", "20")); - validateRow(response, List.of("Male", "202208", "PMa2VCrupOd", "ACTIVE", "19")); - validateRow(response, List.of("Male", "202211", "Vth0fbpFcsO", "ACTIVE", "19")); - validateRow(response, List.of("Male", "202212", "Vth0fbpFcsO", "ACTIVE", "19")); - validateRow(response, List.of("Male", "202209", "jUb8gELQApl", "ACTIVE", "19")); - validateRow(response, List.of("Male", "202209", "qhqAxPSTUXp", "ACTIVE", "18")); - validateRow(response, List.of("Male", "202209", "bL4ooGhyHRQ", "ACTIVE", "18")); - validateRow(response, List.of("Male", "202209", "PMa2VCrupOd", "ACTIVE", "18")); - validateRow(response, List.of("Male", "202212", "bL4ooGhyHRQ", "ACTIVE", "17")); - validateRow(response, List.of("Male", "202211", "lc3eMKXaEfw", "ACTIVE", "17")); - validateRow(response, List.of("Male", "202210", "PMa2VCrupOd", "ACTIVE", "17")); - validateRow(response, List.of("Male", "202207", "lc3eMKXaEfw", "ACTIVE", "16")); - validateRow(response, List.of("Male", "202208", "lc3eMKXaEfw", "ACTIVE", "15")); - validateRow(response, List.of("Male", "202210", "jUb8gELQApl", "ACTIVE", "14")); - validateRow(response, List.of("Male", "202210", "lc3eMKXaEfw", "ACTIVE", "13")); - validateRow(response, List.of("Male", "202207", "PMa2VCrupOd", "ACTIVE", "11")); - validateRow(response, List.of("Male", "202207", "qhqAxPSTUXp", "ACTIVE", "10")); - validateRow(response, List.of("Male", "202209", "lc3eMKXaEfw", "ACTIVE", "9")); - validateRow(response, List.of("Male", "202211", "PMa2VCrupOd", "ACTIVE", "9")); - validateRow(response, List.of("Female", "202209", "kJq2mPyFEHo", "ACTIVE", "47")); - validateRow(response, List.of("Female", "202212", "O6uvpzGd5pu", "ACTIVE", "46")); - validateRow(response, List.of("Female", "202211", "TEQlaapDQoK", "ACTIVE", "44")); - validateRow(response, List.of("Female", "202207", "TEQlaapDQoK", "ACTIVE", "39")); - validateRow(response, List.of("Female", "202208", "at6UHUQatSo", "ACTIVE", "39")); - validateRow(response, List.of("Female", "202211", "kJq2mPyFEHo", "ACTIVE", "38")); - validateRow(response, List.of("Female", "202208", "O6uvpzGd5pu", "ACTIVE", "37")); - validateRow(response, List.of("Female", "202208", "TEQlaapDQoK", "ACTIVE", "36")); - validateRow(response, List.of("Female", "202209", "TEQlaapDQoK", "ACTIVE", "35")); - validateRow(response, List.of("Female", "202210", "kJq2mPyFEHo", "ACTIVE", "35")); - validateRow(response, List.of("Female", "202208", "jmIPBj66vD6", "ACTIVE", "35")); - validateRow(response, List.of("Female", "202207", "O6uvpzGd5pu", "ACTIVE", "35")); - validateRow(response, List.of("Female", "202210", "at6UHUQatSo", "ACTIVE", "33")); - validateRow(response, List.of("Female", "202211", "O6uvpzGd5pu", "ACTIVE", "33")); - validateRow(response, List.of("Female", "202209", "Vth0fbpFcsO", "ACTIVE", "32")); - validateRow(response, List.of("Female", "202210", "fdc6uOvgoji", "ACTIVE", "32")); - validateRow(response, List.of("Female", "202208", "jUb8gELQApl", "ACTIVE", "32")); - validateRow(response, List.of("Female", "202210", "TEQlaapDQoK", "ACTIVE", "31")); - validateRow(response, List.of("Female", "202210", "O6uvpzGd5pu", "ACTIVE", "31")); - validateRow(response, List.of("Female", "202208", "kJq2mPyFEHo", "ACTIVE", "31")); - validateRow(response, List.of("Female", "202212", "fdc6uOvgoji", "ACTIVE", "30")); - validateRow(response, List.of("Female", "202211", "eIQbndfxQMb", "ACTIVE", "30")); - validateRow(response, List.of("Female", "202210", "jmIPBj66vD6", "ACTIVE", "30")); - validateRow(response, List.of("Female", "202212", "at6UHUQatSo", "ACTIVE", "30")); - validateRow(response, List.of("Female", "202207", "jmIPBj66vD6", "ACTIVE", "30")); - validateRow(response, List.of("Female", "202212", "TEQlaapDQoK", "ACTIVE", "29")); - validateRow(response, List.of("Female", "202209", "fdc6uOvgoji", "ACTIVE", "29")); - validateRow(response, List.of("Female", "202212", "kJq2mPyFEHo", "ACTIVE", "28")); - validateRow(response, List.of("Female", "202211", "fdc6uOvgoji", "ACTIVE", "28")); - validateRow(response, List.of("Female", "202210", "eIQbndfxQMb", "ACTIVE", "28")); - validateRow(response, List.of("Female", "202207", "kJq2mPyFEHo", "ACTIVE", "28")); - validateRow(response, List.of("Female", "202207", "fdc6uOvgoji", "ACTIVE", "27")); - validateRow(response, List.of("Female", "202207", "at6UHUQatSo", "ACTIVE", "27")); - validateRow(response, List.of("Female", "202208", "eIQbndfxQMb", "ACTIVE", "26")); - validateRow(response, List.of("Female", "202211", "qhqAxPSTUXp", "ACTIVE", "26")); - validateRow(response, List.of("Female", "202207", "Vth0fbpFcsO", "ACTIVE", "26")); - validateRow(response, List.of("Female", "202209", "O6uvpzGd5pu", "ACTIVE", "26")); - validateRow(response, List.of("Female", "202211", "at6UHUQatSo", "ACTIVE", "25")); - validateRow(response, List.of("Female", "202208", "bL4ooGhyHRQ", "ACTIVE", "25")); - validateRow(response, List.of("Female", "202212", "jmIPBj66vD6", "ACTIVE", "24")); - validateRow(response, List.of("Female", "202209", "qhqAxPSTUXp", "ACTIVE", "24")); - validateRow(response, List.of("Female", "202207", "jUb8gELQApl", "ACTIVE", "24")); - validateRow(response, List.of("Female", "202209", "eIQbndfxQMb", "ACTIVE", "24")); - validateRow(response, List.of("Female", "202207", "eIQbndfxQMb", "ACTIVE", "24")); - validateRow(response, List.of("Female", "202210", "Vth0fbpFcsO", "ACTIVE", "24")); - validateRow(response, List.of("Female", "202211", "jmIPBj66vD6", "ACTIVE", "24")); - validateRow(response, List.of("Female", "202211", "Vth0fbpFcsO", "ACTIVE", "23")); - validateRow(response, List.of("Female", "202209", "jmIPBj66vD6", "ACTIVE", "23")); - validateRow(response, List.of("Female", "202211", "bL4ooGhyHRQ", "ACTIVE", "23")); - validateRow(response, List.of("Female", "202208", "Vth0fbpFcsO", "ACTIVE", "23")); - validateRow(response, List.of("Female", "202208", "fdc6uOvgoji", "ACTIVE", "23")); - validateRow(response, List.of("Female", "202210", "PMa2VCrupOd", "ACTIVE", "22")); - validateRow(response, List.of("Female", "202209", "at6UHUQatSo", "ACTIVE", "20")); - validateRow(response, List.of("Female", "202210", "bL4ooGhyHRQ", "ACTIVE", "20")); - validateRow(response, List.of("Female", "202208", "PMa2VCrupOd", "ACTIVE", "20")); - validateRow(response, List.of("Female", "202210", "qhqAxPSTUXp", "ACTIVE", "19")); - validateRow(response, List.of("Female", "202207", "bL4ooGhyHRQ", "ACTIVE", "19")); - validateRow(response, List.of("Female", "202212", "Vth0fbpFcsO", "ACTIVE", "19")); - validateRow(response, List.of("Female", "202212", "jUb8gELQApl", "ACTIVE", "19")); - validateRow(response, List.of("Female", "202208", "qhqAxPSTUXp", "ACTIVE", "19")); - validateRow(response, List.of("Female", "202209", "bL4ooGhyHRQ", "ACTIVE", "18")); - validateRow(response, List.of("Female", "202209", "jUb8gELQApl", "ACTIVE", "18")); - validateRow(response, List.of("Female", "202210", "jUb8gELQApl", "ACTIVE", "18")); - validateRow(response, List.of("Female", "202211", "PMa2VCrupOd", "ACTIVE", "18")); - validateRow(response, List.of("Female", "202212", "PMa2VCrupOd", "ACTIVE", "18")); - validateRow(response, List.of("Female", "202212", "eIQbndfxQMb", "ACTIVE", "17")); - validateRow(response, List.of("Female", "202209", "lc3eMKXaEfw", "ACTIVE", "16")); - validateRow(response, List.of("Female", "202207", "qhqAxPSTUXp", "ACTIVE", "16")); - validateRow(response, List.of("Female", "202209", "PMa2VCrupOd", "ACTIVE", "15")); - validateRow(response, List.of("Female", "202212", "qhqAxPSTUXp", "ACTIVE", "15")); - validateRow(response, List.of("Female", "202210", "lc3eMKXaEfw", "ACTIVE", "15")); - validateRow(response, List.of("Female", "202212", "bL4ooGhyHRQ", "ACTIVE", "15")); - validateRow(response, List.of("Female", "202211", "lc3eMKXaEfw", "ACTIVE", "14")); - validateRow(response, List.of("Female", "202207", "lc3eMKXaEfw", "ACTIVE", "13")); - validateRow(response, List.of("Female", "202207", "PMa2VCrupOd", "ACTIVE", "13")); - validateRow(response, List.of("Female", "202212", "lc3eMKXaEfw", "ACTIVE", "12")); - validateRow(response, List.of("Female", "202208", "lc3eMKXaEfw", "ACTIVE", "12")); - validateRow(response, List.of("Female", "202211", "jUb8gELQApl", "ACTIVE", "9")); + // rowContext not found or empty in the response, skipping assertions. + + // 7. Assert row existence by value (unsorted results - validates all columns). + // Validate row exists with values from original row index 0 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Male", + "programstatus", + "ACTIVE", + "pe", + "202210", + "ou", + "TEQlaapDQoK", + "value", + "51")); + + // Validate row exists with values from original row index 12 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Male", + "programstatus", + "ACTIVE", + "pe", + "202208", + "ou", + "O6uvpzGd5pu", + "value", + "35")); + + // Validate row exists with values from original row index 24 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Male", + "programstatus", + "ACTIVE", + "pe", + "202208", + "ou", + "jmIPBj66vD6", + "value", + "28")); + + // Validate row exists with values from original row index 36 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Male", + "programstatus", + "ACTIVE", + "pe", + "202211", + "ou", + "fdc6uOvgoji", + "value", + "25")); + + // Validate row exists with values from original row index 48 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Male", + "programstatus", + "ACTIVE", + "pe", + "202212", + "ou", + "PMa2VCrupOd", + "value", + "23")); + + // Validate row exists with values from original row index 60 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Male", + "programstatus", + "ACTIVE", + "pe", + "202209", + "ou", + "jUb8gELQApl", + "value", + "19")); + + // Validate row exists with values from original row index 72 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Male", + "programstatus", + "ACTIVE", + "pe", + "202210", + "ou", + "jUb8gELQApl", + "value", + "14")); + + // Validate row exists with values from original row index 84 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Female", + "programstatus", + "ACTIVE", + "pe", + "202208", + "ou", + "O6uvpzGd5pu", + "value", + "37")); + + // Validate row exists with values from original row index 96 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Female", + "programstatus", + "ACTIVE", + "pe", + "202210", + "ou", + "O6uvpzGd5pu", + "value", + "31")); + + // Validate row exists with values from original row index 108 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Female", + "programstatus", + "ACTIVE", + "pe", + "202212", + "ou", + "kJq2mPyFEHo", + "value", + "28")); + + // Validate row exists with values from original row index 120 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Female", + "programstatus", + "ACTIVE", + "pe", + "202212", + "ou", + "jmIPBj66vD6", + "value", + "24")); + + // Validate row exists with values from original row index 132 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Female", + "programstatus", + "ACTIVE", + "pe", + "202208", + "ou", + "PMa2VCrupOd", + "value", + "20")); + + // Validate row exists with values from original row index 144 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Female", + "programstatus", + "ACTIVE", + "pe", + "202209", + "ou", + "lc3eMKXaEfw", + "value", + "16")); + + // Validate row exists with values from original row index 155 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Female", + "programstatus", + "ACTIVE", + "pe", + "202211", + "ou", + "jUb8gELQApl", + "value", + "9")); + } + + @Test + public void withProgramStatusNoFilter() throws JSONException { + // Read the 'expect.postgis' system property at runtime to adapt assertions. + boolean expectPostgis = isPostgres(); + + // Given + QueryParamsBuilder params = + new QueryParamsBuilder() + .add("stage=A03MvHHogjR") + .add("displayProperty=NAME") + .add("sortOrder=desc") + .add("totalPages=false") + .add( + "dimension=PROGRAM_STATUS,pe:LAST_12_MONTHS,cejWyOfXge6,ou:O6uvpzGd5pu;fdc6uOvgoji") + .add("relativePeriodDate=2023-07-01"); + + // When + ApiResponse response = actions.aggregate().get("IpHINAT79UW", JSON, JSON, params); + + // Then + // 1. Validate Response Structure (Counts, Headers, Height/Width) + // This helper checks basic counts and dimensions, adapting based on the runtime + // 'expectPostgis' flag. + validateResponseStructure( + response, + expectPostgis, + 24, + 5, + 5); // Pass runtime flag, row count, and expected header counts + + // 2. Extract Headers into a List of Maps for easy access by name + List> actualHeaders = + response.extractList("headers", Map.class).stream() + .map(obj -> (Map) obj) // Ensure correct type + .collect(Collectors.toList()); + + // 3. Assert metaData. + String expectedMetaData = + "{\"items\":{\"Mnp3oXrpAbK\":{\"code\":\"Female\",\"name\":\"Female\"},\"rBvjJYbMCVx\":{\"code\":\"Male\",\"name\":\"Male\"},\"IpHINAT79UW\":{\"name\":\"Child Programme\"},\"ou\":{\"name\":\"Organisation unit\"},\"202208\":{\"name\":\"August 2022\"},\"O6uvpzGd5pu\":{\"name\":\"Bo\"},\"202209\":{\"name\":\"September 2022\"},\"202305\":{\"name\":\"May 2023\"},\"202207\":{\"name\":\"July 2022\"},\"202306\":{\"name\":\"June 2023\"},\"202303\":{\"name\":\"March 2023\"},\"202304\":{\"name\":\"April 2023\"},\"202301\":{\"name\":\"January 2023\"},\"fdc6uOvgoji\":{\"name\":\"Bombali\"},\"202302\":{\"name\":\"February 2023\"},\"LAST_12_MONTHS\":{\"name\":\"Last 12 months\"},\"cejWyOfXge6\":{\"name\":\"Gender\"},\"202211\":{\"name\":\"November 2022\"},\"202212\":{\"name\":\"December 2022\"},\"programstatus\":{\"name\":\"Program status\"},\"202210\":{\"name\":\"October 2022\"},\"pe\":{\"name\":\"Period\"},\"A03MvHHogjR\":{\"name\":\"Birth\"}},\"dimensions\":{\"programstatus\":[],\"pe\":[\"202207\",\"202208\",\"202209\",\"202210\",\"202211\",\"202212\",\"202301\",\"202302\",\"202303\",\"202304\",\"202305\",\"202306\"],\"ou\":[\"O6uvpzGd5pu\",\"fdc6uOvgoji\"],\"cejWyOfXge6\":[\"rBvjJYbMCVx\",\"Mnp3oXrpAbK\"]}}"; + String actualMetaData = new JSONObject((Map) response.extract("metaData")).toString(); + assertEquals(expectedMetaData, actualMetaData, false); + + // 4. Validate Headers By Name (conditionally checking PostGIS headers). + validateHeaderPropertiesByName( + response, actualHeaders, "cejWyOfXge6", "Gender", "TEXT", "java.lang.String", false, true); + validateHeaderPropertiesByName( + response, + actualHeaders, + "ou", + "Organisation unit", + "TEXT", + "java.lang.String", + false, + true); + validateHeaderPropertiesByName( + response, + actualHeaders, + "programstatus", + "Program status", + "TEXT", + "java.lang.String", + false, + true); + validateHeaderPropertiesByName( + response, actualHeaders, "pe", "Period", "TEXT", "java.lang.String", false, true); + validateHeaderPropertiesByName( + response, actualHeaders, "value", "Value", "NUMBER", "java.lang.Double", false, false); + + // rowContext not found or empty in the response, skipping assertions. + + // 7. Assert row existence by value (unsorted results - validates all columns). + // Validate row exists with values from original row index 0 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Male", + "ou", + "O6uvpzGd5pu", + "programstatus", + "ACTIVE", + "pe", + "202212", + "value", + "39")); + + // Validate row exists with values from original row index 5 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Male", + "ou", + "O6uvpzGd5pu", + "programstatus", + "ACTIVE", + "pe", + "202210", + "value", + "32")); + + // Validate row exists with values from original row index 10 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Male", + "ou", + "fdc6uOvgoji", + "programstatus", + "ACTIVE", + "pe", + "202207", + "value", + "23")); + + // Validate row exists with values from original row index 15 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Female", + "ou", + "O6uvpzGd5pu", + "programstatus", + "ACTIVE", + "pe", + "202211", + "value", + "33")); + + // Validate row exists with values from original row index 20 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Female", + "ou", + "fdc6uOvgoji", + "programstatus", + "ACTIVE", + "pe", + "202211", + "value", + "28")); + + // Validate row exists with values from original row index 23 + validateRowExists( + response, + actualHeaders, + Map.of( + "cejWyOfXge6", + "Female", + "ou", + "fdc6uOvgoji", + "programstatus", + "ACTIVE", + "pe", + "202208", + "value", + "23")); + } + + @Test + public void withProgramStatusAndEnrollmentOu() throws JSONException { + // Read the 'expect.postgis' system property at runtime to adapt assertions. + boolean expectPostgis = isPostgres(); + + // Given + QueryParamsBuilder params = + new QueryParamsBuilder() + .add("stage=A03MvHHogjR") + .add("displayProperty=NAME") + .add("sortOrder=desc") + .add("totalPages=false") + .add("dimension=pe:LAST_12_MONTHS,PROGRAM_STATUS:ACTIVE,ENROLLMENT_OU:USER_ORGUNIT") + .add("relativePeriodDate=2023-07-01"); + + // When + ApiResponse response = actions.aggregate().get("IpHINAT79UW", JSON, JSON, params); + + // Then + // 1. Validate Response Structure (Counts, Headers, Height/Width) + // This helper checks basic counts and dimensions, adapting based on the runtime + // 'expectPostgis' flag. + validateResponseStructure( + response, + expectPostgis, + 6, + 4, + 4); // Pass runtime flag, row count, and expected header counts + + // 2. Extract Headers into a List of Maps for easy access by name + List> actualHeaders = + response.extractList("headers", Map.class).stream() + .map(obj -> (Map) obj) // Ensure correct type + .collect(Collectors.toList()); + + // 3. Assert metaData. + String expectedMetaData = + "{\"items\":{\"ACTIVE\":{\"name\":\"Active\"},\"IpHINAT79UW\":{\"name\":\"Child Programme\"},\"USER_ORGUNIT\":{\"organisationUnits\":[\"ImspTQPwCqd\"]},\"202208\":{\"name\":\"August 2022\"},\"202209\":{\"name\":\"September 2022\"},\"202305\":{\"name\":\"May 2023\"},\"202207\":{\"name\":\"July 2022\"},\"202306\":{\"name\":\"June 2023\"},\"202303\":{\"name\":\"March 2023\"},\"202304\":{\"name\":\"April 2023\"},\"202301\":{\"name\":\"January 2023\"},\"202302\":{\"name\":\"February 2023\"},\"LAST_12_MONTHS\":{\"name\":\"Last 12 months\"},\"202211\":{\"name\":\"November 2022\"},\"202212\":{\"name\":\"December 2022\"},\"ImspTQPwCqd\":{\"name\":\"Sierra Leone\"},\"programstatus\":{\"name\":\"Program status\"},\"202210\":{\"name\":\"October 2022\"},\"enrollmentou\":{\"name\":\"Enrollment org. unit\"},\"pe\":{\"name\":\"Period\"},\"A03MvHHogjR\":{\"name\":\"Birth\"}},\"dimensions\":{\"enrollmentou\":[\"ImspTQPwCqd\"],\"pe\":[\"202207\",\"202208\",\"202209\",\"202210\",\"202211\",\"202212\",\"202301\",\"202302\",\"202303\",\"202304\",\"202305\",\"202306\"],\"programstatus\":[\"ACTIVE\"]}}"; + String actualMetaData = new JSONObject((Map) response.extract("metaData")).toString(); + assertEquals(expectedMetaData, actualMetaData, false); + + // 4. Validate Headers By Name (conditionally checking PostGIS headers). + validateHeaderPropertiesByName( + response, + actualHeaders, + "programstatus", + "Program status", + "TEXT", + "java.lang.String", + false, + true); + validateHeaderPropertiesByName( + response, actualHeaders, "pe", "Period", "TEXT", "java.lang.String", false, true); + validateHeaderPropertiesByName( + response, + actualHeaders, + "enrollmentou", + "Enrollment org unit", + "TEXT", + "java.lang.String", + false, + true); + validateHeaderPropertiesByName( + response, actualHeaders, "value", "Value", "NUMBER", "java.lang.Double", false, false); + + // rowContext not found or empty in the response, skipping assertions. + + // 7. Assert row existence by value (unsorted results - validates all columns). + // Validate row exists with values from original row index 0 + validateRowExists( + response, + actualHeaders, + Map.of( + "programstatus", + "ACTIVE", + "pe", + "202208", + "enrollmentou", + "ImspTQPwCqd", + "value", + "713")); + + // Validate row exists with values from original row index 2 + validateRowExists( + response, + actualHeaders, + Map.of( + "programstatus", + "ACTIVE", + "pe", + "202211", + "enrollmentou", + "ImspTQPwCqd", + "value", + "658")); + + // Validate row exists with values from original row index 4 + validateRowExists( + response, + actualHeaders, + Map.of( + "programstatus", + "ACTIVE", + "pe", + "202212", + "enrollmentou", + "ImspTQPwCqd", + "value", + "656")); + + // Validate row exists with values from original row index 5 + validateRowExists( + response, + actualHeaders, + Map.of( + "programstatus", + "ACTIVE", + "pe", + "202209", + "enrollmentou", + "ImspTQPwCqd", + "value", + "643")); } @Test diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/enroll-aggregated.json b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/enroll-aggregated.json index 1e3de8552cb8..f62c74fb393d 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/enroll-aggregated.json +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/enroll-aggregated.json @@ -154,6 +154,13 @@ "min": 43 } }, + { + "name": "withProgramStatusNoFilter", + "query": "/api/analytics/enrollments/aggregate/IpHINAT79UW.json?dimension=ou:USER_ORGUNIT,PROGRAM_STATUS,pe:202211", + "version": { + "min": 43 + } + }, { "name": "stageAndUnfilteredEventStatus", "query": "/api/analytics/enrollments/aggregate/IpHINAT79UW.json?includeMetadataDetails=true&displayProperty=NAME&totalPages=false&pageSize=100&outputType=ENROLLMENT&page=1&dimension=A03MvHHogjR.EVENT_STATUS&dimension=A03MvHHogjR.EVENT_DATE:202205&dimension=A03MvHHogjR.wQLfBvPrXqq&dimension=A03MvHHogjR.ou:USER_ORGUNIT", diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/event-aggregated.json b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/event-aggregated.json index 973745b7cfdf..63ee13cb1cdd 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/event-aggregated.json +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/event-aggregated.json @@ -209,6 +209,27 @@ "version": { "min": 43 } + }, + { + "name": "withProgramStatusNoFilter", + "query": "/api/analytics/events/aggregate/IpHINAT79UW.json?stage=A03MvHHogjR&displayProperty=NAME&sortOrder=desc&totalPages=false&outputType=EVENT&dimension=PROGRAM_STATUS,pe:LAST_12_MONTHS,cejWyOfXge6,ou:O6uvpzGd5pu;fdc6uOvgoji&relativePeriodDate=2023-07-01", + "version": { + "min": 43 + } + }, + { + "name": "withProgramStatusAndEnrollmentOu", + "query": "/api/analytics/events/aggregate/IpHINAT79UW.json?stage=A03MvHHogjR&displayProperty=NAME&sortOrder=desc&totalPages=false&dimension=pe:LAST_12_MONTHS&dimension=PROGRAM_STATUS:ACTIVE&dimension=ENROLLMENT_OU:USER_ORGUNIT&relativePeriodDate=2023-07-01", + "version": { + "min": 43 + } + }, + { + "name": "queryActiveStatusCasesByGenderLast12MonthsDim", + "query": "/api/analytics/events/aggregate/IpHINAT79UW.json?stage=A03MvHHogjR&displayProperty=NAME&sortOrder=desc&totalPages=false&outputType=EVENT&dimension=PROGRAM_STATUS:ACTIVE,pe:LAST_12_MONTHS,cejWyOfXge6,ou:O6uvpzGd5pu;fdc6uOvgoji;lc3eMKXaEfw;jUb8gELQApl;PMa2VCrupOd;kJq2mPyFEHo;qhqAxPSTUXp;Vth0fbpFcsO;jmIPBj66vD6;TEQlaapDQoK;bL4ooGhyHRQ;eIQbndfxQMb;at6UHUQatSo&relativePeriodDate=2023-07-01", + "version": { + "min": 43 + } } ] } From 047df632ef5f6a79b698499fec8c4ad4be61c789 Mon Sep 17 00:00:00 2001 From: Luciano Fiandesio Date: Wed, 20 May 2026 14:38:04 +0200 Subject: [PATCH 2/2] format after conflict resolution --- .../analytics/event/data/JdbcEnrollmentAnalyticsManager.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java index 68a5be5e78ab..835e8ffdc010 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java @@ -1358,8 +1358,7 @@ private void addAggregateEnrollmentSelectColumnsInHeaderOrder( } } case OTHER -> - addHeaderAggregateColumns( - List.of(header), params, cteContext, sb, periodProjections); + addHeaderAggregateColumns(List.of(header), params, cteContext, sb, periodProjections); } } }