diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/analytics/Aggregation.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/analytics/Aggregation.java new file mode 100644 index 000000000000..6a17f7a9554c --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/analytics/Aggregation.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.analytics; + +public enum Aggregation { + AGGREGATED, + DISAGGREGATED +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseAnalyticalObject.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseAnalyticalObject.java index 7478bef21c40..4b8d7d6c3b3c 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseAnalyticalObject.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseAnalyticalObject.java @@ -121,6 +121,15 @@ @JacksonXmlRootElement(localName = "analyticalObject", namespace = DxfNamespaces.DXF_2_0) public abstract class BaseAnalyticalObject extends BaseNameableObject implements AnalyticalObject { + private static final BaseDimensionalItemObject USER_OU_ITEM_OBJ = + buildDimItemObj(KEY_USER_ORGUNIT, "User organisation unit"); + + private static final BaseDimensionalItemObject USER_OU_CHILDREN_ITEM_OBJ = + buildDimItemObj(KEY_USER_ORGUNIT_CHILDREN, "User organisation unit children"); + + private static final BaseDimensionalItemObject USER_OU_GRANDCHILDREN_ITEM_OBJ = + buildDimItemObj(KEY_USER_ORGUNIT_GRANDCHILDREN, "User organisation unit grand children"); + public static final String NOT_A_VALID_DIMENSION = "Not a valid dimension: %s"; /** Line and axis labels. */ @@ -320,6 +329,19 @@ public abstract void init( List organisationUnitsInGroups, I18nFormat format); + /** + * Returns the dimensional item object for the given dimension and name. + * + * @param uid the dimension uid. + * @param name the dimension name. + * @return the DimensionalObject. + */ + private static BaseDimensionalItemObject buildDimItemObj(String uid, String name) { + BaseDimensionalItemObject itemObj = new BaseDimensionalItemObject(uid); + itemObj.setName(name); + return itemObj; + } + @Override public abstract void populateAnalyticalProperties(); @@ -700,15 +722,15 @@ protected Optional getDimensionalObject(String dimension) { ouList.addAll(transientOrganisationUnits); if (userOrganisationUnit) { - ouList.add(new BaseDimensionalItemObject(KEY_USER_ORGUNIT)); + ouList.add(USER_OU_ITEM_OBJ); } if (userOrganisationUnitChildren) { - ouList.add(new BaseDimensionalItemObject(KEY_USER_ORGUNIT_CHILDREN)); + ouList.add(USER_OU_CHILDREN_ITEM_OBJ); } if (userOrganisationUnitGrandChildren) { - ouList.add(new BaseDimensionalItemObject(KEY_USER_ORGUNIT_GRANDCHILDREN)); + ouList.add(USER_OU_GRANDCHILDREN_ITEM_OBJ); } if (organisationUnitLevels != null && !organisationUnitLevels.isEmpty()) { diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseDimensionalItemObject.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseDimensionalItemObject.java index 494ec44d4d22..aefcea136b2a 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseDimensionalItemObject.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseDimensionalItemObject.java @@ -51,6 +51,9 @@ public class BaseDimensionalItemObject extends BaseNameableObject implements Dim /** The aggregation type for this dimension. */ protected AggregationType aggregationType; + /** The client's OptionSet for this dimension item. */ + protected OptionSetItem optionSetItem; + /** Query modifiers for this object. */ protected transient QueryModifiers queryMods; @@ -92,6 +95,17 @@ public AggregationType getAggregationType() { : aggregationType; } + @Override + @JsonProperty + @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) + public OptionSetItem getOptionSetItem() { + return optionSetItem; + } + + public void setOptionSetItem(OptionSetItem optionSetItem) { + this.optionSetItem = optionSetItem; + } + // ------------------------------------------------------------------------- // DimensionalItemObject // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DataDimensionItem.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DataDimensionItem.java index 623259605851..e0d189e0ad5f 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DataDimensionItem.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DataDimensionItem.java @@ -27,16 +27,22 @@ */ package org.hisp.dhis.common; +import static org.hisp.dhis.analytics.Aggregation.AGGREGATED; + import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import com.google.common.collect.Lists; +import java.io.Serializable; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import org.hisp.dhis.analytics.Aggregation; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataelement.DataElementOperand; import org.hisp.dhis.expressiondimensionitem.ExpressionDimensionItem; @@ -104,6 +110,40 @@ public class DataDimensionItem { private SubexpressionDimensionItem subexpressionDimensionItem; + private Attributes attributes; + + @NoArgsConstructor + @AllArgsConstructor + public static class Attributes implements Serializable { + /** The option item for this dimension item. * */ + private OptionSetItem optionSetItem; + + @JsonProperty + @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) + public OptionSetItem getOptionSetItem() { + return optionSetItem; + } + + /** + * This method ensure that existing persisted items will return default values, case the current + * {@link OptionSetItem} is null or does not have an {@link Aggregation} defined. + * + * @return the correct version of an {@link OptionSetItem}. + */ + public OptionSetItem getOptionSetItemOrDefault() { + if (optionSetItem != null) { + return new OptionSetItem( + optionSetItem.getOptions(), optionSetItem.getAggregationOrDefault()); + } + + return new OptionSetItem(Set.of(), AGGREGATED); + } + + public void setOptionSetItem(OptionSetItem optionSetItem) { + this.optionSetItem = optionSetItem; + } + } + // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- @@ -112,7 +152,6 @@ public DataDimensionItem() {} public static List createWithDependencies( DimensionalItemObject object, List items) { - if (DataElement.class.isAssignableFrom(object.getClass())) { DataDimensionItem dimension = new DataDimensionItem(); DataElement dataElement = (DataElement) object; @@ -187,6 +226,7 @@ public DimensionalItemObject getDimensionalItemObject() { if (indicator != null) { return indicator; } else if (dataElement != null) { + loadAttributes(dataElement); return dataElement; } else if (dataElementOperand != null) { return dataElementOperand; @@ -195,18 +235,34 @@ public DimensionalItemObject getDimensionalItemObject() { } else if (programIndicator != null) { return programIndicator; } else if (programDataElement != null) { + loadAttributes(programDataElement); return programDataElement; } else if (programAttribute != null) { + loadAttributes(programAttribute); return programAttribute; } else if (expressionDimensionItem != null) { return expressionDimensionItem; } else if (subexpressionDimensionItem != null) { - return expressionDimensionItem; + return subexpressionDimensionItem; } return null; } + /** + * Simply loads the internal attributes into the given item object. Some objects, when null, will + * be loaded with their respective defaults. + * + * @param itemObject the {@link BaseDimensionalItemObject}. + */ + private void loadAttributes(BaseDimensionalItemObject itemObject) { + if (attributes == null) { + attributes = new Attributes(); + } + + itemObject.setOptionSetItem(attributes.getOptionSetItemOrDefault()); + } + @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public DataDimensionItemType getDataDimensionItemType() { @@ -287,6 +343,14 @@ public void setId(int id) { this.id = id; } + public Attributes getAttributes() { + return attributes; + } + + public void setAttributes(Attributes attributes) { + this.attributes = attributes; + } + @JsonProperty @JsonSerialize(as = BaseNameableObject.class) @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) @@ -298,9 +362,6 @@ public void setIndicator(Indicator indicator) { this.indicator = indicator; } - @JsonProperty - @JsonSerialize(as = BaseNameableObject.class) - @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public DataElement getDataElement() { return dataElement; } 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 d286175a1571..a98438b87b68 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 @@ -46,6 +46,7 @@ public enum DimensionType { ORGANISATION_UNIT_GROUP, CATEGORY, OPTION_GROUP_SET, + OPTION_SET, VALIDATION_RULE, STATIC, ORGANISATION_UNIT_LEVEL; diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalItemObject.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalItemObject.java index c12c4e954df7..ed5448291384 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalItemObject.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalItemObject.java @@ -54,6 +54,9 @@ public interface DimensionalItemObject extends NameableObject { /** Gets the legend sets. */ List getLegendSets(); + /** Option set saved for client usage. */ + OptionSetItem getOptionSetItem(); + /** * Gets the first legend set in the legend set list. This field is derived from {@link * DimensionalObject#getLegendSet()} and is not persisted. diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/OptionSetItem.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/OptionSetItem.java new file mode 100644 index 000000000000..166085a45c8a --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/OptionSetItem.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.common; + +import static org.hisp.dhis.analytics.Aggregation.AGGREGATED; +import static org.hisp.dhis.common.DxfNamespaces.DXF_2_0; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import java.io.Serializable; +import java.util.LinkedHashSet; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.hisp.dhis.analytics.Aggregation; + +/** Encapsulates {@link org.hisp.dhis.option.Option}s uids and the {@link Aggregation} type. */ +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +@JacksonXmlRootElement(localName = "optionSetItem", namespace = DXF_2_0) +public class OptionSetItem implements Serializable { + /** The uids of the options. */ + private Set options = new LinkedHashSet<>(); + + /** The aggregation for this option item. */ + private Aggregation aggregation; + + @JsonProperty + @JacksonXmlProperty(namespace = DXF_2_0) + public Set getOptions() { + return options; + } + + public void setOptions(Set options) { + this.options = options; + } + + @JsonProperty + @JacksonXmlProperty(namespace = DXF_2_0) + public Aggregation getAggregation() { + return aggregation; + } + + public void setAggregation(Aggregation aggregation) { + this.aggregation = aggregation; + } + + /** + * Returns the current {@link Aggregation} or default. + * + * @return the respective {@link Aggregation} object. + */ + public Aggregation getAggregationOrDefault() { + if (aggregation == null) { + return AGGREGATED; + } + + return aggregation; + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultDataQueryService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultDataQueryService.java index 6ed9ff5c5838..b5511f5ebad6 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultDataQueryService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultDataQueryService.java @@ -27,7 +27,6 @@ */ package org.hisp.dhis.analytics.data; -import static java.util.stream.Collectors.toList; import static org.apache.commons.collections4.CollectionUtils.addIgnoreNull; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.apache.commons.lang3.StringUtils.isNotEmpty; @@ -64,6 +63,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.hisp.dhis.analytics.AnalyticsSecurityManager; import org.hisp.dhis.analytics.DataQueryParams; @@ -290,7 +290,7 @@ public List getUserOrgUnits(DataQueryParams params, String use getItemsFromParam(userOrgUnit).stream() .map(ou -> idObjectManager.get(OrganisationUnit.class, ou)) .filter(Objects::nonNull) - .collect(toList())); + .toList()); } else if (currentUser != null && params != null && params.getUserOrgUnitType() != null) { switch (params.getUserOrgUnitType()) { case DATA_CAPTURE: @@ -441,6 +441,6 @@ private List getCategoryOptionComboList( return items.stream() .map(item -> idObjectManager.getObject(CategoryOptionCombo.class, inputIdScheme, item)) .filter(Objects::nonNull) - .collect(toList()); + .collect(Collectors.toList()); } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManager.java index e33ad0454aac..7054896f8821 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManager.java @@ -253,14 +253,14 @@ private String getInputSql(Program program) { from ${programownershiphistory} h \ where h.programid = ${programId} \ and h.organisationunitid is not null \ - union \ + union distinct \ select o.trackedentityid, '${trackedEntityOwnTableId}' as startdate, null as enddate, o.organisationunitid \ from ${trackedentityprogramowner} o \ where o.programid = ${programId} \ - and exists (\ - select 1 from ${programownershiphistory} p \ - where o.trackedentityid = p.trackedentityid \ - and p.programid = ${programId} \ + and o.trackedentityid in (\ + select distinct p.trackedentityid \ + from ${programownershiphistory} p \ + where p.programid = ${programId} \ and p.organisationunitid is not null)) a \ inner join ${trackedentity} te on a.trackedentityid = te.trackedentityid \ inner join ${organisationunit} ou on a.organisationunitid = ou.organisationunitid \ diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java index 62de96dba85d..d18d0fb6353e 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java @@ -280,15 +280,15 @@ select te.uid,a.startdate,a.enddate,ou.uid from (\ from "programownershiphistory" h \ where h.programid = 0 \ and h.organisationunitid is not null \ - union \ + union distinct \ select o.trackedentityid, '2002-02-02' as startdate, null as enddate, o.organisationunitid \ from "trackedentityprogramowner" o \ where o.programid = 0 \ - and exists (select 1 from "programownershiphistory" p \ - where o.trackedentityid = p.trackedentityid \ - and p.programid = 0 \ - and p.organisationunitid is not null)\ - ) a \ + and o.trackedentityid in (\ + select distinct p.trackedentityid \ + from "programownershiphistory" p \ + where p.programid = 0 \ + and p.organisationunitid is not null)) a \ inner join "trackedentity" te on a.trackedentityid = te.trackedentityid \ inner join "organisationunit" ou on a.organisationunitid = ou.organisationunitid \ left join analytics_rs_orgunitstructure ous on a.organisationunitid = ous.organisationunitid \ diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dimension/DefaultDimensionService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dimension/DefaultDimensionService.java index b7f120d82a13..8d37336eda60 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dimension/DefaultDimensionService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dimension/DefaultDimensionService.java @@ -41,6 +41,7 @@ import static org.hisp.dhis.common.DimensionType.PROGRAM_DATA_ELEMENT; import static org.hisp.dhis.common.DimensionType.PROGRAM_INDICATOR; import static org.hisp.dhis.common.DimensionalObjectUtils.COMPOSITE_DIM_OBJECT_ESCAPED_SEP; +import static org.hisp.dhis.common.IdScheme.UID; import static org.hisp.dhis.common.IdentifiableObjectUtils.getUids; import static org.hisp.dhis.commons.util.TextUtils.splitSafe; import static org.hisp.dhis.eventvisualization.Attribute.COLUMN; @@ -74,6 +75,7 @@ import org.hisp.dhis.common.BaseDimensionalObject; import org.hisp.dhis.common.CodeGenerator; import org.hisp.dhis.common.DataDimensionItem; +import org.hisp.dhis.common.DataDimensionItem.Attributes; import org.hisp.dhis.common.DimensionService; import org.hisp.dhis.common.DimensionType; import org.hisp.dhis.common.DimensionalItemId; @@ -338,7 +340,7 @@ public DimensionalObject getDimensionalObjectCopy(String uid, boolean filterCanR @Override @Transactional(readOnly = true) public DimensionalItemObject getDataDimensionalItemObject(String dimensionItem) { - return getDataDimensionalItemObject(IdScheme.UID, dimensionItem); + return getDataDimensionalItemObject(UID, dimensionItem); } @Override @@ -532,13 +534,16 @@ private void mergeDimensionalObjects( List uids = getUids(items); if (DATA_X.equals(type)) { - for (String uid : uids) { - DimensionalItemObject dimItemObject = getDataDimensionalItemObject(IdScheme.UID, uid); + for (DimensionalItemObject item : items) { + DimensionalItemObject dimItemObject = getDataDimensionalItemObject(UID, item.getUid()); if (dimItemObject != null) { - DataDimensionItem item = DataDimensionItem.create(dimItemObject); + DataDimensionItem dataItem = DataDimensionItem.create(dimItemObject); - object.getDataDimensionItems().add(item); + // Adds attributes to the current data item object. + dataItem.setAttributes(new Attributes(item.getOptionSetItem())); + + object.getDataDimensionItems().add(dataItem); } } } else if (PERIOD.equals(type)) { @@ -560,7 +565,6 @@ private void mergeDimensionalObjects( } object.setRawRelativePeriods(new ArrayList<>(relativePeriods)); - // object.setRelatives(new RelativePeriods().setRelativePeriodsFromEnums(enums)); object.setPeriods(periodService.reloadPeriods(new ArrayList<>(periods))); } else if (ORGANISATION_UNIT.equals(type)) { for (String ou : uids) { diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/common.hibernate/DataDimensionItem.hbm.xml b/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/common.hibernate/DataDimensionItem.hbm.xml index 8a0f55132cfd..b138afe6a9e5 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/common.hibernate/DataDimensionItem.hbm.xml +++ b/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/common.hibernate/DataDimensionItem.hbm.xml @@ -21,7 +21,7 @@ - + @@ -43,7 +43,7 @@ column="programindicatorid" foreign-key="fk_datadimensionitem_programindicatorid" /> - - + column="expressiondimensionitemid" foreign-key="fk_datadimensionitem_expressiondimensionitemid" /> + + + diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/program/ProgramSqlGeneratorFunctionsTest.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/program/ProgramSqlGeneratorFunctionsTest.java index 008625291277..303ce975fafb 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/program/ProgramSqlGeneratorFunctionsTest.java +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/program/ProgramSqlGeneratorFunctionsTest.java @@ -212,7 +212,7 @@ void testConditionWithBooleanAsBoolean() { sql, is( "case when (coalesce(" - + "case when ax.\"ps\" = 'ProgrmStagA' then \"DataElmentE\" else null end::numeric!=0,false)) " + + "case when ax.\"ps\" = 'ProgrmStagA' then \"DataElmentE\" else null end::numeric != 0,false)) " + "then 10 + 5 else 3 * 2 end")); } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/HibernateEventChangeLogStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/HibernateEventChangeLogStore.java index 7f762d67f06c..53766ba38842 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/HibernateEventChangeLogStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/HibernateEventChangeLogStore.java @@ -57,23 +57,21 @@ public class HibernateEventChangeLogStore { private static final String COLUMN_CHANGELOG_USER = "ecl.createdByUsername"; private static final String COLUMN_CHANGELOG_DATA_ELEMENT = "d.uid"; private static final String COLUMN_CHANGELOG_FIELD = "ecl.eventField"; - + private static final String ORDER_CHANGE_EXPRESSION = + "CONCAT(COALESCE(d.formName, ''), COALESCE(" + COLUMN_CHANGELOG_FIELD + ", ''))"; private static final String DEFAULT_ORDER = COLUMN_CHANGELOG_CREATED + " " + SortDirection.DESC.getValue(); /** * Event change logs can be ordered by given fields which correspond to fields on {@link - * EventChangeLog}. Maps fields to DB columns. The order implementation for change logs is - * different from other tracker exporters {@link EventChangeLog} is the view which is already - * returned from the service/store. Tracker exporter services return a representation we have to - * map to a view model. This mapping is not necessary for change logs. + * EventChangeLog}. Maps fields to DB columns, except when sorting by 'change'. In that case we + * need to sort by concatenation, to treat the dataElement and eventField as a single entity. */ private static final Map ORDERABLE_FIELDS = Map.ofEntries( entry("createdAt", COLUMN_CHANGELOG_CREATED), entry("username", COLUMN_CHANGELOG_USER), - entry("dataElement", COLUMN_CHANGELOG_DATA_ELEMENT), - entry("field", COLUMN_CHANGELOG_FIELD)); + entry("change", ORDER_CHANGE_EXPRESSION)); private static final Map>, String> FILTERABLE_FIELDS = Map.ofEntries( diff --git a/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_27__Add_optionsets_column_to_datadimensionitem.sql b/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_27__Add_optionsets_column_to_datadimensionitem.sql new file mode 100644 index 000000000000..8b7db765fb80 --- /dev/null +++ b/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_27__Add_optionsets_column_to_datadimensionitem.sql @@ -0,0 +1,5 @@ + +-- DHIS2-18370 - Visualization API: Support saving and loading "optionSet" in "items" + +-- Add new json column for array of option sets. +alter table datadimensionitem add column if not exists optionsetitem jsonb; diff --git a/dhis-2/dhis-support/dhis-support-hibernate/src/main/resources/org/hisp/dhis/usertype/UserTypes.hbm.xml b/dhis-2/dhis-support/dhis-support-hibernate/src/main/resources/org/hisp/dhis/usertype/UserTypes.hbm.xml index 5ffc6ca8c9fb..641cdd8628b1 100644 --- a/dhis-2/dhis-support/dhis-support-hibernate/src/main/resources/org/hisp/dhis/usertype/UserTypes.hbm.xml +++ b/dhis-2/dhis-support/dhis-support-hibernate/src/main/resources/org/hisp/dhis/usertype/UserTypes.hbm.xml @@ -33,6 +33,10 @@ org.hisp.dhis.visualization.VisualizationFontStyle + + org.hisp.dhis.common.OptionSetItem + + org.hisp.dhis.visualization.AxisV2 diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java index a94a0b299df8..b4677fd81538 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java @@ -236,21 +236,22 @@ public String jsonExtract(String json, String key, String property) { @Override public String cast(String column, DataType dataType) { return switch (dataType) { - case NUMERIC -> String.format("toDecimal64(%s, 8)", column); // 8 decimal places precision + case NUMERIC -> String.format("toFloat64(%s)", column); case BOOLEAN -> String.format("toUInt8(%s) != 0", column); // ClickHouse uses UInt8 for boolean case TEXT -> String.format("toString(%s)", column); }; } - @Override - public String age(String endDate, String startDate) { - throw new UnsupportedOperationException(); - } - @Override public String dateDifference(String startDate, String endDate, DateUnit dateUnit) { - throw new UnsupportedOperationException(); + return switch (dateUnit) { + case DAYS -> String.format("dateDiff('day', %s, %s)", startDate, endDate); + case MINUTES -> String.format("dateDiff('minute', %s, %s)", startDate, endDate); + case MONTHS -> String.format("dateDiff('month', %s, %s)", startDate, endDate); + case YEARS -> String.format("dateDiff('year', %s, %s)", startDate, endDate); + case WEEKS -> String.format("dateDiff('week', %s, %s)", startDate, endDate); + }; } @Override diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java index 5797a81b8502..1b01c4d1a5e9 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java @@ -241,12 +241,6 @@ public String cast(String column, DataType dataType) { }; } - @Override - public String age(String endDate, String startDate) { - return String.format( - "TIMESTAMPDIFF(YEAR, cast(%s as date), cast(%s as date))", startDate, endDate); - } - @Override public String dateDifference(String startDate, String endDate, DateUnit dateUnit) { return switch (dateUnit) { diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java index ade5a4845a38..a6898fb73e19 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java @@ -255,16 +255,11 @@ public String jsonExtract(String json, String key, String property) { public String cast(String column, DataType dataType) { return switch (dataType) { case NUMERIC -> String.format("%s::numeric", column); - case BOOLEAN -> String.format("%s::numeric!=0", column); + case BOOLEAN -> String.format("%s::numeric != 0", column); case TEXT -> String.format("%s::text", column); }; } - @Override - public String age(String endDate, String startDate) { - return String.format("age(cast(%s as date), cast(%s as date))", endDate, startDate); - } - @Override public String dateDifference(String startDate, String endDate, DateUnit dateUnit) { return switch (dateUnit) { diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java index 0387241cecfd..5ac7c3c723bb 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java @@ -309,15 +309,6 @@ public interface SqlBuilder { */ String cast(String column, DataType dataType); - /** - * Generates a SQL expression that calculates the time interval between two dates in years. - * - * @param endDate The end date expression in the calculation - * @param startDate The start date expression in the calculation. - * @return A SQL string that calculates the age between the two dates - */ - String age(String endDate, String startDate); - /** * Generates SQL to calculate the difference between two dates based on the specified date part. * diff --git a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java index 96bd70b71a7b..8b77a3c0bd8d 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java @@ -241,6 +241,22 @@ void testJsonExtractObject() { sqlBuilder.jsonExtract("ev.eventdatavalues", "qrur9Dvnyt5", "value")); } + @Test + void testCast() { + assertEquals( + """ + toFloat64(ax."qrur9Dvnyt5")""", + sqlBuilder.cast("ax.\"qrur9Dvnyt5\"", org.hisp.dhis.analytics.DataType.NUMERIC)); + assertEquals( + """ + toUInt8(ax."qrur9Dvnyt5") != 0""", + sqlBuilder.cast("ax.\"qrur9Dvnyt5\"", org.hisp.dhis.analytics.DataType.BOOLEAN)); + assertEquals( + """ + toString(ax."qrur9Dvnyt5")""", + sqlBuilder.cast("ax.\"qrur9Dvnyt5\"", org.hisp.dhis.analytics.DataType.TEXT)); + } + @Test void testIfThen() { assertEquals( diff --git a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java index 141fdcc90c1a..3715c3b7000e 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java @@ -41,7 +41,7 @@ import org.junit.jupiter.api.Test; class DorisSqlBuilderTest { - private final SqlBuilder sqlBuilder = new DorisSqlBuilder("pg_dhis", "postgresql.jar"); + private final DorisSqlBuilder sqlBuilder = new DorisSqlBuilder("pg_dhis", "postgresql.jar"); private Table getTableA() { List columns = @@ -225,6 +225,22 @@ void testJsonExtractObject() { sqlBuilder.jsonExtract("ev.eventdatavalues", "qrur9Dvnyt5", "value")); } + @Test + void testCast() { + assertEquals( + """ + CAST(ax."qrur9Dvnyt5" AS DECIMAL)""", + sqlBuilder.cast("ax.\"qrur9Dvnyt5\"", org.hisp.dhis.analytics.DataType.NUMERIC)); + assertEquals( + """ + CAST(ax."qrur9Dvnyt5" AS DECIMAL) != 0""", + sqlBuilder.cast("ax.\"qrur9Dvnyt5\"", org.hisp.dhis.analytics.DataType.BOOLEAN)); + assertEquals( + """ + CAST(ax."qrur9Dvnyt5" AS CHAR)""", + sqlBuilder.cast("ax.\"qrur9Dvnyt5\"", org.hisp.dhis.analytics.DataType.TEXT)); + } + @Test void testIfThen() { assertEquals( diff --git a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java index a0cd4e5d3953..1cc1841f202b 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java @@ -44,7 +44,7 @@ import org.junit.jupiter.api.Test; class PostgreSqlBuilderTest { - private final SqlBuilder sqlBuilder = new PostgreSqlBuilder(); + private final PostgreSqlBuilder sqlBuilder = new PostgreSqlBuilder(); private Table getTableA() { List columns = @@ -251,6 +251,22 @@ void testJsonExtractObject() { sqlBuilder.jsonExtract("ev.eventdatavalues", "qrur9Dvnyt5", "value")); } + @Test + void testCast() { + assertEquals( + """ + ax."qrur9Dvnyt5"::numeric""", + sqlBuilder.cast("ax.\"qrur9Dvnyt5\"", org.hisp.dhis.analytics.DataType.NUMERIC)); + assertEquals( + """ + ax."qrur9Dvnyt5"::numeric != 0""", + sqlBuilder.cast("ax.\"qrur9Dvnyt5\"", org.hisp.dhis.analytics.DataType.BOOLEAN)); + assertEquals( + """ + ax."qrur9Dvnyt5"::text""", + sqlBuilder.cast("ax.\"qrur9Dvnyt5\"", org.hisp.dhis.analytics.DataType.TEXT)); + } + @Test void testIfThen() { assertEquals( diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonMeDto.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonMeDto.java new file mode 100644 index 000000000000..9dd5325a9e28 --- /dev/null +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonMeDto.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.test.webapi.json.domain; + +import java.time.LocalDateTime; +import org.hisp.dhis.jsontree.JsonBoolean; +import org.hisp.dhis.jsontree.JsonDate; +import org.hisp.dhis.jsontree.JsonList; + +/** + * Web API equivalent of a {@link org.hisp.dhis.webapi.controller.user.MeDto}. + * + * @author Morten Svanæs + */ +public interface JsonMeDto extends JsonIdentifiableObject { + default String getUsername() { + return getString("username").string(); + } + + default String getSurname() { + return getString("surname").string(); + } + + default String getFirstName() { + return getString("firstName").string(); + } + + default JsonList getUserGroups() { + return getList("userGroups", JsonUserGroup.class); + } + + default boolean getEmailVerified() { + return get("emailVerified", JsonBoolean.class).booleanValue(); + } + + default JsonList getOrganisationUnits() { + return getList("organisationUnits", JsonOrganisationUnit.class); + } + + default JsonList getDataViewOrganisationUnits() { + return getList("dataViewOrganisationUnits", JsonOrganisationUnit.class); + } + + default JsonList getTeiSearchOrganisationUnits() { + return getList("teiSearchOrganisationUnits", JsonOrganisationUnit.class); + } + + default LocalDateTime getLastLogin() { + return get("lastLogin", JsonDate.class).date(); + } + + default LocalDateTime getAccountExpiry() { + return get("accountExpiry", JsonDate.class).date(); + } +} diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/resources/tracker/simple_metadata.json b/dhis-2/dhis-support/dhis-support-test/src/main/resources/tracker/simple_metadata.json index ef773fcfb09e..2b61abbb750d 100644 --- a/dhis-2/dhis-support/dhis-support-test/src/main/resources/tracker/simple_metadata.json +++ b/dhis-2/dhis-support/dhis-support-test/src/main/resources/tracker/simple_metadata.json @@ -529,6 +529,7 @@ "lastUpdated": "2015-03-31T11:22:51.642", "name": "Height in cm", "shortName": "Height in cm", + "formName": "Height in cm", "url": "", "valueType": "NUMBER" }, @@ -549,6 +550,7 @@ "lastUpdated": "2015-03-31T11:22:51.642", "name": "Height in mm", "shortName": "Height in mm", + "formName": "Height in mm", "url": "", "valueType": "NUMBER" } diff --git a/dhis-2/dhis-test-e2e/pom.xml b/dhis-2/dhis-test-e2e/pom.xml index 6029aa1ae56a..5aef4f352e51 100644 --- a/dhis-2/dhis-test-e2e/pom.xml +++ b/dhis-2/dhis-test-e2e/pom.xml @@ -13,12 +13,12 @@ 3.13.0 3.5.2 1.4.0 - 5.11.3 + 5.11.4 2.11.0 2.24.3 5.5.0 2.18.2 - 33.3.1-jre + 33.4.0-jre 1.5 5.2.1 1.0.2 diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/tracked-entity-query.json b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/tracked-entity-query.json index 5cd5c698060c..e73a1d4f377c 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/tracked-entity-query.json +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/generator/scenarios/tracked-entity-query.json @@ -128,7 +128,7 @@ }, { "name": "financialYear2018Sep", - "query": "/api/42/analytics/trackedEntities/query/Zy2SEgA61ys.json?dimension=ou:USER_ORGUNIT,B6TnnFMgmCk&headers=ouname,B6TnnFMgmCk,created&totalPages=false&rowContext=true&created=2018Sep&displayProperty=NAME&pageSize=100&page=1&includeMetadataDetails=true", + "query": "/api/42/analytics/trackedEntities/query/Zy2SEgA61ys.json?dimension=ou:USER_ORGUNIT,B6TnnFMgmCk&headers=ouname,B6TnnFMgmCk,created&totalPages=false&rowContext=true&created=2018Sep&displayProperty=NAME&pageSize=100&page=1&includeMetadataDetails=true&asc=created", "version": { "min": 42 } diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityQuery8AutoTest.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityQuery8AutoTest.java index 5764f1946812..3f4abcc7261b 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityQuery8AutoTest.java +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityQuery8AutoTest.java @@ -60,6 +60,7 @@ public void financialYear2018Sep() throws JSONException { .add("rowContext=true") .add("pageSize=100") .add("page=1") + .add("asc=created") .add("dimension=ou:USER_ORGUNIT,B6TnnFMgmCk"); // When @@ -97,42 +98,42 @@ public void financialYear2018Sep() throws JSONException { response, 2, "created", "Created", "DATETIME", "java.time.LocalDateTime", false, true); // Assert rows. - validateRow(response, 0, List.of("Ngelehun CHC", "29", "2019-08-21 13:23:45.456")); - validateRow(response, 1, List.of("Ngelehun CHC", "19", "2019-08-21 13:23:48.994")); - validateRow(response, 2, List.of("Ngelehun CHC", "0", "2019-08-21 13:23:53.055")); - validateRow(response, 3, List.of("Ngelehun CHC", "33", "2019-08-21 13:23:19.551")); - validateRow(response, 4, List.of("Ngelehun CHC", "64", "2019-08-21 13:23:24.357")); - validateRow(response, 5, List.of("Ngelehun CHC", "29", "2019-08-21 13:23:34.045")); - validateRow(response, 6, List.of("Ngelehun CHC", "15", "2019-08-21 13:23:37.63")); - validateRow(response, 7, List.of("Ngelehun CHC", "33", "2019-08-21 13:23:41.186")); - validateRow(response, 8, List.of("Ngelehun CHC", "", "2019-08-21 13:24:04.74")); - validateRow(response, 9, List.of("Ngelehun CHC", "17", "2019-08-21 13:24:08.457")); - validateRow(response, 10, List.of("Ngelehun CHC", "30", "2019-08-21 13:24:13.102")); - validateRow(response, 11, List.of("Njandama MCHP", "11", "2019-08-21 13:24:17.391")); - validateRow(response, 12, List.of("Ngelehun CHC", "", "2019-08-21 13:24:21.658")); - validateRow(response, 13, List.of("Njandama MCHP", "26", "2019-08-21 13:24:56.022")); - validateRow(response, 14, List.of("Ngelehun CHC", "27", "2019-08-21 13:25:03.887")); - validateRow(response, 15, List.of("Ngelehun CHC", "25", "2019-08-21 13:25:07.634")); - validateRow(response, 16, List.of("Ngelehun CHC", "27", "2019-08-21 13:25:12.115")); - validateRow(response, 17, List.of("Ngelehun CHC", "30", "2019-08-21 13:25:16.651")); - validateRow(response, 18, List.of("Ngelehun CHC", "6", "2019-08-21 13:25:34.059")); - validateRow(response, 19, List.of("Ngelehun CHC", "9", "2019-08-21 13:25:47.06")); - validateRow(response, 20, List.of("Ngelehun CHC", "9", "2019-08-21 13:25:51.247")); - validateRow(response, 21, List.of("Njandama MCHP", "0", "2019-08-21 13:23:56.994")); - validateRow(response, 22, List.of("Ngelehun CHC", "32", "2019-08-21 13:24:00.882")); - validateRow(response, 23, List.of("Njandama MCHP", "36", "2019-08-21 13:23:30.144")); - validateRow(response, 24, List.of("Ngelehun CHC", "46", "2019-08-21 13:24:26.104")); - validateRow(response, 25, List.of("Ngelehun CHC", "0", "2019-08-21 13:24:30.66")); - validateRow(response, 26, List.of("Ngelehun CHC", "0", "2019-08-21 13:24:34.951")); - validateRow(response, 27, List.of("Ngelehun CHC", "0", "2019-08-21 13:24:38.952")); - validateRow(response, 28, List.of("Ngelehun CHC", "", "2019-08-21 13:24:43.358")); - validateRow(response, 29, List.of("Ngelehun CHC", "0", "2019-08-21 13:24:47.119")); - validateRow(response, 30, List.of("Njandama MCHP", "0", "2019-08-21 13:24:52.073")); - validateRow(response, 31, List.of("Ngelehun CHC", "21", "2019-08-21 13:24:59.811")); - validateRow(response, 32, List.of("Ngelehun CHC", "24", "2019-08-21 13:25:20.729")); - validateRow(response, 33, List.of("Ngelehun CHC", "23", "2019-08-21 13:25:25.258")); - validateRow(response, 34, List.of("Ngelehun CHC", "43", "2019-08-21 13:25:29.756")); - validateRow(response, 35, List.of("Ngelehun CHC", "30", "2019-08-21 13:25:38.022")); - validateRow(response, 36, List.of("Ngelehun CHC", "32", "2019-08-21 13:25:42.703")); + validateRow(response, 0, List.of("Ngelehun CHC", "33", "2019-08-21 13:23:19.551")); + validateRow(response, 1, List.of("Ngelehun CHC", "64", "2019-08-21 13:23:24.357")); + validateRow(response, 2, List.of("Njandama MCHP", "36", "2019-08-21 13:23:30.144")); + validateRow(response, 3, List.of("Ngelehun CHC", "29", "2019-08-21 13:23:34.045")); + validateRow(response, 4, List.of("Ngelehun CHC", "15", "2019-08-21 13:23:37.63")); + validateRow(response, 5, List.of("Ngelehun CHC", "33", "2019-08-21 13:23:41.186")); + validateRow(response, 6, List.of("Ngelehun CHC", "29", "2019-08-21 13:23:45.456")); + validateRow(response, 7, List.of("Ngelehun CHC", "19", "2019-08-21 13:23:48.994")); + validateRow(response, 8, List.of("Ngelehun CHC", "0", "2019-08-21 13:23:53.055")); + validateRow(response, 9, List.of("Njandama MCHP", "0", "2019-08-21 13:23:56.994")); + validateRow(response, 10, List.of("Ngelehun CHC", "32", "2019-08-21 13:24:00.882")); + validateRow(response, 11, List.of("Ngelehun CHC", "", "2019-08-21 13:24:04.74")); + validateRow(response, 12, List.of("Ngelehun CHC", "17", "2019-08-21 13:24:08.457")); + validateRow(response, 13, List.of("Ngelehun CHC", "30", "2019-08-21 13:24:13.102")); + validateRow(response, 14, List.of("Njandama MCHP", "11", "2019-08-21 13:24:17.391")); + validateRow(response, 15, List.of("Ngelehun CHC", "", "2019-08-21 13:24:21.658")); + validateRow(response, 16, List.of("Ngelehun CHC", "46", "2019-08-21 13:24:26.104")); + validateRow(response, 17, List.of("Ngelehun CHC", "0", "2019-08-21 13:24:30.66")); + validateRow(response, 18, List.of("Ngelehun CHC", "0", "2019-08-21 13:24:34.951")); + validateRow(response, 19, List.of("Ngelehun CHC", "0", "2019-08-21 13:24:38.952")); + validateRow(response, 20, List.of("Ngelehun CHC", "", "2019-08-21 13:24:43.358")); + validateRow(response, 21, List.of("Ngelehun CHC", "0", "2019-08-21 13:24:47.119")); + validateRow(response, 22, List.of("Njandama MCHP", "0", "2019-08-21 13:24:52.073")); + validateRow(response, 23, List.of("Njandama MCHP", "26", "2019-08-21 13:24:56.022")); + validateRow(response, 24, List.of("Ngelehun CHC", "21", "2019-08-21 13:24:59.811")); + validateRow(response, 25, List.of("Ngelehun CHC", "27", "2019-08-21 13:25:03.887")); + validateRow(response, 26, List.of("Ngelehun CHC", "25", "2019-08-21 13:25:07.634")); + validateRow(response, 27, List.of("Ngelehun CHC", "27", "2019-08-21 13:25:12.115")); + validateRow(response, 28, List.of("Ngelehun CHC", "30", "2019-08-21 13:25:16.651")); + validateRow(response, 29, List.of("Ngelehun CHC", "24", "2019-08-21 13:25:20.729")); + validateRow(response, 30, List.of("Ngelehun CHC", "23", "2019-08-21 13:25:25.258")); + validateRow(response, 31, List.of("Ngelehun CHC", "43", "2019-08-21 13:25:29.756")); + validateRow(response, 32, List.of("Ngelehun CHC", "6", "2019-08-21 13:25:34.059")); + validateRow(response, 33, List.of("Ngelehun CHC", "30", "2019-08-21 13:25:38.022")); + validateRow(response, 34, List.of("Ngelehun CHC", "32", "2019-08-21 13:25:42.703")); + validateRow(response, 35, List.of("Ngelehun CHC", "9", "2019-08-21 13:25:47.06")); + validateRow(response, 36, List.of("Ngelehun CHC", "9", "2019-08-21 13:25:51.247")); } } diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/login/LoginTest.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/login/LoginTest.java index b74127a9c8b2..87851d0f940a 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/login/LoginTest.java +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/login/LoginTest.java @@ -37,6 +37,7 @@ import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.HashMap; import java.util.List; import java.util.Map; import javax.mail.BodyPart; @@ -230,6 +231,20 @@ void testReEnrollFails() throws JsonProcessingException { "User has 2FA enabled already, disable 2FA before you try to enroll again"); } + @Test + void redirectAfterEmailVerificationFailure() { + RestTemplate template = addAdminBasicAuthHeaders(getRestTemplateNoRedirects()); + ResponseEntity response = + template.exchange( + dhis2ServerApi + "/account/verifyEmail?token=WRONGTOKEN", + HttpMethod.GET, + new HttpEntity<>(new HashMap(), jsonHeaders()), + String.class); + assertEquals(HttpStatus.FOUND, response.getStatusCode()); + List location = response.getHeaders().get("Location"); + assertEquals(dhis2Server + "dhis-web-login/#/email-verification-failure", location.get(0)); + } + @Test void testRedirectWithQueryParam() { assertRedirectToSameUrl("/api/users?fields=id,name,displayName"); @@ -269,7 +284,7 @@ void testRedirectToCssResourceWorker() { void testRedirectAccountWhenVerifiedEmailEnforced() { changeSystemSetting("enforceVerifiedEmail", "true"); try { - assertRedirectUrl("/dhis-web-dashboard/", "/dhis-web-user-profile/#/account"); + assertRedirectUrl("/dhis-web-dashboard/", "/dhis-web-user-profile/#/profile"); } finally { changeSystemSetting("enforceVerifiedEmail", "false"); } @@ -472,8 +487,11 @@ private static void sendVerificationEmail(String cookie) { private static void verifyEmailWithToken(String cookie, String verifyToken) { ResponseEntity verifyEmailResp = - getWithCookie("/account/verifyEmail?token=" + verifyToken, cookie); - assertEquals(HttpStatus.OK, verifyEmailResp.getStatusCode()); + getWithCookie( + getRestTemplateNoRedirects(), "/account/verifyEmail?token=" + verifyToken, cookie); + assertEquals(HttpStatus.FOUND, verifyEmailResp.getStatusCode()); + List location = verifyEmailResp.getHeaders().get("Location"); + assertEquals(dhis2Server + "dhis-web-login/#/email-verification-success", location.get(0)); } // -------------------------------------------------------------------------------------------- @@ -536,17 +554,7 @@ private static void assertRedirectUrl(String url, String redirectUrl) { private static void testRedirectWhenLoggedIn(String url, String redirectUrl) { // Disable auto-redirects - ClientHttpRequestFactory requestFactory = - new SimpleClientHttpRequestFactory() { - @Override - protected void prepareConnection(HttpURLConnection connection, String httpMethod) - throws IOException { - super.prepareConnection(connection, httpMethod); - connection.setInstanceFollowRedirects(false); - } - }; - - RestTemplate restTemplateNoRedirects = new RestTemplate(requestFactory); + RestTemplate restTemplateNoRedirects = getRestTemplateNoRedirects(); // Do an invalid login to capture URL request ResponseEntity firstResponse = @@ -623,29 +631,34 @@ private static ResponseEntity postWithCookie(String path, Object body, S } } - private static ResponseEntity getWithCookie(String path, String cookie) { + private static ResponseEntity getWithCookie( + RestTemplate template, String path, String cookie) { HttpHeaders headers = jsonHeaders(); headers.set("Cookie", cookie); - return exchangeWithHeaders(path, HttpMethod.GET, null, headers); + return exchangeWithHeaders(template, path, HttpMethod.GET, null, headers); + } + + private static ResponseEntity getWithCookie(String path, String cookie) { + return getWithCookie(restTemplate, path, cookie); } private static ResponseEntity getWithAdminBasicAuth( String path, Map map) { - RestTemplate rt = createRestTemplateWithAdminBasicAuthHeader(); + RestTemplate rt = addAdminBasicAuthHeaders(new RestTemplate()); return rt.exchange( dhis2ServerApi + path, HttpMethod.GET, new HttpEntity<>(map, jsonHeaders()), String.class); } private static ResponseEntity postWithAdminBasicAuth( String path, Map map) { - RestTemplate rt = createRestTemplateWithAdminBasicAuthHeader(); + RestTemplate rt = addAdminBasicAuthHeaders(new RestTemplate()); return rt.exchange( dhis2ServerApi + path, HttpMethod.POST, new HttpEntity<>(map, jsonHeaders()), String.class); } private static ResponseEntity deleteWithAdminBasicAuth( String path, Map map) { - RestTemplate rt = createRestTemplateWithAdminBasicAuthHeader(); + RestTemplate rt = addAdminBasicAuthHeaders(new RestTemplate()); return rt.exchange( dhis2ServerApi + path, HttpMethod.DELETE, @@ -655,16 +668,20 @@ private static ResponseEntity deleteWithAdminBasicAuth( private static ResponseEntity exchangeWithHeaders( String path, HttpMethod method, Object body, HttpHeaders headers) { + return exchangeWithHeaders(restTemplate, path, method, body, headers); + } + + private static ResponseEntity exchangeWithHeaders( + RestTemplate template, String path, HttpMethod method, Object body, HttpHeaders headers) { try { - return restTemplate.exchange( + return template.exchange( dhis2ServerApi + path, method, new HttpEntity<>(body, headers), String.class); } catch (HttpClientErrorException e) { return ResponseEntity.status(e.getStatusCode()).body(e.getResponseBodyAsString()); } } - private static RestTemplate createRestTemplateWithAdminBasicAuthHeader() { - RestTemplate template = new RestTemplate(); + private static RestTemplate addAdminBasicAuthHeaders(RestTemplate template) { String authHeader = Base64.getUrlEncoder().encodeToString("admin:district".getBytes(StandardCharsets.UTF_8)); template @@ -677,6 +694,21 @@ private static RestTemplate createRestTemplateWithAdminBasicAuthHeader() { return template; } + @NotNull + private static RestTemplate getRestTemplateNoRedirects() { + // Disable auto-redirects + ClientHttpRequestFactory requestFactory = + new SimpleClientHttpRequestFactory() { + @Override + protected void prepareConnection(HttpURLConnection connection, String httpMethod) + throws IOException { + super.prepareConnection(connection, httpMethod); + connection.setInstanceFollowRedirects(false); + } + }; + return new RestTemplate(requestFactory); + } + // -------------------------------------------------------------------------------------------- // Private helper methods for parsing and extracting content from emails // -------------------------------------------------------------------------------------------- @@ -812,7 +844,7 @@ private static void setSystemProperty(String property, String value) { private static void changeSystemSetting(String key, String value) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); - RestTemplate rt = createRestTemplateWithAdminBasicAuthHeader(); + RestTemplate rt = addAdminBasicAuthHeaders(new RestTemplate()); ResponseEntity response = rt.exchange( dhis2ServerApi + "/systemSettings/" + key, diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/program/ProgramIndicatorServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/program/ProgramIndicatorServiceTest.java index 27f87bd1af07..f7c124b61627 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/program/ProgramIndicatorServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/program/ProgramIndicatorServiceTest.java @@ -551,14 +551,14 @@ void testBooleanAsNumeric() { @Test void testBooleanAsBoolean() { assertEquals( - "coalesce(case when ax.\"ps\" = 'ProgrmStagA' then \"DataElmentG\" else null end::numeric!=0,false)", + "coalesce(case when ax.\"ps\" = 'ProgrmStagA' then \"DataElmentG\" else null end::numeric != 0,false)", filter("#{ProgrmStagA.DataElmentG}")); } @Test void testBooleanAsBooleanWithinIf() { assertEquals( - " case when coalesce(case when ax.\"ps\" = 'ProgrmStagA' then \"DataElmentG\" else null end::numeric!=0,false) then 4 else 5 end", + " case when coalesce(case when ax.\"ps\" = 'ProgrmStagA' then \"DataElmentG\" else null end::numeric != 0,false) then 4 else 5 end", sql("if(#{ProgrmStagA.DataElmentG},4,5)")); } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/event/OrderAndFilterEventChangeLogTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/event/OrderAndFilterEventChangeLogTest.java index 943e3318264c..c01879abbcb2 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/event/OrderAndFilterEventChangeLogTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/event/OrderAndFilterEventChangeLogTest.java @@ -153,55 +153,58 @@ void shouldSortChangeLogsWhenOrderingByCreatedAtAsc() void shouldSortChangeLogsWhenOrderingByDataElementAsc() throws ForbiddenException, NotFoundException { EventChangeLogOperationParams params = - EventChangeLogOperationParams.builder().orderBy("dataElement", SortDirection.ASC).build(); + EventChangeLogOperationParams.builder().orderBy("change", SortDirection.ASC).build(); Event event = getEvent("kWjSezkXHVp"); updateDataValues(event, "GieVkTxp4HH", "20", "25"); updateDataValues(event, "GieVkTxp4HG", "20"); List changeLogs = - getDataElementChangeLogs( - eventChangeLogService.getEventChangeLog( - UID.of("kWjSezkXHVp"), params, defaultPageParams)); + eventChangeLogService + .getEventChangeLog(UID.of("kWjSezkXHVp"), params, defaultPageParams) + .getItems(); - assertNumberOfChanges(5, changeLogs); + assertNumberOfChanges(7, changeLogs); assertAll( - () -> assertDataElementUpdate("GieVkTxp4HG", "10", "20", changeLogs.get(0)), - () -> assertDataElementCreate("GieVkTxp4HG", "10", changeLogs.get(1)), - () -> assertDataElementUpdate("GieVkTxp4HH", "20", "25", changeLogs.get(2)), - () -> assertDataElementUpdate("GieVkTxp4HH", "15", "20", changeLogs.get(3)), - () -> assertDataElementCreate("GieVkTxp4HH", "15", changeLogs.get(4))); + () -> assertDataElementUpdate("GieVkTxp4HH", "20", "25", changeLogs.get(0)), + () -> assertDataElementUpdate("GieVkTxp4HH", "15", "20", changeLogs.get(1)), + () -> assertDataElementCreate("GieVkTxp4HH", "15", changeLogs.get(2)), + () -> assertDataElementUpdate("GieVkTxp4HG", "10", "20", changeLogs.get(3)), + () -> assertDataElementCreate("GieVkTxp4HG", "10", changeLogs.get(4)), + () -> assertFieldCreate("occurredAt", "2022-04-22 06:00:38.343", changeLogs.get(5)), + () -> assertFieldCreate("scheduledAt", "2022-04-26 06:00:34.323", changeLogs.get(6))); } @Test - void shouldSortChangeLogsWhenOrderingByDataElementDesc() - throws ForbiddenException, NotFoundException { + void shouldSortChangeLogsWhenOrderingByChangeDesc() throws ForbiddenException, NotFoundException { EventChangeLogOperationParams params = - EventChangeLogOperationParams.builder().orderBy("dataElement", SortDirection.DESC).build(); + EventChangeLogOperationParams.builder().orderBy("change", SortDirection.DESC).build(); Event event = getEvent("kWjSezkXHVp"); updateDataValues(event, "GieVkTxp4HH", "20", "25"); updateDataValues(event, "GieVkTxp4HG", "20"); List changeLogs = - getDataElementChangeLogs( - eventChangeLogService.getEventChangeLog( - UID.of("kWjSezkXHVp"), params, defaultPageParams)); + eventChangeLogService + .getEventChangeLog(UID.of("kWjSezkXHVp"), params, defaultPageParams) + .getItems(); - assertNumberOfChanges(5, changeLogs); + assertNumberOfChanges(7, changeLogs); assertAll( - () -> assertDataElementUpdate("GieVkTxp4HH", "20", "25", changeLogs.get(0)), - () -> assertDataElementUpdate("GieVkTxp4HH", "15", "20", changeLogs.get(1)), - () -> assertDataElementCreate("GieVkTxp4HH", "15", changeLogs.get(2)), - () -> assertDataElementUpdate("GieVkTxp4HG", "10", "20", changeLogs.get(3)), - () -> assertDataElementCreate("GieVkTxp4HG", "10", changeLogs.get(4))); + () -> assertFieldCreate("scheduledAt", "2022-04-26 06:00:34.323", changeLogs.get(0)), + () -> assertFieldCreate("occurredAt", "2022-04-22 06:00:38.343", changeLogs.get(1)), + () -> assertDataElementUpdate("GieVkTxp4HG", "10", "20", changeLogs.get(2)), + () -> assertDataElementCreate("GieVkTxp4HG", "10", changeLogs.get(3)), + () -> assertDataElementUpdate("GieVkTxp4HH", "20", "25", changeLogs.get(4)), + () -> assertDataElementUpdate("GieVkTxp4HH", "15", "20", changeLogs.get(5)), + () -> assertDataElementCreate("GieVkTxp4HH", "15", changeLogs.get(6))); } @Test - void shouldSortChangeLogsWhenOrderingByFieldAsc() + void shouldSortChangeLogsWhenOrderingByChangeAscAndChangesOnlyToEventFields() throws ForbiddenException, NotFoundException, IOException { EventChangeLogOperationParams params = - EventChangeLogOperationParams.builder().orderBy("field", SortDirection.ASC).build(); + EventChangeLogOperationParams.builder().orderBy("change", SortDirection.ASC).build(); UID event = UID.of("QRYjLTiJTrA"); LocalDateTime currentTime = LocalDateTime.now(); @@ -232,10 +235,10 @@ void shouldSortChangeLogsWhenOrderingByFieldAsc() } @Test - void shouldSortChangeLogsWhenOrderingByFieldDesc() + void shouldSortChangeLogsWhenOrderingByChangeDescAndChangesOnlyToEventFields() throws ForbiddenException, NotFoundException, IOException { EventChangeLogOperationParams params = - EventChangeLogOperationParams.builder().orderBy("field", SortDirection.DESC).build(); + EventChangeLogOperationParams.builder().orderBy("change", SortDirection.DESC).build(); UID event = UID.of("QRYjLTiJTrA"); LocalDateTime currentTime = LocalDateTime.now(); diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AccountControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AccountControllerTest.java index 6422a1c19332..04aebea47b15 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AccountControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AccountControllerTest.java @@ -246,11 +246,17 @@ void testVerifyEmailWithTokenTwice() { String token = extractTokenFromEmailText(emailMessage.getText()); assertNotNull(token); - assertStatus(HttpStatus.OK, GET("/account/verifyEmail?token=" + token)); + HttpResponse success = GET("/account/verifyEmail?token=" + token); + assertStatus(HttpStatus.FOUND, success); + assertEquals( + "http://localhost/dhis-web-login/#/email-verification-success", success.header("Location")); user = userService.getUser(user.getId()); assertTrue(userService.isEmailVerified(user)); - assertStatus(HttpStatus.CONFLICT, GET("/account/verifyEmail?token=" + token)); + HttpResponse failure = GET("/account/verifyEmail?token=" + token); + assertStatus(HttpStatus.FOUND, failure); + assertEquals( + "http://localhost/dhis-web-login/#/email-verification-failure", failure.header("Location")); } @Test @@ -283,7 +289,10 @@ void testVerifyEmailWithToken() { String token = extractTokenFromEmailText(emailMessage.getText()); assertValidEmailVerificationToken(token); - assertStatus(HttpStatus.OK, GET("/account/verifyEmail?token=" + token)); + HttpResponse success = GET("/account/verifyEmail?token=" + token); + assertStatus(HttpStatus.FOUND, success); + assertEquals( + "http://localhost/dhis-web-login/#/email-verification-success", success.header("Location")); user = userService.getUser(user.getId()); assertTrue(userService.isEmailVerified(user)); } @@ -291,12 +300,10 @@ void testVerifyEmailWithToken() { @Test void testVerifyWithBadToken() { switchToNewUser("zod"); - assertWebMessage( - "Conflict", - 409, - "ERROR", - "Verification token is invalid", - GET("/account/verifyEmail?token=eviltoken").content(HttpStatus.CONFLICT)); + HttpResponse response = GET("/account/verifyEmail?token=WRONGTOKEN"); + assertStatus(HttpStatus.FOUND, response); + String location = response.header("Location"); + assertEquals("http://localhost/dhis-web-login/#/email-verification-failure", location); } @Test diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/MeControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/MeControllerTest.java index 2b342076fd48..57efc656588b 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/MeControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/MeControllerTest.java @@ -48,7 +48,7 @@ import org.hisp.dhis.security.apikey.ApiKeyTokenGenerator; import org.hisp.dhis.security.apikey.ApiTokenStore; import org.hisp.dhis.test.webapi.H2ControllerIntegrationTestBase; -import org.hisp.dhis.test.webapi.json.domain.JsonUser; +import org.hisp.dhis.test.webapi.json.domain.JsonMeDto; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.User; import org.junit.jupiter.api.BeforeEach; @@ -77,7 +77,7 @@ void setUp() { @Test void testGetCurrentUser() { switchToAdminUser(); - assertEquals(getCurrentUser().getUid(), GET("/me").content().as(JsonUser.class).getId()); + assertEquals(getCurrentUser().getUid(), GET("/me").content().as(JsonMeDto.class).getId()); } @Test @@ -97,7 +97,7 @@ void testGetAuthorities() { @Test void testUpdateCurrentUser() { assertSeries(Series.SUCCESSFUL, PUT("/me", "{'surname':'Lars'}")); - assertEquals("Lars", GET("/me").content().as(JsonUser.class).getSurname()); + assertEquals("Lars", GET("/me").content().as(JsonMeDto.class).getSurname()); } @Test @@ -108,6 +108,11 @@ void testHasAuthority() { assertFalse(GET("/me/authorities/missing").content(HttpStatus.OK).booleanValue()); } + @Test + void testGetEmailVerifiedProperty() { + assertFalse(GET("/me").content().as(JsonMeDto.class).getEmailVerified()); + } + @Test void testGetSettings() { JsonObject settings = GET("/me/settings").content(HttpStatus.OK); @@ -274,6 +279,6 @@ void testGetCurrentUserAttributeValues() { userService.updateUser(userByUsername); assertEquals( - "myvalue", GET("/me").content().as(JsonUser.class).getAttributeValues().get(0).getValue()); + "myvalue", GET("/me").content().as(JsonMeDto.class).getAttributeValues().get(0).getValue()); } } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/VisualizationControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/VisualizationControllerTest.java index b6d1c41a690b..5a4e7c5c2fab 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/VisualizationControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/VisualizationControllerTest.java @@ -30,6 +30,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; +import static org.hisp.dhis.analytics.AggregationType.SUM; +import static org.hisp.dhis.common.ValueType.TEXT; import static org.hisp.dhis.http.HttpAssertions.assertStatus; import static org.hisp.dhis.http.HttpStatus.CONFLICT; import static org.hisp.dhis.http.HttpStatus.CREATED; @@ -39,6 +41,7 @@ import java.nio.file.Path; import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.http.HttpStatus; import org.hisp.dhis.indicator.Indicator; import org.hisp.dhis.indicator.IndicatorType; @@ -47,8 +50,10 @@ import org.hisp.dhis.jsontree.JsonNode; import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.program.Program; +import org.hisp.dhis.program.ProgramTrackedEntityAttribute; import org.hisp.dhis.test.webapi.H2ControllerIntegrationTestBase; import org.hisp.dhis.test.webapi.json.domain.JsonImportSummary; +import org.hisp.dhis.trackedentity.TrackedEntityAttribute; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -313,4 +318,222 @@ void testRelativePeriods() { assertTrue( visualization.getObject("relativePeriods").getBoolean("last12Months").booleanValue()); } + + @Test + void testPostOptionSetItemInProgramAttribute() { + // Given + TrackedEntityAttribute trackedEntityAttribute = + new TrackedEntityAttribute("tea", "tea-desc", TEXT, false, false); + trackedEntityAttribute.setShortName("tea-shortName"); + trackedEntityAttribute.setAggregationType(SUM); + manager.save(trackedEntityAttribute); + + ProgramTrackedEntityAttribute programTrackedEntityAttribute = + new ProgramTrackedEntityAttribute(mockProgram, trackedEntityAttribute); + manager.save(programTrackedEntityAttribute); + + String programUid = programTrackedEntityAttribute.getProgram().getUid(); + String attributeUid = programTrackedEntityAttribute.getAttribute().getUid(); + String jsonBody = + """ +{ + "type": "PIVOT_TABLE", + "columns": [ + { + "dimension": "dx", + "items": [ + { + "id": "${program}.${attribute}", + "name": "Program Attribute - OptionSet", + "dimensionItemType": "PROGRAM_ATTRIBUTE", + "optionSetItem": { + "aggregation": "DISAGGREGATED", + "options": [ + "BFhv3jQZ8Cw" + ] + } + } + ] + } + ], + "name": "OptionSetItem - Test" +} +""" + .replace("${program}", programUid) + .replace("${attribute}", attributeUid); + + // When + String uid = assertStatus(CREATED, POST("/visualizations/", jsonBody)); + + // Then + String getParams = + "?fields=dataDimensionItems[programAttribute[attribute[optionSet[options[:all]]]]],attributeValues[:all,attribute[id,name,displayName]],columns[:all,items[:all,optionSetItem[options,aggregation]]"; + JsonObject response = GET("/visualizations/" + uid + getParams).content(); + + JsonNode columnNode = response.get("columns").node().element(0); + JsonNode itemsNode = columnNode.get("items").elementOrNull(0); + + assertEquals("DATA_X", columnNode.get("dimensionType").value()); + assertTrue((boolean) columnNode.get("dataDimension").value()); + assertEquals("PROGRAM_ATTRIBUTE", itemsNode.get("dimensionItemType").value()); + assertEquals("SUM", itemsNode.get("aggregationType").value()); + assertEquals(programUid + "." + attributeUid, itemsNode.get("dimensionItem").value()); + assertEquals("DISAGGREGATED", itemsNode.get("optionSetItem").get("aggregation").value()); + assertEquals( + "[\"BFhv3jQZ8Cw\"]", itemsNode.get("optionSetItem").get("options").value().toString()); + } + + @Test + void testPostOptionSetItemInDataElement() { + // Given + DataElement dataElement = createDataElement('A'); + manager.save(dataElement); + + String dataElementUid = dataElement.getUid(); + String jsonBody = + """ +{ + "type": "PIE", + "columns": [ + { + "dimension": "dx", + "items": [ + { + "id": "${dataElement}", + "name": "Data Element - OptionSet", + "dimensionItemType": "DATA_ELEMENT", + "optionSetItem": { + "aggregation": "AGGREGATED", + "options": [ + "BFhv3jQZ8Cw" + ] + } + } + ] + } + ], + "name": "OptionSetItem - Test" +} +""" + .replace("${dataElement}", dataElementUid); + + // When + String uid = assertStatus(CREATED, POST("/visualizations/", jsonBody)); + + // Then + String getParams = "?fields=columns[:all,items[:all,optionSetItem[options,aggregation]]"; + JsonObject response = GET("/visualizations/" + uid + getParams).content(); + + JsonNode columnNode = response.get("columns").node().element(0); + JsonNode itemsNode = columnNode.get("items").elementOrNull(0); + + assertEquals("DATA_X", columnNode.get("dimensionType").value()); + assertTrue((boolean) columnNode.get("dataDimension").value()); + assertEquals("DATA_ELEMENT", itemsNode.get("dimensionItemType").value()); + assertEquals("SUM", itemsNode.get("aggregationType").value()); + assertEquals(dataElementUid, itemsNode.get("dimensionItem").value()); + assertEquals("AGGREGATED", itemsNode.get("optionSetItem").get("aggregation").value()); + assertEquals( + "[\"BFhv3jQZ8Cw\"]", itemsNode.get("optionSetItem").get("options").value().toString()); + } + + @Test + void testPostWithoutOptionSetItemAndLoadDefaults() { + // Given + DataElement dataElement = createDataElement('A'); + manager.save(dataElement); + + String dataElementUid = dataElement.getUid(); + String jsonBody = + """ +{ + "type": "PIE", + "columns": [ + { + "dimension": "dx", + "items": [ + { + "id": "${dataElement}", + "name": "Data Element - OptionSet", + "dimensionItemType": "DATA_ELEMENT" + } + ] + } + ], + "name": "OptionSetItem - Test" +} +""" + .replace("${dataElement}", dataElementUid); + + // When + String uid = assertStatus(CREATED, POST("/visualizations/", jsonBody)); + + // Then + String getParams = "?fields=columns[:all,items[:all,optionSetItem[options,aggregation]]"; + JsonObject response = GET("/visualizations/" + uid + getParams).content(); + + JsonNode columnNode = response.get("columns").node().element(0); + JsonNode itemsNode = columnNode.get("items").elementOrNull(0); + + assertEquals("DATA_X", columnNode.get("dimensionType").value()); + assertTrue((boolean) columnNode.get("dataDimension").value()); + assertEquals("DATA_ELEMENT", itemsNode.get("dimensionItemType").value()); + assertEquals("SUM", itemsNode.get("aggregationType").value()); + assertEquals(dataElementUid, itemsNode.get("dimensionItem").value()); + assertEquals("AGGREGATED", itemsNode.get("optionSetItem").get("aggregation").value()); + assertEquals("[]", itemsNode.get("optionSetItem").get("options").value().toString()); + } + + @Test + void testPostOptionSetItemWithNoAggregation() { + // Given + DataElement dataElement = createDataElement('A'); + manager.save(dataElement); + + String dataElementUid = dataElement.getUid(); + String jsonBody = + """ +{ + "type": "PIE", + "columns": [ + { + "dimension": "dx", + "items": [ + { + "id": "${dataElement}", + "name": "Data Element - OptionSet", + "dimensionItemType": "DATA_ELEMENT", + "optionSetItem": { + "options": [ + "BFhv3jQZ8Cw" + ] + } + } + ] + } + ], + "name": "OptionSetItem - Test" +} +""" + .replace("${dataElement}", dataElementUid); + + // When + String uid = assertStatus(CREATED, POST("/visualizations/", jsonBody)); + + // Then + String getParams = "?fields=columns[:all,items[:all,optionSetItem[options,aggregation]]"; + JsonObject response = GET("/visualizations/" + uid + getParams).content(); + + JsonNode columnNode = response.get("columns").node().element(0); + JsonNode itemsNode = columnNode.get("items").elementOrNull(0); + + assertEquals("DATA_X", columnNode.get("dimensionType").value()); + assertTrue((boolean) columnNode.get("dataDimension").value()); + assertEquals("DATA_ELEMENT", itemsNode.get("dimensionItemType").value()); + assertEquals("SUM", itemsNode.get("aggregationType").value()); + assertEquals(dataElementUid, itemsNode.get("dimensionItem").value()); + assertEquals("AGGREGATED", itemsNode.get("optionSetItem").get("aggregation").value()); + assertEquals( + "[\"BFhv3jQZ8Cw\"]", itemsNode.get("optionSetItem").get("options").value().toString()); + } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AccountController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AccountController.java index 8fbe8cdf33fe..c8bbe94ddc7c 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AccountController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AccountController.java @@ -81,6 +81,7 @@ import org.hisp.dhis.user.UserRole; import org.hisp.dhis.user.UserService; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; +import org.hisp.dhis.webapi.utils.ContextUtils; import org.hisp.dhis.webapi.utils.HttpServletRequestPaths; import org.hisp.dhis.webapi.webdomain.user.UserLookups; import org.springframework.http.HttpStatus; @@ -564,15 +565,21 @@ public void sendEmailVerification(@CurrentUser User currentUser, HttpServletRequ currentUser, token, HttpServletRequestPaths.getContextPath(request)); if (!successfullySent) { - throw new ConflictException("Failed to send email verification token"); + throw new ConflictException( + "Sorry, we couldn’t send your verification email. Please try again or contact support."); } } @GetMapping("/verifyEmail") - @ResponseStatus(HttpStatus.OK) - public void verifyEmail(@RequestParam String token) throws ConflictException { - if (!userService.verifyEmail(token)) { - throw new ConflictException("Verification token is invalid"); + public void verifyEmail( + @RequestParam String token, HttpServletRequest request, HttpServletResponse response) + throws IOException { + if (userService.verifyEmail(token)) { + response.sendRedirect( + ContextUtils.getRootPath(request) + "/dhis-web-login/#/email-verification-success"); + } else { + response.sendRedirect( + ContextUtils.getRootPath(request) + "/dhis-web-login/#/email-verification-failure"); } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/security/AuthenticationController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/security/AuthenticationController.java index d88aa8a24b54..dbb96cae01c2 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/security/AuthenticationController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/security/AuthenticationController.java @@ -229,12 +229,12 @@ private String getRedirectUrl( redirectUrl += "/"; } - // Check enforce verified email, redirect to the account page if email is not verified + // Check enforce verified email, redirect to the profile page if email is not verified boolean enforceVerifiedEmail = settingsProvider.getCurrentSettings().getEnforceVerifiedEmail(); if (enforceVerifiedEmail) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); if (!userDetails.isEmailVerified()) { - return request.getContextPath() + "/dhis-web-user-profile/#/account"; + return request.getContextPath() + "/dhis-web-user-profile/#/profile"; } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/MeDto.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/MeDto.java index 1747f8c9c883..76de960a06f1 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/MeDto.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/MeDto.java @@ -85,6 +85,7 @@ public MeDto( this.access = user.getAccess(); this.name = user.getName(); this.email = user.getEmail(); + this.emailVerified = user.isEmailVerified(); this.phoneNumber = user.getPhoneNumber(); this.introduction = user.getIntroduction(); this.birthday = user.getBirthday(); @@ -166,6 +167,8 @@ public MeDto( @JsonProperty private String email; + @JsonProperty private boolean emailVerified; + @JsonProperty private String phoneNumber; @JsonProperty private String introduction; diff --git a/dhis-2/pom.xml b/dhis-2/pom.xml index 489db5b714b8..ead83aebaee0 100644 --- a/dhis-2/pom.xml +++ b/dhis-2/pom.xml @@ -169,7 +169,7 @@ 2.38.0 - 4.1.115.Final + 4.1.116.Final 4.8.179 @@ -181,7 +181,7 @@ 1.18.3 - 33.3.1-jre + 33.4.0-jre 3.0.2 4.2 2.7.1 @@ -213,7 +213,7 @@ 1.7.36 - 5.11.3 + 5.11.4 5.2.0 2.0.9 3.0 diff --git a/jenkinsfiles/stable b/jenkinsfiles/stable index 321b5040a9a9..0018d4142124 100644 --- a/jenkinsfiles/stable +++ b/jenkinsfiles/stable @@ -122,7 +122,7 @@ pipeline { oldImageTag = env.DOCKER_IMAGE_TAG.replace("-rc", "") // If version contains more than 2 dots... It's a hotfix. - boolean isHotfix = oldImageTag.length() - oldImageTag.replace(".", "").length() > 2 + isHotfix = oldImageTag.length() - oldImageTag.replace(".", "").length() > 2 if (!isHotfix) { oldImageTag = "$oldImageTag.0"