TSK-1741: WorkbasketPriorityReport now accepts column headers as request params

This commit is contained in:
Mustapha Zorgati 2021-09-29 01:16:30 +02:00
parent ea864474ad
commit 4ae94ff108
10 changed files with 253 additions and 36 deletions

View File

@ -1,7 +1,6 @@
package pro.taskana.common.test.rest; package pro.taskana.common.test.rest;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
@ -16,7 +15,6 @@ import org.springframework.test.context.ActiveProfiles;
// DirtiesContext is required to make the integration tests run with embedded LDAP. // DirtiesContext is required to make the integration tests run with embedded LDAP.
// Otherwise the LDAP server is not shut down correctly and will not come up again. (socket busy) // Otherwise the LDAP server is not shut down correctly and will not come up again. (socket busy)
@DirtiesContext(classMode = ClassMode.AFTER_CLASS) @DirtiesContext(classMode = ClassMode.AFTER_CLASS)
@Inherited
@ActiveProfiles({"test"}) @ActiveProfiles({"test"})
@SpringBootTest( @SpringBootTest(
classes = TestConfiguration.class, classes = TestConfiguration.class,

View File

@ -27,4 +27,12 @@ public class PriorityColumnHeader implements ColumnHeader<PriorityQueryItem> {
public boolean fits(PriorityQueryItem item) { public boolean fits(PriorityQueryItem item) {
return lowerBoundInc <= item.getPriority() && upperBoundInc >= item.getPriority(); return lowerBoundInc <= item.getPriority() && upperBoundInc >= item.getPriority();
} }
public int getLowerBoundInc() {
return lowerBoundInc;
}
public int getUpperBoundInc() {
return upperBoundInc;
}
} }

View File

@ -0,0 +1,27 @@
package pro.taskana.common.rest;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.beans.PropertyEditorSupport;
import java.net.URLDecoder;
public class JsonPropertyEditor extends PropertyEditorSupport {
private final ObjectMapper objectMapper;
private final Class<?> requiredType;
public JsonPropertyEditor(ObjectMapper objectMapper, Class<?> requiredType) {
this.objectMapper = objectMapper;
this.requiredType = requiredType;
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
if (text != null && !text.isEmpty()) {
try {
setValue(objectMapper.readValue(URLDecoder.decode(text, "UTF-8"), requiredType));
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
}
}

View File

@ -0,0 +1,27 @@
package pro.taskana.common.rest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;
import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel;
@ControllerAdvice
public class JsonPropertyEditorRegistrator {
private final ObjectMapper objectMapper;
@Autowired
public JsonPropertyEditorRegistrator(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(
PriorityColumnHeaderRepresentationModel.class,
new JsonPropertyEditor(objectMapper, PriorityColumnHeaderRepresentationModel.class));
}
}

View File

@ -3,6 +3,7 @@ package pro.taskana.monitor.rest;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -24,7 +25,9 @@ import pro.taskana.monitor.api.reports.TimestampReport;
import pro.taskana.monitor.api.reports.WorkbasketPriorityReport; import pro.taskana.monitor.api.reports.WorkbasketPriorityReport;
import pro.taskana.monitor.api.reports.WorkbasketReport; import pro.taskana.monitor.api.reports.WorkbasketReport;
import pro.taskana.monitor.api.reports.header.PriorityColumnHeader; import pro.taskana.monitor.api.reports.header.PriorityColumnHeader;
import pro.taskana.monitor.rest.assembler.PriorityColumnHeaderRepresentationModelAssembler;
import pro.taskana.monitor.rest.assembler.ReportRepresentationModelAssembler; import pro.taskana.monitor.rest.assembler.ReportRepresentationModelAssembler;
import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel;
import pro.taskana.monitor.rest.models.ReportRepresentationModel; import pro.taskana.monitor.rest.models.ReportRepresentationModel;
import pro.taskana.task.api.TaskCustomField; import pro.taskana.task.api.TaskCustomField;
import pro.taskana.task.api.TaskState; import pro.taskana.task.api.TaskState;
@ -38,13 +41,19 @@ public class MonitorController {
private final MonitorService monitorService; private final MonitorService monitorService;
private final ReportRepresentationModelAssembler reportRepresentationModelAssembler; private final ReportRepresentationModelAssembler reportRepresentationModelAssembler;
private final PriorityColumnHeaderRepresentationModelAssembler
priorityColumnHeaderRepresentationModelAssembler;
@Autowired @Autowired
MonitorController( MonitorController(
MonitorService monitorService, MonitorService monitorService,
ReportRepresentationModelAssembler reportRepresentationModelAssembler) { ReportRepresentationModelAssembler reportRepresentationModelAssembler,
PriorityColumnHeaderRepresentationModelAssembler
priorityColumnHeaderRepresentationModelAssembler) {
this.monitorService = monitorService; this.monitorService = monitorService;
this.reportRepresentationModelAssembler = reportRepresentationModelAssembler; this.reportRepresentationModelAssembler = reportRepresentationModelAssembler;
this.priorityColumnHeaderRepresentationModelAssembler =
priorityColumnHeaderRepresentationModelAssembler;
} }
/** /**
@ -82,23 +91,15 @@ public class MonitorController {
} }
/** /**
* This endpoint generates a Workbasket Report by priority ranges. * This endpoint generates a Workbasket Priority Report.
* *
* <p>Each Row represents a Workbasket. * <p>Each Row represents a Workbasket.
* *
* <p>Each Column Header represents a priority range. <br> * <p>Each Column Header represents a priority range.
* <br>
*
* <p><b>Default ranges</b>
*
* <p>High: priority &gt; 500
*
* <p>Medium: 250 &ge; priority &le; 500
*
* <p>Low: priority &lt; 250
* *
* @title Compute a Workbasket Priority Report * @title Compute a Workbasket Priority Report
* @param workbasketTypes determine the WorkbasketTypes to include in the report * @param workbasketTypes determine the WorkbasketTypes to include in the report
* @param columnHeaders the column headers for the report
* @return the computed Report * @return the computed Report
* @throws NotAuthorizedException if the current user is not authorized to compute the Report * @throws NotAuthorizedException if the current user is not authorized to compute the Report
* @throws InvalidArgumentException if topicWorkbaskets or useDefaultValues are false * @throws InvalidArgumentException if topicWorkbaskets or useDefaultValues are false
@ -106,21 +107,25 @@ public class MonitorController {
@GetMapping(path = RestEndpoints.URL_MONITOR_WORKBASKET_PRIORITY_REPORT) @GetMapping(path = RestEndpoints.URL_MONITOR_WORKBASKET_PRIORITY_REPORT)
@Transactional(readOnly = true, rollbackFor = Exception.class) @Transactional(readOnly = true, rollbackFor = Exception.class)
public ResponseEntity<ReportRepresentationModel> computePriorityWorkbasketReport( public ResponseEntity<ReportRepresentationModel> computePriorityWorkbasketReport(
@RequestParam(name = "workbasket-type", required = false) WorkbasketType[] workbasketTypes) @RequestParam(name = "workbasket-type", required = false) WorkbasketType[] workbasketTypes,
@RequestParam(name = "columnHeader", required = false)
PriorityColumnHeaderRepresentationModel[] columnHeaders)
throws NotAuthorizedException, InvalidArgumentException { throws NotAuthorizedException, InvalidArgumentException {
WorkbasketPriorityReport.Builder builder = WorkbasketPriorityReport.Builder builder =
monitorService monitorService.createWorkbasketPriorityReportBuilder().workbasketTypeIn(workbasketTypes);
.createWorkbasketPriorityReportBuilder()
.withColumnHeaders( if (columnHeaders != null) {
Arrays.asList( List<PriorityColumnHeader> priorityColumnHeaders =
new PriorityColumnHeader(Integer.MIN_VALUE, 249), Arrays.stream(columnHeaders)
new PriorityColumnHeader(250, 500), .map(priorityColumnHeaderRepresentationModelAssembler::toEntityModel)
new PriorityColumnHeader(501, Integer.MAX_VALUE))) .collect(Collectors.toList());
.workbasketTypeIn(workbasketTypes); builder.withColumnHeaders(priorityColumnHeaders);
}
ReportRepresentationModel report = ReportRepresentationModel report =
reportRepresentationModelAssembler.toModel(builder.buildReport(), workbasketTypes); reportRepresentationModelAssembler.toModel(
builder.buildReport(), workbasketTypes, columnHeaders);
return ResponseEntity.status(HttpStatus.OK).body(report); return ResponseEntity.status(HttpStatus.OK).body(report);
} }

View File

@ -0,0 +1,25 @@
package pro.taskana.monitor.rest.assembler;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import pro.taskana.monitor.api.reports.header.PriorityColumnHeader;
import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel;
@Component
public class PriorityColumnHeaderRepresentationModelAssembler
implements RepresentationModelAssembler<
PriorityColumnHeader, PriorityColumnHeaderRepresentationModel> {
@Override
@NonNull
public PriorityColumnHeaderRepresentationModel toModel(@NonNull PriorityColumnHeader entity) {
return new PriorityColumnHeaderRepresentationModel(
entity.getLowerBoundInc(), entity.getUpperBoundInc());
}
public PriorityColumnHeader toEntityModel(PriorityColumnHeaderRepresentationModel repModel) {
return new PriorityColumnHeader(repModel.getLowerBound(), repModel.getUpperBound());
}
}

View File

@ -32,6 +32,7 @@ import pro.taskana.monitor.api.reports.row.Row;
import pro.taskana.monitor.api.reports.row.SingleRow; import pro.taskana.monitor.api.reports.row.SingleRow;
import pro.taskana.monitor.rest.MonitorController; import pro.taskana.monitor.rest.MonitorController;
import pro.taskana.monitor.rest.TimeIntervalReportFilterParameter; import pro.taskana.monitor.rest.TimeIntervalReportFilterParameter;
import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel;
import pro.taskana.monitor.rest.models.ReportRepresentationModel; import pro.taskana.monitor.rest.models.ReportRepresentationModel;
import pro.taskana.monitor.rest.models.ReportRepresentationModel.RowRepresentationModel; import pro.taskana.monitor.rest.models.ReportRepresentationModel.RowRepresentationModel;
import pro.taskana.task.api.TaskCustomField; import pro.taskana.task.api.TaskCustomField;
@ -59,11 +60,15 @@ public class ReportRepresentationModelAssembler {
@NonNull @NonNull
public ReportRepresentationModel toModel( public ReportRepresentationModel toModel(
@NonNull WorkbasketPriorityReport report, @NonNull WorkbasketType[] workbasketTypes) @NonNull WorkbasketPriorityReport report,
throws NotAuthorizedException, InvalidArgumentException { WorkbasketType[] workbasketTypes,
PriorityColumnHeaderRepresentationModel[] columnHeaders)
throws InvalidArgumentException, NotAuthorizedException {
ReportRepresentationModel resource = toReportResource(report); ReportRepresentationModel resource = toReportResource(report);
resource.add( resource.add(
linkTo(methodOn(MonitorController.class).computePriorityWorkbasketReport(workbasketTypes)) linkTo(
methodOn(MonitorController.class)
.computePriorityWorkbasketReport(workbasketTypes, columnHeaders))
.withSelfRel()); .withSelfRel());
return resource; return resource;
} }

View File

@ -0,0 +1,30 @@
package pro.taskana.monitor.rest.models;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.beans.ConstructorProperties;
import org.springframework.hateoas.RepresentationModel;
@JsonIgnoreProperties("links")
public class PriorityColumnHeaderRepresentationModel
extends RepresentationModel<PriorityColumnHeaderRepresentationModel> {
/** Determine the lower priority for this column header. This value is inclusive. */
private final int lowerBound;
/** Determine the upper priority for this column header. This value is inclusive. */
private final int upperBound;
@ConstructorProperties({"lowerBound", "upperBound"})
public PriorityColumnHeaderRepresentationModel(int lowerBound, int upperBound) {
this.lowerBound = lowerBound;
this.upperBound = upperBound;
}
public int getLowerBound() {
return lowerBound;
}
public int getUpperBound() {
return upperBound;
}
}

View File

@ -1,8 +1,13 @@
package pro.taskana.monitor.rest; package pro.taskana.monitor.rest;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static pro.taskana.common.test.rest.RestHelper.TEMPLATE; import static pro.taskana.common.test.rest.RestHelper.TEMPLATE;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
@ -10,11 +15,12 @@ import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException.BadRequest;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.rest.RestEndpoints; import pro.taskana.common.rest.RestEndpoints;
import pro.taskana.common.test.rest.RestHelper; import pro.taskana.common.test.rest.RestHelper;
import pro.taskana.common.test.rest.TaskanaSpringBootTest; import pro.taskana.common.test.rest.TaskanaSpringBootTest;
import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel;
import pro.taskana.monitor.rest.models.ReportRepresentationModel; import pro.taskana.monitor.rest.models.ReportRepresentationModel;
import pro.taskana.monitor.rest.models.ReportRepresentationModel.RowRepresentationModel; import pro.taskana.monitor.rest.models.ReportRepresentationModel.RowRepresentationModel;
@ -23,12 +29,12 @@ import pro.taskana.monitor.rest.models.ReportRepresentationModel.RowRepresentati
class MonitorControllerIntTest { class MonitorControllerIntTest {
private final RestHelper restHelper; private final RestHelper restHelper;
private final TaskanaEngine taskanaEngine; private final ObjectMapper objectMapper;
@Autowired @Autowired
MonitorControllerIntTest(RestHelper restHelper, TaskanaEngine taskanaEngine) { MonitorControllerIntTest(RestHelper restHelper, ObjectMapper objectMapper) {
this.restHelper = restHelper; this.restHelper = restHelper;
this.taskanaEngine = taskanaEngine; this.objectMapper = objectMapper;
} }
@Test @Test
@ -106,10 +112,10 @@ class MonitorControllerIntTest {
} }
@Test @Test
void should_ComputeReport_When_QueryingForAWorkbasketPriorityReport() { void should_ComputeWorkbasketPriorityReport_When_QueryingForAWorkbasketPriorityReport() {
String url = String url =
restHelper.toUrl(RestEndpoints.URL_MONITOR_WORKBASKET_PRIORITY_REPORT) restHelper.toUrl(RestEndpoints.URL_MONITOR_WORKBASKET_PRIORITY_REPORT)
+ "?workbasket-type=TOPIC,GROUP"; + "?workbasket-type=TOPIC&workbasket-type=GROUP";
HttpEntity<?> auth = new HttpEntity<>(RestHelper.generateHeadersForUser("monitor")); HttpEntity<?> auth = new HttpEntity<>(RestHelper.generateHeadersForUser("monitor"));
ResponseEntity<ReportRepresentationModel> response = ResponseEntity<ReportRepresentationModel> response =
@ -123,8 +129,50 @@ class MonitorControllerIntTest {
assertThat(report).isNotNull(); assertThat(report).isNotNull();
assertThat(report.getSumRow()) assertThat(report.getSumRow()).extracting(RowRepresentationModel::getTotal).containsExactly(26);
.extracting(RowRepresentationModel::getCells) }
.containsExactly(new int[] {25, 1, 0});
@Test
void should_DetectPriorityColumnHeader_When_HeaderIsPassedAsQueryParameter() throws Exception {
PriorityColumnHeaderRepresentationModel columnHeader =
new PriorityColumnHeaderRepresentationModel(10, 20);
String url =
restHelper.toUrl(RestEndpoints.URL_MONITOR_WORKBASKET_PRIORITY_REPORT)
+ "?columnHeader="
+ URLEncoder.encode(
objectMapper.writeValueAsString(columnHeader), StandardCharsets.UTF_8);
HttpEntity<?> auth = new HttpEntity<>(RestHelper.generateHeadersForUser("monitor"));
ResponseEntity<ReportRepresentationModel> response =
TEMPLATE.exchange(
url,
HttpMethod.GET,
auth,
ParameterizedTypeReference.forType(ReportRepresentationModel.class));
ReportRepresentationModel report = response.getBody();
assertThat(report).isNotNull();
assertThat(report.getMeta().getHeader()).containsExactly("10 - 20");
}
@Test
void should_ReturnBadRequest_When_PriorityColumnHeaderIsNotAValidJson() {
String url =
restHelper.toUrl(RestEndpoints.URL_MONITOR_WORKBASKET_PRIORITY_REPORT)
+ "?columnHeader=invalidJson";
HttpEntity<?> auth = new HttpEntity<>(RestHelper.generateHeadersForUser("monitor"));
ThrowingCallable httpCall =
() ->
TEMPLATE.exchange(
url,
HttpMethod.GET,
auth,
ParameterizedTypeReference.forType(ReportRepresentationModel.class));
assertThatThrownBy(httpCall).isInstanceOf(BadRequest.class);
} }
} }

View File

@ -0,0 +1,44 @@
package pro.taskana.monitor.rest.assembler;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import pro.taskana.common.test.rest.TaskanaSpringBootTest;
import pro.taskana.monitor.api.reports.header.PriorityColumnHeader;
import pro.taskana.monitor.rest.models.PriorityColumnHeaderRepresentationModel;
@TaskanaSpringBootTest
class PriorityColumnHeaderRepresentationModelAssemblerTest {
private final PriorityColumnHeaderRepresentationModelAssembler assembler;
@Autowired
public PriorityColumnHeaderRepresentationModelAssemblerTest(
PriorityColumnHeaderRepresentationModelAssembler assembler) {
this.assembler = assembler;
}
@Test
void should_convertEntityToRepresentationModel() {
PriorityColumnHeader columnHeader = new PriorityColumnHeader(10, 20);
PriorityColumnHeaderRepresentationModel expectedRepModel =
new PriorityColumnHeaderRepresentationModel(10, 20);
PriorityColumnHeaderRepresentationModel repModel = assembler.toModel(columnHeader);
assertThat(repModel).usingRecursiveComparison().isEqualTo(expectedRepModel);
}
@Test
void should_convertRepresentationModelToEntity() {
PriorityColumnHeaderRepresentationModel repModel =
new PriorityColumnHeaderRepresentationModel(10, 20);
PriorityColumnHeader expectedColumnHeader = new PriorityColumnHeader(10, 20);
PriorityColumnHeader columnHeader = assembler.toEntityModel(repModel);
assertThat(columnHeader).usingRecursiveComparison().isEqualTo(expectedColumnHeader);
}
}