Skip to content

Commit

Permalink
feat: Add sorting into Visualization [DHIS2-16369] (#16086)
Browse files Browse the repository at this point in the history
  • Loading branch information
maikelarabori authored Jan 9, 2024
1 parent d9af17c commit 202bb64
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* (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.eventvisualization;
package org.hisp.dhis.analytics;

import static org.hisp.dhis.common.DxfNamespaces.DXF_2_0;

Expand All @@ -35,7 +35,6 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.hisp.dhis.analytics.SortOrder;

/** This class is responsible for the encapsulation of objects and attributes related to sorting. */
@Data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import java.util.List;
import org.hisp.dhis.analytics.EventDataType;
import org.hisp.dhis.analytics.EventOutputType;
import org.hisp.dhis.analytics.Sorting;
import org.hisp.dhis.common.AnalyticsType;
import org.hisp.dhis.common.BaseAnalyticalObject;
import org.hisp.dhis.common.BaseDimensionalItemObject;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
import static com.fasterxml.jackson.annotation.JsonProperty.Access.READ_ONLY;
import static com.google.common.base.Verify.verify;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.collections4.CollectionUtils.isEmpty;
import static org.apache.commons.lang3.StringUtils.containsAny;
import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.join;
Expand Down Expand Up @@ -64,6 +66,7 @@
import java.util.Objects;
import java.util.Set;
import org.hisp.dhis.analytics.NumberType;
import org.hisp.dhis.analytics.Sorting;
import org.hisp.dhis.category.CategoryCombo;
import org.hisp.dhis.common.BaseAnalyticalObject;
import org.hisp.dhis.common.CombinationGenerator;
Expand Down Expand Up @@ -143,6 +146,9 @@ public class Visualization extends BaseAnalyticalObject implements MetadataObjec
/** The number type. */
private NumberType numberType;

/** Stores the sorting state in the current object. */
private List<Sorting> sorting = new ArrayList<>();

/**
* List of {@link Series}. Refers to the dimension items in the first dimension of the "columns"
* list by dimension item identifier.
Expand Down Expand Up @@ -397,6 +403,19 @@ public void setIcons(Set<Icon> icons) {
this.icons = icons;
}

@JsonProperty("sorting")
@JacksonXmlElementWrapper(localName = "sorting", namespace = DXF_2_0)
@JacksonXmlProperty(localName = "sortingItem", namespace = DXF_2_0)
public List<Sorting> getSorting() {
return sorting;
}

public void setSorting(List<Sorting> sorting) {
if (sorting != null) {
this.sorting = sorting.stream().distinct().collect(toList());
}
}

@JsonProperty
@JacksonXmlProperty(namespace = DXF_2_0)
public NumberType getNumberType() {
Expand Down Expand Up @@ -807,6 +826,22 @@ private List<DimensionalItemObject> getDimensionalItemObjects(String dimension)
return object != null ? object.getItems() : null;
}

/** Validates the state of the current list of {@link Sorting} objects (if one is defined). */
public void validateSortingState() {
List<String> columns = getColumnDimensions();
List<Sorting> sortingList = getSorting();

sortingList.forEach(
s -> {
if (isBlank(s.getDimension()) || s.getDirection() == null) {
throw new IllegalArgumentException("Sorting is not valid");
} else if (columns.stream()
.noneMatch(c -> containsAny(s.getDimension(), c.split("\\.")))) {
throw new IllegalStateException(s.getDimension());
}
});
}

/**
* Based on the given arguments, this method will populate the current "gridColumns" and
* "gridRows" objects. It also sets the title of the grid ("gridTitle").
Expand Down Expand Up @@ -975,8 +1010,8 @@ public Grid getGrid(
addHeadersForRows(grid);
addHeadersForReport(grid, reportParamColumns);

final int startColumnIndex = grid.getHeaders().size();
final int numberOfColumns = getGridColumns().size();
int startColumnIndex = grid.getHeaders().size();
int numberOfColumns = getGridColumns().size();

addHeadersForColumns(grid, displayProperty);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@

<property name="icons" type="jbIcons"/>

<property name="sorting" type="jblSorting"/>

<component name="legendDefinitions" class="org.hisp.dhis.visualization.LegendDefinitions">
<property name="legendDisplayStyle" length="40">
<type name="org.hibernate.type.EnumType">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- https://dhis2.atlassian.net/browse/DHIS2-16369
-- Adds a new column "sorting" into Visualization table.

alter table visualization add column if not exists "sorting" jsonb default '[]'::jsonb;
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
</typedef>

<typedef class="org.hisp.dhis.hibernate.jsonb.type.JsonListBinaryType" name="jblSorting">
<param name="clazz">org.hisp.dhis.eventvisualization.Sorting</param>
<param name="clazz">org.hisp.dhis.analytics.Sorting</param>
</typedef>

<typedef class="org.hisp.dhis.hibernate.jsonb.type.JsonBinaryType" name="jblDashboardLayout">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,38 @@
*/
package org.hisp.dhis.webapi.controller;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.hisp.dhis.web.HttpStatus.CONFLICT;
import static org.hisp.dhis.web.HttpStatus.CREATED;
import static org.hisp.dhis.web.WebClientUtils.assertStatus;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.hisp.dhis.common.IdentifiableObjectManager;
import org.hisp.dhis.jsontree.JsonList;
import org.hisp.dhis.jsontree.JsonMixed;
import org.hisp.dhis.jsontree.JsonObject;
import org.hisp.dhis.program.Program;
import org.hisp.dhis.web.HttpStatus;
import org.hisp.dhis.web.WebClient;
import org.hisp.dhis.webapi.DhisControllerConvenienceTest;
import org.hisp.dhis.webapi.json.domain.JsonImportSummary;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

class VisualizationControllerTest extends DhisControllerConvenienceTest {

@Autowired private IdentifiableObjectManager manager;
private Program mockProgram;

@BeforeEach
public void beforeEach() {
mockProgram = createProgram('A');
manager.save(mockProgram);
}

@Test
void testGetVisualizationWithNestedFilters() {
JsonImportSummary report =
Expand Down Expand Up @@ -91,4 +109,160 @@ void testPostForInvalidOutlierMaxResults() {
"Allowed length range for property `maxResults` is [1 to 500], but given length was 501",
response.error(CONFLICT).getMessage());
}

@Test
void testPostInvalidSortingObject() {
// Given
String invalidDimension = "invalidOne";
String sorting = "'sorting': [{'dimension': '" + invalidDimension + "', 'direction':'ASC'}]";
String body =
"{'name': 'Name Test', 'type': 'STACKED_COLUMN', 'program': {'id':'"
+ mockProgram.getUid()
+ "'}, 'columns': [{'dimension': 'pe'}],"
+ sorting
+ "}";

// When
HttpResponse response = POST("/visualizations/", body);

// Then
assertEquals(
"Sorting dimension ‘" + invalidDimension + "’ is not a column",
response.error(CONFLICT).getMessage());
}

@Test
void testPostSortingObject() {
// Given
String dimension = "pe";
String sorting = "'sorting': [{'dimension': '" + dimension + "', 'direction':'ASC'}]";
String body =
"{'name': 'Name Test', 'type': 'STACKED_COLUMN', 'program': {'id':'"
+ mockProgram.getUid()
+ "'}, 'columns': [{'dimension': '"
+ dimension
+ "'}],"
+ sorting
+ "}";

// When
String uid = assertStatus(CREATED, POST("/visualizations/", body));

// Then
String getParams = "?fields=:all,columns[:all,items,sorting]";
JsonObject response = GET("/visualizations/" + uid + getParams).content();

assertThat(response.get("sorting").toString(), containsString("pe"));
assertThat(response.get("sorting").toString(), containsString("ASC"));
}

@Test
void testPostMultipleSortingObject() {
// Given
String dimension1 = "pe";
String dimension2 = "ou";
String sorting =
"'sorting': [{'dimension': '"
+ dimension1
+ "', 'direction':'ASC'},"
+ "{'dimension': '"
+ dimension2
+ "', 'direction':'DESC'}]";
String body =
"{'name': 'Name Test', 'type': 'STACKED_COLUMN', 'program': {'id':'"
+ mockProgram.getUid()
+ "'}, 'columns': [{'dimension': '"
+ dimension1
+ "'}, {'dimension': '"
+ dimension2
+ "'}],"
+ sorting
+ "}";

// When
String uid = assertStatus(CREATED, POST("/visualizations/", body));

// Then
String getParams = "?fields=:all,columns[:all,items,sorting]";
JsonObject response = GET("/visualizations/" + uid + getParams).content();

assertThat(response.get("sorting").toString(), containsString("pe"));
assertThat(response.get("sorting").toString(), containsString("ASC"));
assertThat(response.get("sorting").toString(), containsString("ou"));
assertThat(response.get("sorting").toString(), containsString("DESC"));
}

@Test
void testPostSortingObjectWithDuplication() {
// Given
String dimension = "pe";
String sorting =
"'sorting': [{'dimension': '"
+ dimension
+ "', 'direction':'ASC'},"
+ "{'dimension': '"
+ dimension
+ "', 'direction':'DESC'}]";
String body =
"{'name': 'Name Test', 'type': 'STACKED_COLUMN', 'program': {'id':'"
+ mockProgram.getUid()
+ "'}, 'columns': [{'dimension': '"
+ dimension
+ "'}],"
+ sorting
+ "}";

// When
String uid = assertStatus(CREATED, POST("/visualizations/", body));

// Then
String getParams = "?fields=:all,columns[:all,items,sorting]";
JsonObject response = GET("/visualizations/" + uid + getParams).content();

assertThat(response.get("sorting").toString(), containsString("pe"));
assertThat(response.get("sorting").toString(), containsString("ASC"));
assertThat(response.get("sorting").toString(), not(containsString("DESC")));
}

@Test
void testPostBlankSortingObject() {
// Given
String blankDimension = " ";
String sorting = "'sorting': [{'dimension': '" + blankDimension + "', 'direction':'ASC'}]";
String body =
"{'name': 'Name Test', 'type': 'STACKED_COLUMN', 'program': {'id':'"
+ mockProgram.getUid()
+ "'}, 'columns': [{'dimension': 'pe'}],"
+ sorting
+ "}";

// When
HttpResponse response = POST("/visualizations/", body);

// Then
assertEquals(
"Sorting must have a valid dimension and a direction",
response.error(CONFLICT).getMessage());
}

@Test
void testPostNullSortingObject() {
// Given
String blankDimension = " ";
String sorting = "'sorting': [{'dimension': '" + blankDimension + "', 'direction':'ASC'}]";
String body =
"{'name': 'Name Test', 'type': 'STACKED_COLUMN', 'program': {'id':'"
+ mockProgram.getUid()
+ "'}, 'columns': [{'dimension': 'pe'}],"
+ sorting
+ "}";

// When
HttpResponse response = POST("/visualizations/", body);

// Then
assertEquals(
"Sorting must have a valid dimension and a direction",
response.error(CONFLICT).getMessage());
}
}
Loading

0 comments on commit 202bb64

Please sign in to comment.