Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -992,27 +1003,66 @@ private boolean isProgramStatusDimension(String dimensionId) {
|| ColumnHeader.PROGRAM_STATUS.getItem().equalsIgnoreCase(dimensionId);
}

private Set<EnrollmentStatus> parseEnrollmentStatuses(
List<String> 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<String> 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<EnrollmentStatus> parseEnrollmentStatuses(
List<String> rawItems, String dimensionString) {
requireNonEmptyStatusFilter(rawItems, dimensionString);

return rawItems.stream()
.map(raw -> parseEnrollmentStatus(raw, dimensionString))
.collect(Collectors.toCollection(LinkedHashSet::new));
}

private static List<DimensionalItemObject> parseStatusItems(
List<String> rawItems, String dimensionString) {
if (rawItems == null || rawItems.isEmpty()) {
return List.of();
}

Set<EnrollmentStatus> statuses = new LinkedHashSet<>();
return rawItems.stream()
.map(raw -> parseEnrollmentStatus(raw, dimensionString))
.distinct()
.<DimensionalItemObject>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<String> rawItems, String dimensionString) {
if (rawItems == null || rawItems.isEmpty()) {
throwIllegalQueryEx(ErrorCode.E7222, dimensionString);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -460,13 +467,22 @@ protected String getWhereClause(EventQueryParams params) {

List<DimensionalObject> 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 =
Expand Down Expand Up @@ -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);
Expand All @@ -1303,6 +1312,74 @@ private String buildAggregatedEnrollmentQueryWithCte(
return sb.build();
}

private void addAggregateEnrollmentSelectColumnsInHeaderOrder(
SelectBuilder sb,
List<GridHeader> headers,
EventQueryParams params,
CteContext cteContext,
List<PeriodProjection> periodProjections) {
if (headers.isEmpty()) {
addAggregatedColumns(sb);
addOrgUnitAggregateColumns(sb, params);
addPeriodAggregateColumns(params, sb, periodProjections);
addHeaderAggregateColumns(headers, params, cteContext, sb, periodProjections);
return;
}

Set<String> periodHeaderNames =
periodProjections.stream()
.map(PeriodProjection::responseKey)
.collect(Collectors.toCollection(LinkedHashSet::new));
periodHeaderNames.add(PERIOD_DIM_ID);
periodHeaderNames.addAll(collectPeriodDateFieldKeys(params));

Set<AggregateEnrollmentHeaderType> 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<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -634,9 +634,19 @@ protected String getWhereClause(EventQueryParams params) {

List<DimensionalObject> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -595,16 +596,37 @@ private String getEnrollmentOuDisplayName() {

private void addProgramStatusMetadata(
Map<String, MetadataItem> 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;
}
}

Expand Down
Loading
Loading