From 1f546d8c7d2674499ff4841a8d2f6e21f301a667 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 20 Sep 2022 12:19:19 -0500 Subject: [PATCH] QQQ-42 checkpoint of qqq reports --- .../reporting/ExcelExportStreamer.java | 9 +- .../actions/reporting/FormulaInterpreter.java | 122 ++++++++++++++++-- .../reporting/GenerateReportAction.java | 17 ++- .../core/instances/QInstanceEnricher.java | 15 ++- .../core/instances/QInstanceValidator.java | 13 +- .../model/metadata/layout/QAppSection.java | 38 +++++- .../reports/ExecuteReportStep.java | 8 +- .../reporting/FormulaInterpreterTest.java | 53 ++++++++ .../reporting/GenerateReportActionTest.java | 21 ++- .../instances/QInstanceValidatorTest.java | 24 ++-- .../QMetaDataVariableInterpreterTest.java | 23 ++++ .../reports/BasicRunReportProcessTest.java | 77 +++++++++++ .../javalin/QJavalinImplementationTest.java | 6 +- ...a => PersonsByCreateDateBarChartTest.java} | 4 +- 14 files changed, 375 insertions(+), 55 deletions(-) create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcessTest.java rename qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/{PersonsByCreateDateChartTestData.java => PersonsByCreateDateBarChartTest.java} (95%) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java index d34d4cbd..bb166c97 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java @@ -22,7 +22,6 @@ package com.kingsrook.qqq.backend.core.actions.reporting; -import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.time.LocalDate; @@ -67,7 +66,8 @@ public class ExcelExportStreamer implements ExportStreamerInterface private Workbook workbook; private Worksheet worksheet; - private int row = 0; + private int row = 0; + private int sheetCount = 0; @@ -112,6 +112,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface table = exportInput.getTable(); outputStream = this.exportInput.getReportOutputStream(); this.row = 0; + this.sheetCount++; if(workbook == null) { @@ -127,11 +128,11 @@ public class ExcelExportStreamer implements ExportStreamerInterface worksheet.finish(); } - worksheet = workbook.newWorksheet(label); + worksheet = workbook.newWorksheet(Objects.requireNonNullElse(label, "Sheet " + sheetCount)); writeReportHeaderRow(); } - catch(IOException e) + catch(Exception e) { throw (new QReportingException("Error starting worksheet", e)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java index d229e3b7..330b4fb9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java @@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QFormulaException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -154,17 +155,17 @@ public class FormulaInterpreter case "ADD": { List numbers = getNumberArgumentList(args, 2, variableInterpreter); - return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).add(numbers.get(1))); + return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).add(numbers.get(1))); } case "MINUS": { List numbers = getNumberArgumentList(args, 2, variableInterpreter); - return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).subtract(numbers.get(1))); + return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).subtract(numbers.get(1))); } case "MULTIPLY": { List numbers = getNumberArgumentList(args, 2, variableInterpreter); - return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).multiply(numbers.get(1))); + return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).multiply(numbers.get(1))); } case "DIVIDE": { @@ -173,7 +174,7 @@ public class FormulaInterpreter { return null; } - return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).divide(numbers.get(1), 4, RoundingMode.HALF_UP)); + return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).divide(numbers.get(1), 4, RoundingMode.HALF_UP)); } case "DIVIDE_SCALE": { @@ -182,23 +183,83 @@ public class FormulaInterpreter { return null; } - return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).divide(numbers.get(1), numbers.get(2).intValue(), RoundingMode.HALF_UP)); + return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).divide(numbers.get(1), numbers.get(2).intValue(), RoundingMode.HALF_UP)); } case "ROUND": { List numbers = getNumberArgumentList(args, 2, variableInterpreter); - return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).round(new MathContext(numbers.get(1).intValue()))); + return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).round(new MathContext(numbers.get(1).intValue()))); } case "SCALE": { List numbers = getNumberArgumentList(args, 2, variableInterpreter); - return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).setScale(numbers.get(1).intValue(), RoundingMode.HALF_UP)); + return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).setScale(numbers.get(1).intValue(), RoundingMode.HALF_UP)); } case "NVL": { List numbers = getNumberArgumentList(args, 2, variableInterpreter); return Objects.requireNonNullElse(numbers.get(0), numbers.get(1)); } + case "IF": + { + // IF(CONDITION,TRUE,ELSE) + List actualArgs = getArgumentList(args, 3, variableInterpreter); + Serializable condition = actualArgs.get(0); + boolean conditionBoolean; + if(condition == null) + { + conditionBoolean = false; + } + else if(condition instanceof Boolean b) + { + conditionBoolean = b; + } + else if(condition instanceof BigDecimal bd) + { + conditionBoolean = (bd.compareTo(BigDecimal.ZERO) != 0); + } + else if(condition instanceof String s) + { + if("true".equalsIgnoreCase(s)) + { + conditionBoolean = true; + } + else if("false".equalsIgnoreCase(s)) + { + conditionBoolean = false; + } + else + { + conditionBoolean = StringUtils.hasContent(s); + } + } + else + { + conditionBoolean = false; + } + + return conditionBoolean ? actualArgs.get(1) : actualArgs.get(2); + } + case "LT": + { + List numbers = getNumberArgumentList(args, 2, variableInterpreter); + return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) < 0); + } + case "LTE": + { + List numbers = getNumberArgumentList(args, 2, variableInterpreter); + return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) <= 0); + } + case "GT": + { + List numbers = getNumberArgumentList(args, 2, variableInterpreter); + return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) > 0); + } + case "GTE": + { + List numbers = getNumberArgumentList(args, 2, variableInterpreter); + return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) >= 0); + } default: { //////////////////////////////////////////////////////////////////////////////////////// @@ -235,7 +296,21 @@ public class FormulaInterpreter /******************************************************************************* ** *******************************************************************************/ - private static Serializable nullIfAnyNullArgsElse(List numbers, Supplier supplier) + private static Serializable nullIfAnyNullArgsElseBigDecimal(List numbers, Supplier supplier) + { + if(numbers.stream().anyMatch(Objects::isNull)) + { + return (null); + } + return supplier.get(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Serializable nullIfAnyNullArgsElseBoolean(List numbers, Supplier supplier) { if(numbers.stream().anyMatch(Objects::isNull)) { @@ -275,4 +350,35 @@ public class FormulaInterpreter return (rs); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private static List getArgumentList(List originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException + { + if(howMany != null) + { + if(!howMany.equals(originalArgs.size())) + { + throw (new QFormulaException("Wrong number of arguments (required: " + howMany + ", received: " + originalArgs.size() + ")")); + } + } + + List rs = new ArrayList<>(); + for(Serializable originalArg : originalArgs) + { + try + { + Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null); + rs.add(interpretedArg); + } + catch(QValueException e) + { + throw (new QFormulaException("Could not process [" + originalArg + "] as a number")); + } + } + return (rs); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index ccaf7590..3a5f4184 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -469,6 +469,9 @@ public class GenerateReportAction Map> fieldAggregates = entry.getValue(); variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(fieldAggregates)); + HashMap thisRowValues = new HashMap<>(); + variableInterpreter.addValueMap("thisRow", thisRowValues); + if(!variancePivotAggregates.isEmpty()) { Map>> varianceMap = variancePivotAggregates.getOrDefault(view.getName(), Collections.emptyMap()); @@ -503,6 +506,7 @@ public class GenerateReportAction { Serializable serializable = getValueForColumn(variableInterpreter, column); pivotRow.setValue(column.getName(), serializable); + thisRowValues.put(column.getName(), serializable); } } @@ -583,14 +587,17 @@ public class GenerateReportAction *******************************************************************************/ private Serializable getValueForColumn(QMetaDataVariableInterpreter variableInterpreter, QReportField column) throws QFormulaException { - String formula = column.getFormula(); - Serializable serializable = variableInterpreter.interpretForObject(formula); + String formula = column.getFormula(); + Serializable result; if(formula.startsWith("=") && formula.length() > 1) { - // serializable = interpretFormula(variableInterpreter, formula); - serializable = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1)); + result = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1)); } - return serializable; + else + { + result = variableInterpreter.interpretForObject(formula, null); + } + return (result); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 1acf60fc..6572d26b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -558,17 +558,17 @@ public class QInstanceEnricher ////////////////////////////////////////////////////////////////////////////// // create an identity section for the id and any fields in the record label // ////////////////////////////////////////////////////////////////////////////// - QAppSection defaultSection = new QAppSection(app.getName(), app.getLabel(), new QIcon("badge"), new ArrayList<>(), new ArrayList<>()); + QAppSection defaultSection = new QAppSection(app.getName(), app.getLabel(), new QIcon("badge"), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); boolean foundNonAppChild = false; if(CollectionUtils.nullSafeHasContents(app.getChildren())) { for(QAppChildMetaData child : app.getChildren()) { - //////////////////////////////////////////////////////////////////////////////// - // only tables and processes are allowed to be in sections at this time, apps // - // might be children but not in sections so keep track if we find any non-app // - //////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////// + // only tables, processes, and reports are allowed to be in sections at this time, apps // + // might be children but not in sections so keep track if we find any non-app // + ////////////////////////////////////////////////////////////////////////////////////////// if(child.getClass().equals(QTableMetaData.class)) { defaultSection.getTables().add(child.getName()); @@ -579,6 +579,11 @@ public class QInstanceEnricher defaultSection.getProcesses().add(child.getName()); foundNonAppChild = true; } + else if(child.getClass().equals(QReportMetaData.class)) + { + defaultSection.getReports().add(child.getName()); + foundNonAppChild = true; + } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 92c15190..653ce4b8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -510,7 +510,8 @@ public class QInstanceValidator assertCondition(StringUtils.hasContent(section.getLabel()), "Missing a label for a section in app " + app.getLabel() + "."); boolean hasTables = CollectionUtils.nullSafeHasContents(section.getTables()); boolean hasProcesses = CollectionUtils.nullSafeHasContents(section.getProcesses()); - if(assertCondition(hasTables || hasProcesses, "App " + app.getName() + " section " + section.getName() + " does not have any children.")) + boolean hasReports = CollectionUtils.nullSafeHasContents(section.getReports()); + if(assertCondition(hasTables || hasProcesses || hasReports, "App " + app.getName() + " section " + section.getName() + " does not have any children.")) { if(hasTables) { @@ -532,6 +533,16 @@ public class QInstanceValidator childNamesInSections.add(processName); } } + if(hasReports) + { + for(String reportName : section.getReports()) + { + assertCondition(app.getChildren().stream().anyMatch(c -> c.getName().equals(reportName)), "App " + app.getName() + " section " + section.getName() + " specifies report " + reportName + ", which is not a child of this app."); + assertCondition(!childNamesInSections.contains(reportName), "App " + app.getName() + " has report " + reportName + " listed more than once in its sections."); + + childNamesInSections.add(reportName); + } + } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java index d2d80340..461871b0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java @@ -36,6 +36,7 @@ public class QAppSection private List tables; private List processes; + private List reports; @@ -51,13 +52,14 @@ public class QAppSection /******************************************************************************* ** *******************************************************************************/ - public QAppSection(String name, String label, QIcon icon, List tables, List processes) + public QAppSection(String name, String label, QIcon icon, List tables, List processes, List reports) { this.name = name; this.label = label; this.icon = icon; this.tables = tables; this.processes = processes; + this.reports = reports; } @@ -198,6 +200,40 @@ public class QAppSection + /******************************************************************************* + ** Getter for reports + ** + *******************************************************************************/ + public List getReports() + { + return reports; + } + + + + /******************************************************************************* + ** Setter for reports + ** + *******************************************************************************/ + public void setReports(List reports) + { + this.reports = reports; + } + + + + /******************************************************************************* + ** Fluent setter for reports + ** + *******************************************************************************/ + public QAppSection withReports(List reports) + { + this.reports = reports; + return (this); + } + + + /******************************************************************************* ** Getter for icon ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java index f36d0553..40525957 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java @@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; /******************************************************************************* @@ -50,8 +51,9 @@ public class ExecuteReportStep implements BackendStep { try { - String reportName = runBackendStepInput.getValueString("reportName"); - File tmpFile = File.createTempFile(reportName, ".xlsx", new File("/tmp/")); + String reportName = runBackendStepInput.getValueString("reportName"); + QReportMetaData report = runBackendStepInput.getInstance().getReport(reportName); + File tmpFile = File.createTempFile(reportName, ".xlsx", new File("/tmp/")); runBackendStepInput.getAsyncJobCallback().updateStatus("Generating Report"); @@ -71,7 +73,7 @@ public class ExecuteReportStep implements BackendStep DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmm").withZone(ZoneId.systemDefault()); String datePart = formatter.format(Instant.now()); - runBackendStepOutput.addValue("downloadFileName", reportName + "-" + datePart + ".xlsx"); + runBackendStepOutput.addValue("downloadFileName", report.getLabel() + " " + datePart + ".xlsx"); runBackendStepOutput.addValue("serverFilePath", tmpFile.getCanonicalPath()); } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java index 03bd10a7..0758b2c8 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java @@ -32,7 +32,9 @@ import static com.kingsrook.qqq.backend.core.actions.reporting.FormulaInterprete import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* @@ -140,4 +142,55 @@ class FormulaInterpreterTest assertEquals(new BigDecimal("27.78"), interpretFormula(vi, "SCALE(MULTIPLY(100,DIVIDE_SCALE(${pivot.sum.noOfShoes},${total.sum.noOfShoes},6)),2)")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testComparisons() throws QFormulaException + { + QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter(); + vi.addValueMap("input", Map.of("one", 1, "two", 2, "foo", "bar")); + + assertTrue((Boolean) interpretFormula(vi, "LT(${input.one},${input.two})")); + assertFalse((Boolean) interpretFormula(vi, "LT(${input.two},${input.one})")); + + assertFalse((Boolean) interpretFormula(vi, "GT(${input.one},${input.two})")); + assertTrue((Boolean) interpretFormula(vi, "GT(${input.two},${input.one})")); + + assertTrue((Boolean) interpretFormula(vi, "LTE(${input.one},${input.two})")); + assertTrue((Boolean) interpretFormula(vi, "LTE(${input.one},${input.one})")); + assertFalse((Boolean) interpretFormula(vi, "LTE(${input.two},${input.one})")); + + assertFalse((Boolean) interpretFormula(vi, "GTE(${input.one},${input.two})")); + assertTrue((Boolean) interpretFormula(vi, "GTE(${input.one},${input.one})")); + assertTrue((Boolean) interpretFormula(vi, "GTE(${input.two},${input.one})")); + + // todo - google sheets compares strings differently... + assertThatThrownBy(() -> interpretFormula(vi, "LT(${input.foo},${input.one})")).hasMessageContaining("[bar] as a number"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConditionals() throws QFormulaException + { + QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter(); + vi.addValueMap("input", Map.of("one", 1, "two", 2, "three", 3, "foo", "bar")); + + assertEquals("A", interpretFormula(vi, "IF(LT(${input.one},${input.two}),A,B)")); + assertEquals("B", interpretFormula(vi, "IF(GT(${input.one},${input.two}),A,B)")); + + assertEquals("C", interpretFormula(vi, "IF(GT(${input.one},${input.two}),A,IF(GT(${input.two},${input.three}),B,C))")); + assertEquals("B", interpretFormula(vi, "IF(GT(${input.one},${input.two}),A,IF(LT(${input.two},${input.three}),B,C))")); + assertEquals("A", interpretFormula(vi, "IF(GT(${input.two},${input.one}),A,IF(LT(${input.two},${input.three}),B,C))")); + + assertEquals("Yes", interpretFormula(vi, "IF(GT(${input.one},0),Yes,No)")); + assertEquals("No", interpretFormula(vi, "IF(LT(${input.one},0),Yes,No)")); + } + } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java index 4dce7cf6..f6ba54cc 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java @@ -59,7 +59,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /******************************************************************************* ** Unit test for GenerateReportAction *******************************************************************************/ -class GenerateReportActionTest +public class GenerateReportActionTest { private static final String REPORT_NAME = "personReport1"; @@ -243,32 +243,32 @@ class GenerateReportActionTest Map row = iterator.next(); assertEquals(6, list.size()); - assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Home State Id")).isEqualTo("IL"); assertThat(row.get("Last Name")).isEqualTo("Jonson"); assertThat(row.get("Quantity")).isNull(); row = iterator.next(); - assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Home State Id")).isEqualTo("IL"); assertThat(row.get("Last Name")).isEqualTo("Jones"); assertThat(row.get("Quantity")).isEqualTo("3"); row = iterator.next(); - assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Home State Id")).isEqualTo("IL"); assertThat(row.get("Last Name")).isEqualTo("Kelly"); assertThat(row.get("Quantity")).isEqualTo("4"); row = iterator.next(); - assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Home State Id")).isEqualTo("IL"); assertThat(row.get("Last Name")).isEqualTo("Keller"); assertThat(row.get("Quantity")).isEqualTo("5"); row = iterator.next(); - assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Home State Id")).isEqualTo("IL"); assertThat(row.get("Last Name")).isEqualTo("Kelkhoff"); assertThat(row.get("Quantity")).isEqualTo("6"); row = iterator.next(); - assertThat(row.get("Home State Id")).isEqualTo("2"); + assertThat(row.get("Home State Id")).isEqualTo("MO"); assertThat(row.get("Last Name")).isEqualTo("Kelkhoff"); assertThat(row.get("Quantity")).isEqualTo("7"); } @@ -297,15 +297,14 @@ class GenerateReportActionTest Iterator> iterator = list.iterator(); Map row = iterator.next(); assertEquals(2, list.size()); - assertThat(row.get("Home State Id")).isEqualTo("2"); + assertThat(row.get("Home State Id")).isEqualTo("MO"); assertThat(row.get("Last Name")).isNull(); assertThat(row.get("Quantity")).isEqualTo("7"); row = iterator.next(); - assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Home State Id")).isEqualTo("IL"); assertThat(row.get("Last Name")).isNull(); assertThat(row.get("Quantity")).isEqualTo("18"); - } @@ -398,7 +397,7 @@ class GenerateReportActionTest /******************************************************************************* ** *******************************************************************************/ - private QReportMetaData defineReport(boolean includeTotalRow) + public static QReportMetaData defineReport(boolean includeTotalRow) { return new QReportMetaData() .withName(REPORT_NAME) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index d3397d5f..9552d074 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -584,7 +584,7 @@ class QInstanceValidatorTest { QAppMetaData app = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection(null, "Section 1", new QIcon("person"), List.of("test"), null)); + .withSection(new QAppSection(null, "Section 1", new QIcon("person"), List.of("test"), null, null)); assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "Missing a name"); } @@ -598,7 +598,7 @@ class QInstanceValidatorTest { QAppMetaData app = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection("Section 1", null, new QIcon("person"), List.of("test"), null)); + .withSection(new QAppSection("Section 1", null, new QIcon("person"), List.of("test"), null, null)); assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "Missing a label"); } @@ -612,12 +612,12 @@ class QInstanceValidatorTest { QAppMetaData app1 = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of(), List.of())); + .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of(), List.of(), null)); assertValidationFailureReasons((qInstance) -> qInstance.addApp(app1), "section1 does not have any children", "child test is not listed in any app sections"); QAppMetaData app2 = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), null, null)); + .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), null, null, null)); assertValidationFailureReasons((qInstance) -> qInstance.addApp(app2), "section1 does not have any children", "child test is not listed in any app sections"); } @@ -631,11 +631,11 @@ class QInstanceValidatorTest { QAppMetaData app1 = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test", "tset"), null)); + .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test", "tset"), null, null)); assertValidationFailureReasons((qInstance) -> qInstance.addApp(app1), "not a child of this app"); QAppMetaData app2 = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), List.of("tset"))); + .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), List.of("tset"), null)); assertValidationFailureReasons((qInstance) -> qInstance.addApp(app2), "not a child of this app"); } @@ -649,23 +649,23 @@ class QInstanceValidatorTest { QAppMetaData app1 = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test", "test"), null)); + .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test", "test"), null, null)); assertValidationFailureReasons((qInstance) -> qInstance.addApp(app1), "more than once"); QAppMetaData app2 = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), null)) - .withSection(new QAppSection("section2", "Section 2", new QIcon("person"), List.of("test"), null)); + .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), null, null)) + .withSection(new QAppSection("section2", "Section 2", new QIcon("person"), List.of("test"), null, null)); assertValidationFailureReasons((qInstance) -> qInstance.addApp(app2), "more than once"); QAppMetaData app3 = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), List.of("test"))); + .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), List.of("test"), null)); assertValidationFailureReasons((qInstance) -> qInstance.addApp(app3), "more than once"); QAppMetaData app4 = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), null, List.of("test", "test"))); + .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), null, List.of("test", "test"), null)); assertValidationFailureReasons((qInstance) -> qInstance.addApp(app4), "more than once"); } @@ -687,7 +687,7 @@ class QInstanceValidatorTest QAppMetaData app = new QAppMetaData().withName("test") .withChild(new QTableMetaData().withName("tset")) .withChild(new QTableMetaData().withName("test")) - .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), null)); + .withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), null, null)); assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "not listed in any app sections"); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java index 8b61f22e..3255bcec 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java @@ -200,6 +200,29 @@ class QMetaDataVariableInterpreterTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testLooksLikeVariableButNotFound() + { + QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter(); + variableInterpreter.addValueMap("input", Map.of("x", 1, "y", 2)); + variableInterpreter.addValueMap("others", Map.of("foo", "bar")); + + assertNull(variableInterpreter.interpretForObject("${input.notFound}", null)); + assertEquals(0, variableInterpreter.interpretForObject("${input.notFound}", 0)); + assertEquals("--", variableInterpreter.interpretForObject("${input.notFound}", "--")); + assertEquals("--", variableInterpreter.interpretForObject("${others.notFound}", "--")); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this one doesn't count as "looking like a variable" - because the "prefix" (notValid) isn't a value map... // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertEquals("${notValid.notFound}", variableInterpreter.interpretForObject("${notValid.notFound}", "--")); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcessTest.java new file mode 100644 index 00000000..2cd859b3 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcessTest.java @@ -0,0 +1,77 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.reports; + + +import java.time.LocalDate; +import java.time.Month; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportActionTest; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +/******************************************************************************* + ** Unit test for BasicRunReportProcess + *******************************************************************************/ +class BasicRunReportProcessTest +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRunReport() throws QException + { + QInstance instance = TestUtils.defineInstance(); + QReportMetaData report = GenerateReportActionTest.defineReport(true); + QProcessMetaData runReportProcess = BasicRunReportProcess.defineProcessMetaData(); + + instance.addReport(report); + report.setProcessName(runReportProcess.getName()); + instance.addProcess(runReportProcess); + + RunProcessInput runProcessInput = new RunProcessInput(instance); + runProcessInput.setSession(TestUtils.getMockSession()); + runProcessInput.setProcessName(report.getProcessName()); + runProcessInput.addValue(BasicRunReportProcess.FIELD_REPORT_NAME, report.getName()); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + String processUUID = runProcessOutput.getProcessUUID(); + assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo(BasicRunReportProcess.STEP_NAME_INPUT); + + runProcessInput.addValue("startDate", LocalDate.of(1980, Month.JANUARY, 1)); + runProcessInput.addValue("endDate", LocalDate.of(2099, Month.DECEMBER, 31)); + runProcessInput.setStartAfterStep(BasicRunReportProcess.STEP_NAME_INPUT); + runProcessInput.setProcessUUID(processUUID); + + runProcessOutput = new RunProcessAction().execute(runProcessInput); + assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo(BasicRunReportProcess.STEP_NAME_ACCESS); + assertThat(runProcessOutput.getValues()).containsKeys("downloadFileName", "serverFilePath"); + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 76b660d7..fc0918b0 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -451,8 +451,8 @@ class QJavalinImplementationTest extends QJavalinTestBase @Test void testExportFieldsQueryParam() { - HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/People.csv?fields=id,birthDate").asString(); - String[] csvLines = response.getBody().split("\n"); + HttpResponse response = Unirest.get(BASE_URL + "/data/person/export/People.csv?fields=id,birthDate").asString(); + String[] csvLines = response.getBody().split("\n"); assertEquals(""" "Id","Birth Date\"""", csvLines[0]); } @@ -484,7 +484,7 @@ class QJavalinImplementationTest extends QJavalinTestBase assertNotNull(jsonObject); assertEquals("barChart", jsonObject.getString("type")); assertNotNull(jsonObject.getString("title")); - assertNotNull(jsonObject.getJSONObject("barChartData")); + assertNotNull(jsonObject.getJSONObject("chartData")); } } diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateChartTestData.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateBarChartTest.java similarity index 95% rename from qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateChartTestData.java rename to qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateBarChartTest.java index f70ca51d..e15a96e3 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateChartTestData.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateBarChartTest.java @@ -35,7 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; /******************************************************************************* ** Unit test for PersonsByCreateDateBarChart *******************************************************************************/ -class PersonsByCreateDateChartTestData +class PersonsByCreateDateBarChartTest { /******************************************************************************* @@ -47,7 +47,7 @@ class PersonsByCreateDateChartTestData Object widgetData = new PersonsByCreateDateBarChart().render(SampleMetaDataProvider.defineInstance(), new QSession(), null); assertThat(widgetData).isInstanceOf(ChartData.class); ChartData chartData = (ChartData) widgetData; - assertEquals("chartData", chartData.getType()); + assertEquals("barChart", chartData.getType()); assertThat(chartData.getTitle()).isNotBlank(); assertNotNull(chartData.getChartData()); }