From 80ff7a26e040dac9f4168291102aede6220e8fe7 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Fri, 16 Sep 2022 11:01:26 -0500 Subject: [PATCH 1/3] QQQ-41: fixed failing test --- .../backend/core/actions/dashboard/WidgetDataLoaderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/WidgetDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/WidgetDataLoaderTest.java index ad7fb9b5..73cc29d2 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/WidgetDataLoaderTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/WidgetDataLoaderTest.java @@ -45,7 +45,7 @@ class WidgetDataLoaderTest Object widgetData = new WidgetDataLoader().execute(TestUtils.defineInstance(), TestUtils.getMockSession(), PersonsByCreateDateBarChart.class.getSimpleName()); assertThat(widgetData).isInstanceOf(ChartData.class); ChartData chartData = (ChartData) widgetData; - assertEquals("chartData", chartData.getType()); + assertEquals("barChart", chartData.getType()); assertThat(chartData.getTitle()).isNotBlank(); assertNotNull(chartData.getChartData()); } From 525389e62e7a9ec2bc495d288eae0f10989441fa Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Sep 2022 13:52:43 -0500 Subject: [PATCH 2/3] QQQ-42 checkpoint of qqq reports --- .../core/actions/metadata/MetaDataAction.java | 13 + .../actions/reporting/CsvExportStreamer.java | 7 +- .../reporting/ExcelExportStreamer.java | 86 ++++-- .../core/actions/reporting/ExportAction.java | 9 +- .../reporting/ExportStreamerInterface.java | 8 +- .../actions/reporting/FormulaInterpreter.java | 7 +- .../reporting/GenerateReportAction.java | 259 ++++++++++++++---- .../reporting/ListOfMapsExportStreamer.java | 5 +- .../core/actions/reporting/PivotKey.java | 19 +- .../BoldHeaderAndFooterExcelStyler.java | 71 +++++ .../excelformatting/ExcelStylerInterface.java | 58 ++++ .../excelformatting/PlainExcelStyler.java | 31 +++ .../values/QPossibleValueTranslator.java | 19 +- .../core/instances/QInstanceEnricher.java | 24 ++ .../QMetaDataVariableInterpreter.java | 24 ++ .../actions/metadata/MetaDataOutput.java | 24 ++ .../actions/processes/ProcessSummaryLine.java | 161 +++++++++++ .../model/metadata/frontend/AppTreeNode.java | 5 + .../metadata/frontend/AppTreeNodeType.java | 3 +- .../frontend/QFrontendReportMetaData.java | 122 +++++++++ .../metadata/processes/QComponentType.java | 1 + .../metadata/reporting/QReportField.java | 35 --- .../metadata/reporting/QReportMetaData.java | 133 ++++++++- .../model/metadata/reporting/QReportView.java | 37 ++- .../model/metadata/reporting/ReportType.java | 2 +- .../ProcessSummaryProviderInterface.java | 28 +- .../StreamedETLExecuteStep.java | 4 +- .../StreamedETLValidateStep.java | 4 +- .../reports/BasicRunReportProcess.java | 89 ++++++ .../reports/ExecuteReportStep.java | 83 ++++++ .../reports/PrepareReportStep.java | 79 ++++++ .../qqq/backend/core/utils/Pair.java | 24 +- .../javalin/QJavalinProcessHandler.java | 15 + 33 files changed, 1346 insertions(+), 143 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcess.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java index 97d460ab..4736816f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java @@ -33,10 +33,12 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; 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.model.metadata.tables.QTableMetaData; @@ -80,6 +82,17 @@ public class MetaDataAction } metaDataOutput.setProcesses(processes); + ////////////////////////////////////// + // map reports to frontend metadata // + ////////////////////////////////////// + Map reports = new LinkedHashMap<>(); + for(Map.Entry entry : metaDataInput.getInstance().getReports().entrySet()) + { + reports.put(entry.getKey(), new QFrontendReportMetaData(entry.getValue(), false)); + treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue())); + } + metaDataOutput.setReports(reports); + /////////////////////////////////// // map apps to frontend metadata // /////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java index 057bc42b..3284279e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java @@ -66,7 +66,7 @@ public class CsvExportStreamer implements ExportStreamerInterface ** *******************************************************************************/ @Override - public void start(ExportInput exportInput, List fields) throws QReportingException + public void start(ExportInput exportInput, List fields, String label) throws QReportingException { this.exportInput = exportInput; this.fields = fields; @@ -87,7 +87,7 @@ public class CsvExportStreamer implements ExportStreamerInterface { if(StringUtils.hasContent(exportInput.getTitleRow())) { - outputStream.write(exportInput.getTitleRow().getBytes(StandardCharsets.UTF_8)); + outputStream.write((exportInput.getTitleRow() + "\n").getBytes(StandardCharsets.UTF_8)); } int col = 0; @@ -114,9 +114,8 @@ public class CsvExportStreamer implements ExportStreamerInterface ** *******************************************************************************/ @Override - public int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException + public int addRecords(List qRecords) throws QReportingException { - List qRecords = recordPipe.consumeAvailableRecords(); LOG.info("Consuming [" + qRecords.size() + "] records from the pipe"); for(QRecord qRecord : qRecords) 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 07a7ceef..d34d4cbd 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,6 +22,7 @@ package com.kingsrook.qqq.backend.core.actions.reporting; +import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.time.LocalDate; @@ -31,6 +32,9 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.ExcelStylerInterface; +import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.PlainExcelStyler; import com.kingsrook.qqq.backend.core.exceptions.QReportingException; import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -41,8 +45,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.dhatim.fastexcel.BorderSide; -import org.dhatim.fastexcel.BorderStyle; +import org.dhatim.fastexcel.StyleSetter; import org.dhatim.fastexcel.Workbook; import org.dhatim.fastexcel.Worksheet; @@ -59,7 +62,8 @@ public class ExcelExportStreamer implements ExportStreamerInterface private List fields; private OutputStream outputStream; - private Map excelCellFormats; + private ExcelStylerInterface excelStylerInterface = new PlainExcelStyler(); + private Map excelCellFormats; private Workbook workbook; private Worksheet worksheet; @@ -99,17 +103,38 @@ public class ExcelExportStreamer implements ExportStreamerInterface ** *******************************************************************************/ @Override - public void start(ExportInput exportInput, List fields) throws QReportingException + public void start(ExportInput exportInput, List fields, String label) throws QReportingException { - this.exportInput = exportInput; - this.fields = fields; - table = exportInput.getTable(); - outputStream = this.exportInput.getReportOutputStream(); + try + { + this.exportInput = exportInput; + this.fields = fields; + table = exportInput.getTable(); + outputStream = this.exportInput.getReportOutputStream(); + this.row = 0; - workbook = new Workbook(outputStream, "QQQ", null); - worksheet = workbook.newWorksheet("Sheet 1"); + if(workbook == null) + { + workbook = new Workbook(outputStream, "QQQ", null); + } - writeReportHeaderRow(); + ///////////////////////////////////////////////////////////////////////////////////// + // if start is called a second time (e.g., and there's already an open worksheet), // + // finish that sheet, before a new one is created. // + ///////////////////////////////////////////////////////////////////////////////////// + if(worksheet != null) + { + worksheet.finish(); + } + + worksheet = workbook.newWorksheet(label); + + writeReportHeaderRow(); + } + catch(IOException e) + { + throw (new QReportingException("Error starting worksheet", e)); + } } @@ -121,18 +146,24 @@ public class ExcelExportStreamer implements ExportStreamerInterface { try { + /////////////// + // title row // + /////////////// if(StringUtils.hasContent(exportInput.getTitleRow())) { worksheet.value(row, 0, exportInput.getTitleRow()); worksheet.range(row, 0, row, fields.size() - 1).merge(); - worksheet.range(row, 0, row, fields.size() - 1).style() - .bold() - .fontSize(14) - .horizontalAlignment("center") - .set(); + + StyleSetter titleStyle = worksheet.range(row, 0, row, fields.size() - 1).style(); + excelStylerInterface.styleTitleRow(titleStyle); + titleStyle.set(); + row++; } + //////////////// + // header row // + //////////////// int col = 0; for(QFieldMetaData column : fields) { @@ -140,10 +171,9 @@ public class ExcelExportStreamer implements ExportStreamerInterface col++; } - worksheet.range(row, 0, row, fields.size() - 1).style() - .bold() - .borderStyle(BorderSide.BOTTOM, BorderStyle.THIN) - .set(); + StyleSetter headerStyle = worksheet.range(row, 0, row, fields.size() - 1).style(); + excelStylerInterface.styleHeaderRow(headerStyle); + headerStyle.set(); row++; @@ -161,9 +191,8 @@ public class ExcelExportStreamer implements ExportStreamerInterface ** *******************************************************************************/ @Override - public int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException + public int addRecords(List qRecords) throws QReportingException { - List qRecords = recordPipe.consumeAvailableRecords(); LOG.info("Consuming [" + qRecords.size() + "] records from the pipe"); try @@ -203,6 +232,11 @@ public class ExcelExportStreamer implements ExportStreamerInterface for(QFieldMetaData field : fields) { Serializable value = qRecord.getValue(field.getName()); + if(field.getPossibleValueSourceName() != null) + { + value = Objects.requireNonNullElse(qRecord.getDisplayValue(field.getName()), value); + } + if(value != null) { if(value instanceof String s) @@ -264,11 +298,9 @@ public class ExcelExportStreamer implements ExportStreamerInterface { writeRecord(record); - worksheet.range(row, 0, row, fields.size() - 1).style() - .bold() - .borderStyle(BorderSide.TOP, BorderStyle.THIN) - .borderStyle(BorderSide.BOTTOM, BorderStyle.DOUBLE) - .set(); + StyleSetter totalsRowStyle = worksheet.range(row, 0, row, fields.size() - 1).style(); + excelStylerInterface.styleTotalsRow(totalsRowStyle); + totalsRowStyle.set(); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java index d15c591a..d0a7b3d8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; @@ -164,7 +165,7 @@ public class ExportAction //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ReportFormat reportFormat = exportInput.getReportFormat(); ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer(); - reportStreamer.start(exportInput, getFields(exportInput)); + reportStreamer.start(exportInput, getFields(exportInput), "Sheet 1"); ////////////////////////////////////////// // run the query action as an async job // @@ -207,7 +208,8 @@ public class ExportAction lastReceivedRecordsAt = System.currentTimeMillis(); nextSleepMillis = INIT_SLEEP_MS; - int recordsConsumed = reportStreamer.takeRecordsFromPipe(recordPipe); + List records = recordPipe.consumeAvailableRecords(); + int recordsConsumed = reportStreamer.addRecords(records); recordCount += recordsConsumed; LOG.info(countFromPreExecute != null @@ -235,7 +237,8 @@ public class ExportAction /////////////////////////////////////////////////// // send the final records to the report streamer // /////////////////////////////////////////////////// - int recordsConsumed = reportStreamer.takeRecordsFromPipe(recordPipe); + List records = recordPipe.consumeAvailableRecords(); + int recordsConsumed = reportStreamer.addRecords(records); recordCount += recordsConsumed; long reportEndTime = System.currentTimeMillis(); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java index df0dfd72..791c1aa1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java @@ -38,12 +38,12 @@ public interface ExportStreamerInterface /******************************************************************************* ** Called once, before any rows are available. Meant to write a header, for example. *******************************************************************************/ - void start(ExportInput exportInput, List fields) throws QReportingException; + void start(ExportInput exportInput, List fields, String label) throws QReportingException; /******************************************************************************* ** Called as records flow into the pipe. ******************************************************************************/ - int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException; + int addRecords(List recordList) throws QReportingException; /******************************************************************************* ** Called once, after all rows are available. Meant to write a footer, or close resources, for example. @@ -63,8 +63,6 @@ public interface ExportStreamerInterface *******************************************************************************/ default void addTotalsRow(QRecord record) throws QReportingException { - RecordPipe recordPipe = new RecordPipe(); - recordPipe.addRecord(record); - takeRecordsFromPipe(recordPipe); + addRecords(List.of(record)); } } 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 fde25ff2..d229e3b7 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 @@ -194,6 +194,11 @@ public class FormulaInterpreter List numbers = getNumberArgumentList(args, 2, variableInterpreter); return nullIfAnyNullArgsElse(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)); + } default: { //////////////////////////////////////////////////////////////////////////////////////// @@ -259,7 +264,7 @@ public class FormulaInterpreter { try { - Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg)); + Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null); rs.add(ValueUtils.getValueAsBigDecimal(interpretedArg)); } catch(QValueException e) 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 6174d470..ccaf7590 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 @@ -29,6 +29,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; @@ -69,9 +70,16 @@ public class GenerateReportAction ////////////////////////////////////////////////// // viewName > PivotKey > fieldName > Aggregates // ////////////////////////////////////////////////// - Map>>> pivotAggregates = new HashMap<>(); + Map>>> pivotAggregates = new HashMap<>(); + Map>>> variancePivotAggregates = new HashMap<>(); - Map> totalAggregates = new HashMap<>(); + Map> totalAggregates = new HashMap<>(); + Map> varianceTotalAggregates = new HashMap<>(); + + private boolean includeTableView = false; + private QReportMetaData report; + private ReportFormat reportFormat; + private ExportStreamerInterface reportStreamer; @@ -80,8 +88,56 @@ public class GenerateReportAction *******************************************************************************/ public void execute(ReportInput reportInput) throws QException { + report = reportInput.getInstance().getReport(reportInput.getReportName()); + Optional tableView = report.getViews().stream().filter(v -> v.getType().equals(ReportType.TABLE)).findFirst(); + + reportFormat = reportInput.getReportFormat(); + reportStreamer = reportFormat.newReportStreamer(); + + if(tableView.isPresent()) + { + includeTableView = true; + startTableView(reportInput, tableView.get()); + } + gatherData(reportInput); - output(reportInput); + gatherVarianceData(reportInput); + + outputPivots(reportInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void startTableView(ReportInput reportInput, QReportView reportView) throws QReportingException + { + QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); + + ExportInput exportInput = new ExportInput(reportInput.getInstance()); + exportInput.setSession(reportInput.getSession()); + exportInput.setReportFormat(reportFormat); + exportInput.setFilename(reportInput.getFilename()); + exportInput.setReportOutputStream(reportInput.getReportOutputStream()); + + // todo! reportStreamer.setDisplayFormats(getDisplayFormatMap(view)); + + List fields; + if(CollectionUtils.nullSafeHasContents(reportView.getColumns())) + { + fields = new ArrayList<>(); + for(QReportField column : reportView.getColumns()) + { + fields.add(table.getField(column.getName())); + } + } + else + { + fields = new ArrayList<>(table.getFields().values()); + } + reportStreamer.setDisplayFormats(getDisplayFormatMap(fields)); + reportStreamer.start(exportInput, fields, reportView.getLabel()); } @@ -91,9 +147,7 @@ public class GenerateReportAction *******************************************************************************/ private void gatherData(ReportInput reportInput) throws QException { - QReportMetaData report = reportInput.getInstance().getReport(reportInput.getReportName()); - QQueryFilter queryFilter = report.getQueryFilter(); - + QQueryFilter queryFilter = report.getQueryFilter(); setInputValuesInQueryFilter(reportInput, queryFilter); RecordPipe recordPipe = new RecordPipe(); @@ -104,8 +158,37 @@ public class GenerateReportAction queryInput.setRecordPipe(recordPipe); queryInput.setTableName(report.getSourceTable()); queryInput.setFilter(queryFilter); + queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this? return (new QueryAction().execute(queryInput)); - }, () -> consumeRecords(report, reportInput, recordPipe)); + }, () -> consumeRecords(reportInput, recordPipe, false)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void gatherVarianceData(ReportInput reportInput) throws QException + { + QQueryFilter varianceQueryFilter = report.getVarianceQueryFilter(); + if(varianceQueryFilter == null) + { + return; + } + + setInputValuesInQueryFilter(reportInput, varianceQueryFilter); + + RecordPipe recordPipe = new RecordPipe(); + new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) -> + { + QueryInput queryInput = new QueryInput(reportInput.getInstance()); + queryInput.setSession(reportInput.getSession()); + queryInput.setRecordPipe(recordPipe); + queryInput.setTableName(report.getSourceTable()); + queryInput.setFilter(varianceQueryFilter); + queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this? + return (new QueryAction().execute(queryInput)); + }, () -> consumeRecords(reportInput, recordPipe, true)); } @@ -144,20 +227,35 @@ public class GenerateReportAction /******************************************************************************* ** *******************************************************************************/ - private Integer consumeRecords(QReportMetaData report, ReportInput reportInput, RecordPipe recordPipe) + private Integer consumeRecords(ReportInput reportInput, RecordPipe recordPipe, boolean isForVariance) throws QReportingException { - // todo - stream to output if report has a simple type output List records = recordPipe.consumeAvailableRecords(); + if(includeTableView && !isForVariance) + { + reportStreamer.addRecords(records); + } + ////////////////////////////// // do aggregates for pivots // ////////////////////////////// QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).forEach((view) -> { - doPivotAggregates(view, table, records); + addRecordsToPivotAggregates(view, table, records, isForVariance ? variancePivotAggregates : pivotAggregates); }); + /////////////////////////////////////////// + // do totals too, if any views want them // + /////////////////////////////////////////// + if(report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).anyMatch(QReportView::getTotalRow)) + { + for(QRecord record : records) + { + addRecordToAggregatesMap(table, record, isForVariance ? varianceTotalAggregates : totalAggregates); + } + } + return (records.size()); } @@ -166,44 +264,33 @@ public class GenerateReportAction /******************************************************************************* ** *******************************************************************************/ - private void doPivotAggregates(QReportView view, QTableMetaData table, List records) + private void addRecordsToPivotAggregates(QReportView view, QTableMetaData table, List records, Map>>> aggregatesMap) { - Map>> viewAggregates = pivotAggregates.computeIfAbsent(view.getName(), (name) -> new HashMap<>()); + Map>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>()); for(QRecord record : records) { PivotKey key = new PivotKey(); for(String pivotField : view.getPivotFields()) { - key.add(pivotField, record.getValue(pivotField)); + Serializable pivotValue = record.getValue(pivotField); + if(table.getField(pivotField).getPossibleValueSourceName() != null) + { + pivotValue = record.getDisplayValue(pivotField); + } + key.add(pivotField, pivotValue); + + if(view.getPivotSubTotals() && key.getKeys().size() < view.getPivotFields().size()) + { + ///////////////////////////////////////////////////////////////////////////////////////// + // be careful here, with these key objects, and their identity, being used as map keys // + ///////////////////////////////////////////////////////////////////////////////////////// + PivotKey subKey = key.clone(); + addRecordToPivotKeyAggregates(table, record, viewAggregates, subKey); + } } - Map> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>()); - - for(QFieldMetaData field : table.getFields().values()) - { - if(field.getType().equals(QFieldType.INTEGER)) - { - @SuppressWarnings("unchecked") - AggregatesInterface fieldAggregates = (AggregatesInterface) keyAggregates.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates()); - fieldAggregates.add(record.getValueInteger(field.getName())); - - @SuppressWarnings("unchecked") - AggregatesInterface fieldTotalAggregates = (AggregatesInterface) totalAggregates.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates()); - fieldTotalAggregates.add(record.getValueInteger(field.getName())); - } - else if(field.getType().equals(QFieldType.DECIMAL)) - { - @SuppressWarnings("unchecked") - AggregatesInterface fieldAggregates = (AggregatesInterface) keyAggregates.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates()); - fieldAggregates.add(record.getValueBigDecimal(field.getName())); - - @SuppressWarnings("unchecked") - AggregatesInterface fieldTotalAggregates = (AggregatesInterface) totalAggregates.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates()); - fieldTotalAggregates.add(record.getValueBigDecimal(field.getName())); - } - // todo - more types (dates, at least?) - } + addRecordToPivotKeyAggregates(table, record, viewAggregates, key); } } @@ -212,16 +299,50 @@ public class GenerateReportAction /******************************************************************************* ** *******************************************************************************/ - private void output(ReportInput reportInput) throws QReportingException, QFormulaException + private void addRecordToPivotKeyAggregates(QTableMetaData table, QRecord record, Map>> viewAggregates, PivotKey key) { - QReportMetaData report = reportInput.getInstance().getReport(reportInput.getReportName()); - QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); + Map> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>()); + addRecordToAggregatesMap(table, record, keyAggregates); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map> aggregatesMap) + { + for(QFieldMetaData field : table.getFields().values()) + { + if(field.getType().equals(QFieldType.INTEGER)) + { + @SuppressWarnings("unchecked") + AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates()); + fieldAggregates.add(record.getValueInteger(field.getName())); + } + else if(field.getType().equals(QFieldType.DECIMAL)) + { + @SuppressWarnings("unchecked") + AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates()); + fieldAggregates.add(record.getValueBigDecimal(field.getName())); + } + // todo - more types (dates, at least?) + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void outputPivots(ReportInput reportInput) throws QReportingException, QFormulaException + { + QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); List reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).toList(); for(QReportView view : reportViews) { - PivotOutput pivotOutput = outputPivot(reportInput, view, table); - ReportFormat reportFormat = reportInput.getReportFormat(); + PivotOutput pivotOutput = computePivotRowsForView(reportInput, view, table); ExportInput exportInput = new ExportInput(reportInput.getInstance()); exportInput.setSession(reportInput.getSession()); @@ -230,21 +351,18 @@ public class GenerateReportAction exportInput.setTitleRow(pivotOutput.titleRow); exportInput.setReportOutputStream(reportInput.getReportOutputStream()); - ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer(); reportStreamer.setDisplayFormats(getDisplayFormatMap(view)); - reportStreamer.start(exportInput, getFields(table, view)); + reportStreamer.start(exportInput, getFields(table, view), view.getLabel()); - RecordPipe recordPipe = new RecordPipe(); // todo - make it an unlimited pipe or something... - recordPipe.addRecords(pivotOutput.pivotRows); - reportStreamer.takeRecordsFromPipe(recordPipe); + reportStreamer.addRecords(pivotOutput.pivotRows); // todo - what if this set is huge? if(pivotOutput.totalRow != null) { reportStreamer.addTotalsRow(pivotOutput.totalRow); } - - reportStreamer.finish(); } + + reportStreamer.finish(); } @@ -261,6 +379,18 @@ public class GenerateReportAction + /******************************************************************************* + ** + *******************************************************************************/ + private Map getDisplayFormatMap(List fields) + { + return (fields.stream() + .filter(f -> f.getDisplayFormat() != null) + .collect(Collectors.toMap(QFieldMetaData::getName, QFieldMetaData::getDisplayFormat))); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -284,7 +414,7 @@ public class GenerateReportAction /******************************************************************************* ** *******************************************************************************/ - private PivotOutput outputPivot(ReportInput reportInput, QReportView view, QTableMetaData table) throws QReportingException, QFormulaException + private PivotOutput computePivotRowsForView(ReportInput reportInput, QReportView view, QTableMetaData table) throws QReportingException, QFormulaException { QValueFormatter valueFormatter = new QValueFormatter(); QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter(); @@ -339,13 +469,36 @@ public class GenerateReportAction Map> fieldAggregates = entry.getValue(); variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(fieldAggregates)); + if(!variancePivotAggregates.isEmpty()) + { + Map>> varianceMap = variancePivotAggregates.getOrDefault(view.getName(), Collections.emptyMap()); + Map> varianceSubMap = varianceMap.getOrDefault(pivotKey, Collections.emptyMap()); + variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceSubMap)); + } + QRecord pivotRow = new QRecord(); pivotRows.add(pivotRow); + + ////////////////////////// + // add the pivot values // + ////////////////////////// for(Pair key : pivotKey.getKeys()) { pivotRow.setValue(key.getA(), key.getB()); } + ///////////////////////////////////////////////////////////////////////////// + // for pivot subtotals, add the text "Total" to the last field in this key // + ///////////////////////////////////////////////////////////////////////////// + if(pivotKey.getKeys().size() < view.getPivotFields().size()) + { + String fieldName = pivotKey.getKeys().get(pivotKey.getKeys().size() - 1).getA(); + pivotRow.setValue(fieldName, pivotRow.getValueString(fieldName) + " Total"); + } + + /////////////////////////// + // add the column values // + /////////////////////////// for(QReportField column : view.getColumns()) { Serializable serializable = getValueForColumn(variableInterpreter, column); @@ -406,6 +559,8 @@ public class GenerateReportAction } variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(totalAggregates)); + variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceTotalAggregates)); + for(QReportField column : view.getColumns()) { Serializable serializable = getValueForColumn(variableInterpreter, column); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java index c844dbbb..78b8f365 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java @@ -75,7 +75,7 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface ** *******************************************************************************/ @Override - public void start(ExportInput exportInput, List fields) throws QReportingException + public void start(ExportInput exportInput, List fields, String label) throws QReportingException { this.exportInput = exportInput; this.fields = fields; @@ -93,9 +93,8 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface ** *******************************************************************************/ @Override - public int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException + public int addRecords(List qRecords) throws QReportingException { - List qRecords = recordPipe.consumeAvailableRecords(); LOG.info("Consuming [" + qRecords.size() + "] records from the pipe"); for(QRecord qRecord : qRecords) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/PivotKey.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/PivotKey.java index 6d926a41..a12a54c0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/PivotKey.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/PivotKey.java @@ -32,7 +32,7 @@ import com.kingsrook.qqq.backend.core.utils.Pair; /******************************************************************************* ** *******************************************************************************/ -public class PivotKey +public class PivotKey implements Cloneable { private List> keys = new ArrayList<>(); @@ -108,4 +108,21 @@ public class PivotKey return Objects.hash(keys); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public PivotKey clone() + { + PivotKey clone = new PivotKey(); + + for(Pair key : keys) + { + clone.add(key.getA(), key.getB()); + } + + return (clone); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java new file mode 100644 index 00000000..a42cd573 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java @@ -0,0 +1,71 @@ +/* + * 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.actions.reporting.excelformatting; + + +import org.dhatim.fastexcel.BorderSide; +import org.dhatim.fastexcel.BorderStyle; +import org.dhatim.fastexcel.StyleSetter; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void styleTitleRow(StyleSetter titleRowStyle) + { + titleRowStyle + .bold() + .fontSize(14) + .horizontalAlignment("center"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void styleHeaderRow(StyleSetter headerRowStyle) + { + headerRowStyle + .bold() + .borderStyle(BorderSide.BOTTOM, BorderStyle.THIN); + } + + + + @Override + public void styleTotalsRow(StyleSetter totalsRowStyle) + { + totalsRowStyle + .bold() + .borderStyle(BorderSide.TOP, BorderStyle.THIN) + .borderStyle(BorderSide.BOTTOM, BorderStyle.DOUBLE); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java new file mode 100644 index 00000000..e03855c4 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java @@ -0,0 +1,58 @@ +/* + * 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.actions.reporting.excelformatting; + + +import org.dhatim.fastexcel.StyleSetter; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface ExcelStylerInterface +{ + + /******************************************************************************* + ** + *******************************************************************************/ + default void styleTitleRow(StyleSetter titleRowStyle) + { + + } + + /******************************************************************************* + ** + *******************************************************************************/ + default void styleHeaderRow(StyleSetter headerRowStyle) + { + + } + + /******************************************************************************* + ** + *******************************************************************************/ + default void styleTotalsRow(StyleSetter totalsRowStyle) + { + + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java new file mode 100644 index 00000000..a47b7564 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java @@ -0,0 +1,31 @@ +/* + * 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.actions.reporting.excelformatting; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class PlainExcelStyler implements ExcelStylerInterface +{ + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java index 582dc7d4..317ecb8b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java @@ -294,11 +294,26 @@ public class QPossibleValueTranslator { for(QFieldMetaData field : fieldsByPvsTable.get(tableName)) { - values.add(record.getValue(field.getName())); + Serializable fieldValue = record.getValue(field.getName()); + + ////////////////////////////////////// + // check if value is already cached // + ////////////////////////////////////// + QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0); + possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>()); + Map cacheForPvs = possibleValueCache.get(possibleValueSource.getName()); + + if(!cacheForPvs.containsKey(fieldValue)) + { + values.add(fieldValue); + } } } - primePvsCache(tableName, pvsesByTable.get(tableName), values); + if(!values.isEmpty()) + { + primePvsCache(tableName, pvsesByTable.get(tableName), values); + } } } 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 d56f830a..ced0548e 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 @@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponen import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; @@ -97,6 +98,11 @@ public class QInstanceEnricher { qInstance.getApps().values().forEach(this::enrich); } + + if(qInstance.getReports() != null) + { + qInstance.getReports().values().forEach(this::enrich); + } } @@ -215,6 +221,24 @@ public class QInstanceEnricher + /******************************************************************************* + ** + *******************************************************************************/ + private void enrich(QReportMetaData report) + { + if(!StringUtils.hasContent(report.getLabel())) + { + report.setLabel(nameToLabel(report.getName())); + } + + if(report.getInputFields() != null) + { + report.getInputFields().forEach(this::enrich); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java index c52009e4..202c2687 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java @@ -148,6 +148,23 @@ public class QMetaDataVariableInterpreter ** Else the output is the input. *******************************************************************************/ public Serializable interpretForObject(String value) + { + return (interpretForObject(value, value)); + } + + + + /******************************************************************************* + ** Interpret a value string, which may be a variable, into its run-time value. + ** + ** If input is null, output is null. + ** If input looks like ${env.X}, then the return value is the value of the env variable 'X' + ** If input looks like ${prop.X}, then the return value is the value of the system property 'X' + ** If input looks like ${literal.X}, then the return value is the literal 'X' + ** - used if you really want to get back the literal value, ${env.X}, for example. + ** Else the output is the input. + *******************************************************************************/ + public Serializable interpretForObject(String value, Serializable defaultIfLooksLikeVariableButNotFound) { if(value == null) { @@ -176,6 +193,7 @@ public class QMetaDataVariableInterpreter if(valueMaps != null) { + boolean looksLikeVariable = false; for(Map.Entry> entry : valueMaps.entrySet()) { String name = entry.getKey(); @@ -184,6 +202,7 @@ public class QMetaDataVariableInterpreter String prefix = "${" + name + "."; if(value.startsWith(prefix) && value.endsWith("}")) { + looksLikeVariable = true; String lookupName = value.substring(prefix.length()).replaceFirst("}$", ""); if(valueMap != null && valueMap.containsKey(lookupName)) { @@ -191,6 +210,11 @@ public class QMetaDataVariableInterpreter } } } + + if(looksLikeVariable) + { + return (defaultIfLooksLikeVariableButNotFound); + } } return (value); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataOutput.java index afb3d709..77fe03d5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/MetaDataOutput.java @@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData; import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData; @@ -40,6 +41,7 @@ public class MetaDataOutput extends AbstractActionOutput { private Map tables; private Map processes; + private Map reports; private Map apps; private List appTree; @@ -92,6 +94,28 @@ public class MetaDataOutput extends AbstractActionOutput + /******************************************************************************* + ** Getter for reports + ** + *******************************************************************************/ + public Map getReports() + { + return reports; + } + + + + /******************************************************************************* + ** Setter for reports + ** + *******************************************************************************/ + public void setReports(Map reports) + { + this.reports = reports; + } + + + /******************************************************************************* ** Getter for appTree ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java index 12aa596b..327f5b80 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLine.java @@ -38,6 +38,11 @@ public class ProcessSummaryLine implements Serializable private Integer count = 0; private String message; + private String singularFutureMessage; + private String pluralFutureMessage; + private String singularPastMessage; + private String pluralPastMessage; + ////////////////////////////////////////////////////////////////////////// // using ArrayList, because we need to be Serializable, and List is not // ////////////////////////////////////////////////////////////////////////// @@ -240,4 +245,160 @@ public class ProcessSummaryLine implements Serializable rs.add(this); } } + + + + /******************************************************************************* + ** Getter for singularFutureMessage + ** + *******************************************************************************/ + public String getSingularFutureMessage() + { + return singularFutureMessage; + } + + + + /******************************************************************************* + ** Setter for singularFutureMessage + ** + *******************************************************************************/ + public void setSingularFutureMessage(String singularFutureMessage) + { + this.singularFutureMessage = singularFutureMessage; + } + + + + /******************************************************************************* + ** Fluent setter for singularFutureMessage + ** + *******************************************************************************/ + public ProcessSummaryLine withSingularFutureMessage(String singularFutureMessage) + { + this.singularFutureMessage = singularFutureMessage; + return (this); + } + + + + /******************************************************************************* + ** Getter for pluralFutureMessage + ** + *******************************************************************************/ + public String getPluralFutureMessage() + { + return pluralFutureMessage; + } + + + + /******************************************************************************* + ** Setter for pluralFutureMessage + ** + *******************************************************************************/ + public void setPluralFutureMessage(String pluralFutureMessage) + { + this.pluralFutureMessage = pluralFutureMessage; + } + + + + /******************************************************************************* + ** Fluent setter for pluralFutureMessage + ** + *******************************************************************************/ + public ProcessSummaryLine withPluralFutureMessage(String pluralFutureMessage) + { + this.pluralFutureMessage = pluralFutureMessage; + return (this); + } + + + + /******************************************************************************* + ** Getter for singularPastMessage + ** + *******************************************************************************/ + public String getSingularPastMessage() + { + return singularPastMessage; + } + + + + /******************************************************************************* + ** Setter for singularPastMessage + ** + *******************************************************************************/ + public void setSingularPastMessage(String singularPastMessage) + { + this.singularPastMessage = singularPastMessage; + } + + + + /******************************************************************************* + ** Fluent setter for singularPastMessage + ** + *******************************************************************************/ + public ProcessSummaryLine withSingularPastMessage(String singularPastMessage) + { + this.singularPastMessage = singularPastMessage; + return (this); + } + + + + /******************************************************************************* + ** Getter for pluralPastMessage + ** + *******************************************************************************/ + public String getPluralPastMessage() + { + return pluralPastMessage; + } + + + + /******************************************************************************* + ** Setter for pluralPastMessage + ** + *******************************************************************************/ + public void setPluralPastMessage(String pluralPastMessage) + { + this.pluralPastMessage = pluralPastMessage; + } + + + + /******************************************************************************* + ** Fluent setter for pluralPastMessage + ** + *******************************************************************************/ + public ProcessSummaryLine withPluralPastMessage(String pluralPastMessage) + { + this.pluralPastMessage = pluralPastMessage; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void pickMessage(boolean isPast) + { + if(count != null) + { + if(count.equals(1)) + { + setMessage(isPast ? getSingularPastMessage() : getSingularFutureMessage()); + } + else + { + setMessage(isPast ? getPluralPastMessage() : getPluralFutureMessage()); + } + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java index 5b912094..b4ab8965 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNode.java @@ -27,6 +27,7 @@ import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; 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.model.metadata.tables.QTableMetaData; @@ -64,6 +65,10 @@ public class AppTreeNode { this.type = AppTreeNodeType.PROCESS; } + else if(appChildMetaData.getClass().equals(QReportMetaData.class)) + { + this.type = AppTreeNodeType.REPORT; + } else if(appChildMetaData.getClass().equals(QAppMetaData.class)) { this.type = AppTreeNodeType.APP; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNodeType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNodeType.java index 3b2c7eaf..5a797e32 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNodeType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/AppTreeNodeType.java @@ -23,11 +23,12 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend; /******************************************************************************* - ** Type for an Node in the an app tree. + ** Type for an Node in the app tree. *******************************************************************************/ public enum AppTreeNodeType { TABLE, PROCESS, + REPORT, APP } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java new file mode 100644 index 00000000..3bad2944 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java @@ -0,0 +1,122 @@ +/* + * 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.model.metadata.frontend; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; + + +/******************************************************************************* + * Version of QReportMetaData that's meant for transmitting to a frontend. + * e.g., it excludes backend-only details. + * + *******************************************************************************/ +@JsonInclude(Include.NON_NULL) +public class QFrontendReportMetaData +{ + private String name; + private String label; + private String tableName; + private String processName; + + private String iconName; + + ////////////////////////////////////////////////////////////////////////////////// + // do not add setters. take values from the source-object in the constructor!! // + ////////////////////////////////////////////////////////////////////////////////// + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QFrontendReportMetaData(QReportMetaData reportMetaData, boolean includeSteps) + { + this.name = reportMetaData.getName(); + this.label = reportMetaData.getLabel(); + this.tableName = reportMetaData.getSourceTable(); + this.processName = reportMetaData.getProcessName(); + + if(reportMetaData.getIcon() != null) + { + this.iconName = reportMetaData.getIcon().getName(); + } + } + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } + + + + /******************************************************************************* + ** Getter for primaryKeyField + ** + *******************************************************************************/ + public String getTableName() + { + return tableName; + } + + + + /******************************************************************************* + ** Getter for processName + ** + *******************************************************************************/ + public String getProcessName() + { + return processName; + } + + + + /******************************************************************************* + ** Getter for iconName + ** + *******************************************************************************/ + public String getIconName() + { + return iconName; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QComponentType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QComponentType.java index 70e6a191..fad28dc4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QComponentType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QComponentType.java @@ -32,6 +32,7 @@ public enum QComponentType VALIDATION_REVIEW_SCREEN, EDIT_FORM, VIEW_FORM, + DOWNLOAD_FORM, RECORD_LIST, PROCESS_SUMMARY_RESULTS; /////////////////////////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java index 38a1aedd..cb53f132 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java @@ -29,7 +29,6 @@ public class QReportField { private String name; private String label; - private String fieldName; private String formula; private String displayFormat; // todo - type? @@ -104,40 +103,6 @@ public class QReportField - /******************************************************************************* - ** Getter for fieldName - ** - *******************************************************************************/ - public String getFieldName() - { - return fieldName; - } - - - - /******************************************************************************* - ** Setter for fieldName - ** - *******************************************************************************/ - public void setFieldName(String fieldName) - { - this.fieldName = fieldName; - } - - - - /******************************************************************************* - ** Fluent setter for fieldName - ** - *******************************************************************************/ - public QReportField withFieldName(String fieldName) - { - this.fieldName = fieldName; - return (this); - } - - - /******************************************************************************* ** Getter for formula ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java index 9854a8e8..ccb00b3e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java @@ -25,20 +25,27 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting; import java.util.List; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; /******************************************************************************* ** Meta-data definition of a report generated by QQQ *******************************************************************************/ -public class QReportMetaData +public class QReportMetaData implements QAppChildMetaData { private String name; private String label; private List inputFields; private String sourceTable; + private String processName; private QQueryFilter queryFilter; + private QQueryFilter varianceQueryFilter; private List views; + private String parentAppName; + private QIcon icon; + /******************************************************************************* @@ -177,6 +184,40 @@ public class QReportMetaData + /******************************************************************************* + ** Getter for processName + ** + *******************************************************************************/ + public String getProcessName() + { + return processName; + } + + + + /******************************************************************************* + ** Setter for processName + ** + *******************************************************************************/ + public void setProcessName(String processName) + { + this.processName = processName; + } + + + + /******************************************************************************* + ** Fluent setter for processName + ** + *******************************************************************************/ + public QReportMetaData withProcessName(String processName) + { + this.processName = processName; + return (this); + } + + + /******************************************************************************* ** Getter for queryFilter ** @@ -211,6 +252,40 @@ public class QReportMetaData + /******************************************************************************* + ** Getter for varianceQueryFilter + ** + *******************************************************************************/ + public QQueryFilter getVarianceQueryFilter() + { + return varianceQueryFilter; + } + + + + /******************************************************************************* + ** Setter for varianceQueryFilter + ** + *******************************************************************************/ + public void setVarianceQueryFilter(QQueryFilter varianceQueryFilter) + { + this.varianceQueryFilter = varianceQueryFilter; + } + + + + /******************************************************************************* + ** Fluent setter for varianceQueryFilter + ** + *******************************************************************************/ + public QReportMetaData withVarianceQueryFilter(QQueryFilter varianceQueryFilter) + { + this.varianceQueryFilter = varianceQueryFilter; + return (this); + } + + + /******************************************************************************* ** Getter for views ** @@ -243,4 +318,60 @@ public class QReportMetaData return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void setParentAppName(String parentAppName) + { + this.parentAppName = parentAppName; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String getParentAppName() + { + return (this.parentAppName); + } + + + + /******************************************************************************* + ** Getter for icon + ** + *******************************************************************************/ + public QIcon getIcon() + { + return icon; + } + + + + /******************************************************************************* + ** Setter for icon + ** + *******************************************************************************/ + public void setIcon(QIcon icon) + { + this.icon = icon; + } + + + + /******************************************************************************* + ** Fluent setter for icon + ** + *******************************************************************************/ + public QReportMetaData withIcon(QIcon icon) + { + this.icon = icon; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java index 4e2054ed..3d32d340 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java @@ -37,7 +37,8 @@ public class QReportView private String titleFormat; private List titleFields; private List pivotFields; - private boolean totalRow = false; + private boolean totalRow = false; + private boolean pivotSubTotals = false; private List columns; private List orderByFields; @@ -281,6 +282,40 @@ public class QReportView + /******************************************************************************* + ** Getter for pivotSubTotals + ** + *******************************************************************************/ + public boolean getPivotSubTotals() + { + return pivotSubTotals; + } + + + + /******************************************************************************* + ** Setter for pivotSubTotals + ** + *******************************************************************************/ + public void setPivotSubTotals(boolean pivotSubTotals) + { + this.pivotSubTotals = pivotSubTotals; + } + + + + /******************************************************************************* + ** Fluent setter for pivotSubTotals + ** + *******************************************************************************/ + public QReportView withPivotSubTotals(boolean pivotSubTotals) + { + this.pivotSubTotals = pivotSubTotals; + return (this); + } + + + /******************************************************************************* ** Getter for columns ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java index f3f2f243..2b0b12bd 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java @@ -28,5 +28,5 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting; public enum ReportType { PIVOT, - SIMPLE + TABLE } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ProcessSummaryProviderInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ProcessSummaryProviderInterface.java index aa09fd24..a4ca7609 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ProcessSummaryProviderInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/ProcessSummaryProviderInterface.java @@ -24,10 +24,11 @@ package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwit import java.util.ArrayList; import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine; +import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* - ** Interface for a class that can proivate a ProcessSummary - a list of Process Summary Lines + ** Interface for a class that can provide a ProcessSummary - a list of Process Summary Lines *******************************************************************************/ public interface ProcessSummaryProviderInterface { @@ -37,4 +38,29 @@ public interface ProcessSummaryProviderInterface *******************************************************************************/ ArrayList getProcessSummary(boolean isForResultScreen); + + /******************************************************************************* + ** not meant to be overridden - meant to be called by framework - to make sure that + ** all lines have their proper message picked (e.g., if they have singular/plural + ** and past/future variants). + *******************************************************************************/ + default ArrayList doGetProcessSummary(boolean isForResultScreen) + { + ArrayList processSummary = getProcessSummary(isForResultScreen); + if(processSummary == null) + { + return (null); + } + + for(ProcessSummaryLine processSummaryLine : processSummary) + { + if(!StringUtils.hasContent(processSummaryLine.getMessage())) + { + processSummaryLine.pickMessage(isForResultScreen); + } + } + + return (processSummary); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java index 4f33651f..2de0817e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLExecuteStep.java @@ -90,7 +90,7 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe //////////////////////////////////////////////////////////////////////////////////////////////////// // get the process summary from the ... transform step? the load step? each knows some... todo? // //////////////////////////////////////////////////////////////////////////////////////////////////// - runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, transformStep.getProcessSummary(true)); + runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, transformStep.doGetProcessSummary(true)); transformStep.postRun(runBackendStepInput, runBackendStepOutput); loadStep.postRun(runBackendStepInput, runBackendStepOutput); @@ -147,7 +147,7 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe /////////////////////////////////////////////////////////////////////// // make streamed input & output objects from the run input & outputs // /////////////////////////////////////////////////////////////////////// - StreamedBackendStepInput streamedBackendStepInput = new StreamedBackendStepInput(runBackendStepInput, qRecords); + StreamedBackendStepInput streamedBackendStepInput = new StreamedBackendStepInput(runBackendStepInput, qRecords); StreamedBackendStepOutput streamedBackendStepOutput = new StreamedBackendStepOutput(runBackendStepOutput); ///////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java index 1057c84c..15d7cae6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/StreamedETLValidateStep.java @@ -105,7 +105,7 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back ////////////////////////////////////////////////////// // get the process summary from the validation step // ////////////////////////////////////////////////////// - runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_VALIDATION_SUMMARY, transformStep.getProcessSummary(false)); + runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_VALIDATION_SUMMARY, transformStep.doGetProcessSummary(false)); transformStep.postRun(runBackendStepInput, runBackendStepOutput); } @@ -131,7 +131,7 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back /////////////////////////////////////////////////////////////////////// // make streamed input & output objects from the run input & outputs // /////////////////////////////////////////////////////////////////////// - StreamedBackendStepInput streamedBackendStepInput = new StreamedBackendStepInput(runBackendStepInput, qRecords); + StreamedBackendStepInput streamedBackendStepInput = new StreamedBackendStepInput(runBackendStepInput, qRecords); StreamedBackendStepOutput streamedBackendStepOutput = new StreamedBackendStepOutput(runBackendStepOutput); ///////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcess.java new file mode 100644 index 00000000..3eb738db --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcess.java @@ -0,0 +1,89 @@ +/* + * 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 com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; + + +/******************************************************************************* + ** Definition for Basic process to run a report. + *******************************************************************************/ +public class BasicRunReportProcess +{ + public static final String PROCESS_NAME = "reports.basic"; + + public static final String STEP_NAME_PREPARE = "prepare"; + public static final String STEP_NAME_INPUT = "input"; + public static final String STEP_NAME_EXECUTE = "execute"; + public static final String STEP_NAME_ACCESS = "accessReport"; + + public static final String FIELD_REPORT_NAME = "reportName"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData defineProcessMetaData() + { + QStepMetaData prepareStep = new QBackendStepMetaData() + .withName(STEP_NAME_PREPARE) + .withCode(new QCodeReference(PrepareReportStep.class)) + .withInputData(new QFunctionInputMetaData() + .withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING))); + + QStepMetaData inputStep = new QFrontendStepMetaData() + .withName(STEP_NAME_INPUT) + .withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM)); + + QStepMetaData executeStep = new QBackendStepMetaData() + .withName(STEP_NAME_EXECUTE) + .withCode(new QCodeReference(ExecuteReportStep.class)) + .withInputData(new QFunctionInputMetaData() + .withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING))); + + QStepMetaData accessStep = new QFrontendStepMetaData() + .withName(STEP_NAME_ACCESS) + .withComponent(new QFrontendComponentMetaData().withType(QComponentType.DOWNLOAD_FORM)); + // .withViewField(new QFieldMetaData("outputFile", QFieldType.STRING)) + // .withViewField(new QFieldMetaData("message", QFieldType.STRING)); + + return new QProcessMetaData() + .withName(PROCESS_NAME) + .withIsHidden(true) + .addStep(prepareStep) + .addStep(inputStep) + .addStep(executeStep) + .addStep(accessStep); + } + +} 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 new file mode 100644 index 00000000..f36d0553 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java @@ -0,0 +1,83 @@ +/* + * 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.io.File; +import java.io.FileOutputStream; +import java.io.Serializable; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +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; + + +/******************************************************************************* + ** Process step to execute a report. + ** + ** Writes it to a temp file... Returns that file name in process output. + *******************************************************************************/ +public class ExecuteReportStep implements BackendStep +{ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + try + { + String reportName = runBackendStepInput.getValueString("reportName"); + File tmpFile = File.createTempFile(reportName, ".xlsx", new File("/tmp/")); + + runBackendStepInput.getAsyncJobCallback().updateStatus("Generating Report"); + + try(FileOutputStream reportOutputStream = new FileOutputStream(tmpFile)) + { + ReportInput reportInput = new ReportInput(runBackendStepInput.getInstance()); + reportInput.setSession(runBackendStepInput.getSession()); + reportInput.setReportName(reportName); + reportInput.setReportFormat(ReportFormat.XLSX); // todo - variable + reportInput.setReportOutputStream(reportOutputStream); + + Map values = runBackendStepInput.getValues(); + reportInput.setInputValues(values); + + new GenerateReportAction().execute(reportInput); + + 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("serverFilePath", tmpFile.getCanonicalPath()); + } + } + catch(Exception e) + { + throw (new QException("Error running report", e)); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java new file mode 100644 index 00000000..a80af740 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/PrepareReportStep.java @@ -0,0 +1,79 @@ +/* + * 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.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + ** Process step to prepare for running a report. + ** + ** Checks for input fields - if there are any, it puts them in process value output + ** as inputFieldList (QFieldMetaData objects). + ** If there aren't any input fields, re-routes the process to skip the input screen. + *******************************************************************************/ +public class PrepareReportStep implements BackendStep +{ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + String reportName = runBackendStepInput.getValueString("reportName"); + if(!StringUtils.hasContent(reportName)) + { + throw (new QException("Process value [reportName] was not given.")); + } + + QReportMetaData report = runBackendStepInput.getInstance().getReport(reportName); + if(report == null) + { + throw (new QException("Process named [" + reportName + "] was not found in this instance.")); + } + + ///////////////////////////////////////////////////////////////// + // if there are input fields, communicate them to the frontend // + ///////////////////////////////////////////////////////////////// + if(CollectionUtils.nullSafeHasContents(report.getInputFields())) + { + ArrayList inputFieldList = new ArrayList<>(report.getInputFields()); + runBackendStepOutput.addValue("inputFieldList", inputFieldList); + } + else + { + ////////////////////////////////////////////////////////////// + // no input? re-route the process to skip the input screen // + ////////////////////////////////////////////////////////////// + List stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList()); + stepList.removeIf(s -> s.equals(BasicRunReportProcess.STEP_NAME_INPUT)); + runBackendStepOutput.getProcessState().setStepList(stepList); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Pair.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Pair.java index 4e76dbfa..3fc17e48 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Pair.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Pair.java @@ -28,7 +28,7 @@ import java.util.Objects; /******************************************************************************* ** Simple container for two objects *******************************************************************************/ -public class Pair +public class Pair implements Cloneable { private A a; private B b; @@ -46,6 +46,9 @@ public class Pair + /******************************************************************************* + ** + *******************************************************************************/ @Override public String toString() { @@ -104,4 +107,23 @@ public class Pair { return Objects.hash(a, b); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + @Override + public Pair clone() + { + try + { + return (Pair) super.clone(); + } + catch(CloneNotSupportedException e) + { + throw new AssertionError(); + } + } } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java index d1af5495..126343c0 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -22,6 +22,8 @@ package com.kingsrook.qqq.backend.javalin; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; @@ -106,11 +108,24 @@ public class QJavalinProcessHandler }); }); }); + get("/download/{file}", QJavalinProcessHandler::downloadFile); }); } + /******************************************************************************* + ** + *******************************************************************************/ + private static void downloadFile(Context context) throws FileNotFoundException + { + // todo context.contentType(reportFormat.getMimeType()); + context.header("Content-Disposition", "filename=" + context.pathParam("file")); + context.result(new FileInputStream(context.queryParam("filePath"))); + } + + + /******************************************************************************* ** Init a process (named in path param :process) ** From 1f546d8c7d2674499ff4841a8d2f6e21f301a667 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 20 Sep 2022 12:19:19 -0500 Subject: [PATCH 3/3] 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()); }