QQQ-42 adding data-sources to reports, customizer points

This commit is contained in:
2022-09-23 09:32:18 -05:00
parent f83d2b3fc8
commit 9397934769
17 changed files with 892 additions and 223 deletions

View File

@ -98,6 +98,23 @@
<version>5.2.2</version> <version>5.2.2</version>
</dependency> </dependency>
<!-- the next 3 deps are being added for google drive support -->
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>1.35.2</version>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-drive</artifactId>
<version>v3-rev20220815-2.0.0</version>
</dependency>
<!-- Common deps for all qqq modules --> <!-- Common deps for all qqq modules -->
<dependency> <dependency>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@ -49,6 +49,8 @@ public class FormulaInterpreter
** **
*******************************************************************************/ *******************************************************************************/
public static Serializable interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula) throws QFormulaException public static Serializable interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula) throws QFormulaException
{
try
{ {
List<Serializable> results = interpretFormula(variableInterpreter, formula, new AtomicInteger(0)); List<Serializable> results = interpretFormula(variableInterpreter, formula, new AtomicInteger(0));
if(results.size() == 1) if(results.size() == 1)
@ -64,6 +66,11 @@ public class FormulaInterpreter
throw (new QFormulaException("More than 1 result from formula")); throw (new QFormulaException("More than 1 result from formula"));
} }
} }
catch(Exception e)
{
throw (new QFormulaException("Error interpreting formula [" + formula + "]", e));
}
}

View File

@ -29,15 +29,19 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportViewCustomizer;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QFormulaException; import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException; import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
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.ExportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; 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.actions.reporting.ReportInput;
@ -48,11 +52,13 @@ 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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType; import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -63,7 +69,16 @@ import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
/******************************************************************************* /*******************************************************************************
** Action to generate a report!! ** Action to generate a report.
**
** A report can contain 1 or more Data Sources - e.g., tables + filters that define
** data that goes into the report.
**
** A report can also contain 1 or more Views - e.g., sheets in a spreadsheet workbook.
** (how do those work in non-XLSX formats??). Views can either be plain tables,
** summaries (like pivot tables, but called summary to avoid confusion with "native"
** pivot tables), or native pivot tables (not initially supported, due to lack of
** support in fastexcel...).
*******************************************************************************/ *******************************************************************************/
public class GenerateReportAction public class GenerateReportAction
{ {
@ -76,7 +91,6 @@ public class GenerateReportAction
Map<String, AggregatesInterface<?>> totalAggregates = new HashMap<>(); Map<String, AggregatesInterface<?>> totalAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> varianceTotalAggregates = new HashMap<>(); Map<String, AggregatesInterface<?>> varianceTotalAggregates = new HashMap<>();
private boolean includeTableView = false;
private QReportMetaData report; private QReportMetaData report;
private ReportFormat reportFormat; private ReportFormat reportFormat;
private ExportStreamerInterface reportStreamer; private ExportStreamerInterface reportStreamer;
@ -89,19 +103,52 @@ public class GenerateReportAction
public void execute(ReportInput reportInput) throws QException public void execute(ReportInput reportInput) throws QException
{ {
report = reportInput.getInstance().getReport(reportInput.getReportName()); report = reportInput.getInstance().getReport(reportInput.getReportName());
Optional<QReportView> tableView = report.getViews().stream().filter(v -> v.getType().equals(ReportType.TABLE)).findFirst();
reportFormat = reportInput.getReportFormat(); reportFormat = reportInput.getReportFormat();
reportStreamer = reportFormat.newReportStreamer(); reportStreamer = reportFormat.newReportStreamer();
if(tableView.isPresent()) for(QReportDataSource dataSource : report.getDataSources())
{ {
includeTableView = true; List<QReportView> dataSourceTableViews = report.getViews().stream()
startTableView(reportInput, tableView.get()); .filter(v -> v.getType().equals(ReportType.TABLE))
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList();
List<QReportView> dataSourcePivotViews = report.getViews().stream()
.filter(v -> v.getType().equals(ReportType.SUMMARY))
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList();
List<QReportView> dataSourceVariantViews = report.getViews().stream()
.filter(v -> v.getType().equals(ReportType.SUMMARY))
.filter(v -> v.getVarianceDataSourceName() != null && v.getVarianceDataSourceName().equals(dataSource.getName()))
.toList();
if(dataSourceTableViews.isEmpty())
{
if(!dataSourcePivotViews.isEmpty() || !dataSourceVariantViews.isEmpty())
{
gatherData(reportInput, dataSource, null, dataSourcePivotViews, dataSourceVariantViews);
}
}
else
{
for(QReportView dataSourceTableView : dataSourceTableViews)
{
if(dataSourceTableView.getViewCustomizer() != null)
{
Function<QReportView, QReportView> viewCustomizerFunction = QCodeLoader.getFunction(dataSourceTableView.getViewCustomizer());
if(viewCustomizerFunction instanceof ReportViewCustomizer reportViewCustomizer)
{
reportViewCustomizer.setReportInput(reportInput);
}
dataSourceTableView = viewCustomizerFunction.apply(dataSourceTableView.clone()); // todo - will this throw concurrent mod exception??
} }
gatherData(reportInput); startTableView(reportInput, dataSource, dataSourceTableView);
gatherVarianceData(reportInput); gatherData(reportInput, dataSource, dataSourceTableView, dataSourcePivotViews, dataSourceVariantViews);
}
}
}
outputPivots(reportInput); outputPivots(reportInput);
} }
@ -111,9 +158,9 @@ public class GenerateReportAction
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private void startTableView(ReportInput reportInput, QReportView reportView) throws QReportingException private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView) throws QReportingException
{ {
QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
ExportInput exportInput = new ExportInput(reportInput.getInstance()); ExportInput exportInput = new ExportInput(reportInput.getInstance());
exportInput.setSession(reportInput.getSession()); exportInput.setSession(reportInput.getSession());
@ -128,10 +175,17 @@ public class GenerateReportAction
{ {
fields = new ArrayList<>(); fields = new ArrayList<>();
for(QReportField column : reportView.getColumns()) for(QReportField column : reportView.getColumns())
{
if(column.getIsVirtual())
{
fields.add(column.toField());
}
else
{ {
fields.add(table.getField(column.getName())); fields.add(table.getField(column.getName()));
} }
} }
}
else else
{ {
fields = new ArrayList<>(table.getFields().values()); fields = new ArrayList<>(table.getFields().values());
@ -145,50 +199,70 @@ public class GenerateReportAction
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private void gatherData(ReportInput reportInput) throws QException private void gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List<QReportView> pivotViews, List<QReportView> variantViews) throws QException
{ {
QQueryFilter queryFilter = report.getQueryFilter(); QQueryFilter queryFilter = dataSource.getQueryFilter();
setInputValuesInQueryFilter(reportInput, queryFilter); setInputValuesInQueryFilter(reportInput, queryFilter);
////////////////////////////////////////////////////////////////////////////////////////
// check if this view has a transform step - if so, set it up now and run its pre-run //
////////////////////////////////////////////////////////////////////////////////////////
AbstractTransformStep transformStep = null;
RunBackendStepInput transformStepInput = null;
RunBackendStepOutput transformStepOutput = null;
if(tableView != null && tableView.getRecordTransformStep() != null)
{
transformStep = QCodeLoader.getBackendStep(AbstractTransformStep.class, tableView.getRecordTransformStep());
transformStepInput = new RunBackendStepInput(reportInput.getInstance());
transformStepInput.setSession(reportInput.getSession());
transformStepInput.setValues(reportInput.getInputValues());
transformStepOutput = new RunBackendStepOutput();
transformStep.preRun(transformStepInput, transformStepOutput);
}
////////////////////////////////////////////////////////////////////
// create effectively-final versions of these vars for the lambda //
////////////////////////////////////////////////////////////////////
AbstractTransformStep finalTransformStep = transformStep;
RunBackendStepInput finalTransformStepInput = transformStepInput;
RunBackendStepOutput finalTransformStepOutput = transformStepOutput;
/////////////////////////////////////////////////////////////////
// run a record pipe loop, over the query for this data source //
/////////////////////////////////////////////////////////////////
RecordPipe recordPipe = new RecordPipe(); RecordPipe recordPipe = new RecordPipe();
new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) -> new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) ->
{ {
QueryInput queryInput = new QueryInput(reportInput.getInstance()); QueryInput queryInput = new QueryInput(reportInput.getInstance());
queryInput.setSession(reportInput.getSession()); queryInput.setSession(reportInput.getSession());
queryInput.setRecordPipe(recordPipe); queryInput.setRecordPipe(recordPipe);
queryInput.setTableName(report.getSourceTable()); queryInput.setTableName(dataSource.getSourceTable());
queryInput.setFilter(queryFilter); queryInput.setFilter(queryFilter);
queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this? queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this?
return (new QueryAction().execute(queryInput)); return (new QueryAction().execute(queryInput));
}, () -> consumeRecords(reportInput, recordPipe, false)); }, () ->
{
List<QRecord> records = recordPipe.consumeAvailableRecords();
if(finalTransformStep != null)
{
finalTransformStepInput.setRecords(records);
finalTransformStep.run(finalTransformStepInput, finalTransformStepOutput);
records = finalTransformStepOutput.getRecords();
} }
return (consumeRecords(reportInput, dataSource, records, tableView, pivotViews, variantViews));
});
////////////////////////////////////////////////
/******************************************************************************* // if there's a transformer, run its post-run //
** ////////////////////////////////////////////////
*******************************************************************************/ if(transformStep != null)
private void gatherVarianceData(ReportInput reportInput) throws QException
{ {
QQueryFilter varianceQueryFilter = report.getVarianceQueryFilter(); transformStep.postRun(transformStepInput, transformStepOutput);
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));
} }
@ -227,11 +301,14 @@ public class GenerateReportAction
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private Integer consumeRecords(ReportInput reportInput, RecordPipe recordPipe, boolean isForVariance) throws QReportingException private Integer consumeRecords(ReportInput reportInput, QReportDataSource dataSource, List<QRecord> records, QReportView tableView, List<QReportView> pivotViews, List<QReportView> variantViews) throws QException
{ {
List<QRecord> records = recordPipe.consumeAvailableRecords(); QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
if(includeTableView && !isForVariance) ////////////////////////////////////////////////////////////////////////////
// if this record goes on a table view, add it to the report streamer now //
////////////////////////////////////////////////////////////////////////////
if(tableView != null)
{ {
reportStreamer.addRecords(records); reportStreamer.addRecords(records);
} }
@ -239,20 +316,38 @@ public class GenerateReportAction
////////////////////////////// //////////////////////////////
// do aggregates for pivots // // do aggregates for pivots //
////////////////////////////// //////////////////////////////
QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); if(pivotViews != null)
report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).forEach((view) ->
{ {
addRecordsToPivotAggregates(view, table, records, isForVariance ? variancePivotAggregates : pivotAggregates); for(QReportView pivotView : pivotViews)
}); {
addRecordsToPivotAggregates(pivotView, table, records, pivotAggregates);
}
}
if(variantViews != null)
{
for(QReportView variantView : variantViews)
{
addRecordsToPivotAggregates(variantView, table, records, variancePivotAggregates);
}
}
/////////////////////////////////////////// ///////////////////////////////////////////
// do totals too, if any views want them // // do totals too, if any views want them //
/////////////////////////////////////////// ///////////////////////////////////////////
if(report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).anyMatch(QReportView::getTotalRow)) if(pivotViews != null && pivotViews.stream().anyMatch(QReportView::getTotalRow))
{ {
for(QRecord record : records) for(QRecord record : records)
{ {
addRecordToAggregatesMap(table, record, isForVariance ? varianceTotalAggregates : totalAggregates); addRecordToAggregatesMap(table, record, totalAggregates);
}
}
if(variantViews != null && variantViews.stream().anyMatch(QReportView::getTotalRow))
{
for(QRecord record : records)
{
addRecordToAggregatesMap(table, record, varianceTotalAggregates);
} }
} }
@ -337,11 +432,11 @@ public class GenerateReportAction
*******************************************************************************/ *******************************************************************************/
private void outputPivots(ReportInput reportInput) throws QReportingException, QFormulaException private void outputPivots(ReportInput reportInput) throws QReportingException, QFormulaException
{ {
QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); List<QReportView> reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
List<QReportView> reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).toList();
for(QReportView view : reportViews) for(QReportView view : reportViews)
{ {
QReportDataSource dataSource = report.getDataSource(view.getDataSourceName());
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
PivotOutput pivotOutput = computePivotRowsForView(reportInput, view, table); PivotOutput pivotOutput = computePivotRowsForView(reportInput, view, table);
ExportInput exportInput = new ExportInput(reportInput.getInstance()); ExportInput exportInput = new ExportInput(reportInput.getInstance());
@ -564,11 +659,14 @@ public class GenerateReportAction
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(totalAggregates)); variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(totalAggregates));
variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceTotalAggregates)); variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceTotalAggregates));
HashMap<String, Serializable> thisRowValues = new HashMap<>();
variableInterpreter.addValueMap("thisRow", thisRowValues);
for(QReportField column : view.getColumns()) for(QReportField column : view.getColumns())
{ {
Serializable serializable = getValueForColumn(variableInterpreter, column); Serializable serializable = getValueForColumn(variableInterpreter, column);
totalRow.setValue(column.getName(), serializable); totalRow.setValue(column.getName(), serializable);
thisRowValues.put(column.getName(), serializable);
String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable); String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable);
System.out.printf("%25s", formatted); System.out.printf("%25s", formatted);

View File

@ -46,8 +46,9 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
private ExportInput exportInput; private ExportInput exportInput;
private List<QFieldMetaData> fields; private List<QFieldMetaData> fields;
private static List<Map<String, String>> list = new ArrayList<>(); private static Map<String, List<Map<String, String>>> rows = new LinkedHashMap<>();
private static List<String> headers = new ArrayList<>(); private static Map<String, List<String>> headers = new LinkedHashMap<>();
private static String currentSheetLabel;
@ -60,13 +61,25 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
/*******************************************************************************
**
*******************************************************************************/
public static void reset()
{
rows.clear();
headers.clear();
currentSheetLabel = null;
}
/******************************************************************************* /*******************************************************************************
** Getter for list ** Getter for list
** **
*******************************************************************************/ *******************************************************************************/
public static List<Map<String, String>> getList() public static List<Map<String, String>> getList(String name)
{ {
return (list); return (rows.get(name));
} }
@ -80,10 +93,13 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
this.exportInput = exportInput; this.exportInput = exportInput;
this.fields = fields; this.fields = fields;
headers = new ArrayList<>(); currentSheetLabel = label;
rows.put(label, new ArrayList<>());
headers.put(label, new ArrayList<>());
for(QFieldMetaData field : fields) for(QFieldMetaData field : fields)
{ {
headers.add(field.getLabel()); headers.get(label).add(field.getLabel());
} }
} }
@ -112,10 +128,10 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
private void addRecord(QRecord qRecord) private void addRecord(QRecord qRecord)
{ {
Map<String, String> row = new LinkedHashMap<>(); Map<String, String> row = new LinkedHashMap<>();
list.add(row); rows.get(currentSheetLabel).add(row);
for(int i = 0; i < fields.size(); i++) for(int i = 0; i < fields.size(); i++)
{ {
row.put(headers.get(i), qRecord.getValueString(fields.get(i).getName())); row.put(headers.get(currentSheetLabel).get(i), qRecord.getValueString(fields.get(i).getName()));
} }
} }
@ -125,7 +141,7 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
** **
*******************************************************************************/ *******************************************************************************/
@Override @Override
public void addTotalsRow(QRecord record) throws QReportingException public void addTotalsRow(QRecord record)
{ {
addRecord(record); addRecord(record);
} }

View File

@ -0,0 +1,41 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.customizers;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
/*******************************************************************************
**
*******************************************************************************/
public interface ReportViewCustomizer extends Function<QReportView, QReportView>
{
/*******************************************************************************
**
*******************************************************************************/
void setReportInput(ReportInput reportInput);
}

View File

@ -266,7 +266,7 @@ public class QInstanceEnricher
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
static String nameToLabel(String name) public static String nameToLabel(String name)
{ {
if(!StringUtils.hasContent(name)) if(!StringUtils.hasContent(name))
{ {

View File

@ -37,7 +37,6 @@ public class QFrontendReportMetaData
{ {
private String name; private String name;
private String label; private String label;
private String tableName;
private String processName; private String processName;
private String iconName; private String iconName;
@ -55,7 +54,6 @@ public class QFrontendReportMetaData
{ {
this.name = reportMetaData.getName(); this.name = reportMetaData.getName();
this.label = reportMetaData.getLabel(); this.label = reportMetaData.getLabel();
this.tableName = reportMetaData.getSourceTable();
this.processName = reportMetaData.getProcessName(); this.processName = reportMetaData.getProcessName();
if(reportMetaData.getIcon() != null) if(reportMetaData.getIcon() != null)
@ -88,17 +86,6 @@ public class QFrontendReportMetaData
/*******************************************************************************
** Getter for primaryKeyField
**
*******************************************************************************/
public String getTableName()
{
return tableName;
}
/******************************************************************************* /*******************************************************************************
** Getter for processName ** Getter for processName
** **

View File

@ -34,7 +34,8 @@ public enum QComponentType
VIEW_FORM, VIEW_FORM,
DOWNLOAD_FORM, DOWNLOAD_FORM,
RECORD_LIST, RECORD_LIST,
PROCESS_SUMMARY_RESULTS; PROCESS_SUMMARY_RESULTS,
GOOGLE_DRIVE_SELECT_FOLDER;
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// keep these values in sync with QComponentType.ts in qqq-frontend-core // // keep these values in sync with QComponentType.ts in qqq-frontend-core //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,139 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.reporting;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
/*******************************************************************************
**
*******************************************************************************/
public class QReportDataSource
{
private String name;
private String sourceTable;
private QQueryFilter queryFilter;
/*******************************************************************************
** Getter for name
**
*******************************************************************************/
public String getName()
{
return name;
}
/*******************************************************************************
** Setter for name
**
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
**
*******************************************************************************/
public QReportDataSource withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for sourceTable
**
*******************************************************************************/
public String getSourceTable()
{
return sourceTable;
}
/*******************************************************************************
** Setter for sourceTable
**
*******************************************************************************/
public void setSourceTable(String sourceTable)
{
this.sourceTable = sourceTable;
}
/*******************************************************************************
** Fluent setter for sourceTable
**
*******************************************************************************/
public QReportDataSource withSourceTable(String sourceTable)
{
this.sourceTable = sourceTable;
return (this);
}
/*******************************************************************************
** Getter for queryFilter
**
*******************************************************************************/
public QQueryFilter getQueryFilter()
{
return queryFilter;
}
/*******************************************************************************
** Setter for queryFilter
**
*******************************************************************************/
public void setQueryFilter(QQueryFilter queryFilter)
{
this.queryFilter = queryFilter;
}
/*******************************************************************************
** Fluent setter for queryFilter
**
*******************************************************************************/
public QReportDataSource withQueryFilter(QQueryFilter queryFilter)
{
this.queryFilter = queryFilter;
return (this);
}
}

View File

@ -22,6 +22,10 @@
package com.kingsrook.qqq.backend.core.model.metadata.reporting; package com.kingsrook.qqq.backend.core.model.metadata.reporting;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
/******************************************************************************* /*******************************************************************************
** Field within a report ** Field within a report
*******************************************************************************/ *******************************************************************************/
@ -29,9 +33,29 @@ public class QReportField
{ {
private String name; private String name;
private String label; private String label;
private QFieldType type;
private String formula; private String formula;
private String displayFormat; private String displayFormat;
// todo - type?
///////////////////////////////////////////////////////////////////////////
// Noew: new attributes added here probably belong in the toField method //
///////////////////////////////////////////////////////////////////////////
private boolean isVirtual = false;
/*******************************************************************************
**
*******************************************************************************/
public QFieldMetaData toField()
{
return new QFieldMetaData()
.withName(name)
.withLabel(label)
.withType(type)
.withDisplayFormat(displayFormat);
}
@ -103,6 +127,40 @@ public class QReportField
/*******************************************************************************
** Getter for type
**
*******************************************************************************/
public QFieldType getType()
{
return type;
}
/*******************************************************************************
** Setter for type
**
*******************************************************************************/
public void setType(QFieldType type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
**
*******************************************************************************/
public QReportField withType(QFieldType type)
{
this.type = type;
return (this);
}
/******************************************************************************* /*******************************************************************************
** Getter for formula ** Getter for formula
** **
@ -169,4 +227,37 @@ public class QReportField
return (this); return (this);
} }
/*******************************************************************************
** Getter for isVirtual
**
*******************************************************************************/
public boolean getIsVirtual()
{
return isVirtual;
}
/*******************************************************************************
** Setter for isVirtual
**
*******************************************************************************/
public void setIsVirtual(boolean isVirtual)
{
this.isVirtual = isVirtual;
}
/*******************************************************************************
** Fluent setter for isVirtual
**
*******************************************************************************/
public QReportField withIsVirtual(boolean isVirtual)
{
this.isVirtual = isVirtual;
return (this);
}
} }

View File

@ -23,10 +23,10 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting;
import java.util.List; 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.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/******************************************************************************* /*******************************************************************************
@ -36,11 +36,11 @@ public class QReportMetaData implements QAppChildMetaData
{ {
private String name; private String name;
private String label; private String label;
private List<QFieldMetaData> inputFields;
private String sourceTable;
private String processName; private String processName;
private QQueryFilter queryFilter; private List<QFieldMetaData> inputFields;
private QQueryFilter varianceQueryFilter;
private List<QReportDataSource> dataSources;
private List<QReportView> views; private List<QReportView> views;
private String parentAppName; private String parentAppName;
@ -150,40 +150,6 @@ public class QReportMetaData implements QAppChildMetaData
/*******************************************************************************
** Getter for sourceTable
**
*******************************************************************************/
public String getSourceTable()
{
return sourceTable;
}
/*******************************************************************************
** Setter for sourceTable
**
*******************************************************************************/
public void setSourceTable(String sourceTable)
{
this.sourceTable = sourceTable;
}
/*******************************************************************************
** Fluent setter for sourceTable
**
*******************************************************************************/
public QReportMetaData withSourceTable(String sourceTable)
{
this.sourceTable = sourceTable;
return (this);
}
/******************************************************************************* /*******************************************************************************
** Getter for processName ** Getter for processName
** **
@ -219,68 +185,34 @@ public class QReportMetaData implements QAppChildMetaData
/******************************************************************************* /*******************************************************************************
** Getter for queryFilter ** Getter for dataSources
** **
*******************************************************************************/ *******************************************************************************/
public QQueryFilter getQueryFilter() public List<QReportDataSource> getDataSources()
{ {
return queryFilter; return dataSources;
} }
/******************************************************************************* /*******************************************************************************
** Setter for queryFilter ** Setter for dataSources
** **
*******************************************************************************/ *******************************************************************************/
public void setQueryFilter(QQueryFilter queryFilter) public void setDataSources(List<QReportDataSource> dataSources)
{ {
this.queryFilter = queryFilter; this.dataSources = dataSources;
} }
/******************************************************************************* /*******************************************************************************
** Fluent setter for queryFilter ** Fluent setter for dataSources
** **
*******************************************************************************/ *******************************************************************************/
public QReportMetaData withQueryFilter(QQueryFilter queryFilter) public QReportMetaData withDataSources(List<QReportDataSource> dataSources)
{ {
this.queryFilter = queryFilter; this.dataSources = dataSources;
return (this);
}
/*******************************************************************************
** 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); return (this);
} }
@ -374,4 +306,22 @@ public class QReportMetaData implements QAppChildMetaData
return (this); return (this);
} }
/*******************************************************************************
**
*******************************************************************************/
public QReportDataSource getDataSource(String dataSourceName)
{
for(QReportDataSource dataSource : CollectionUtils.nonNullList(dataSources))
{
if(dataSource.getName().equals(dataSourceName))
{
return (dataSource);
}
}
return (null);
}
} }

View File

@ -22,17 +22,21 @@
package com.kingsrook.qqq.backend.core.model.metadata.reporting; package com.kingsrook.qqq.backend.core.model.metadata.reporting;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public class QReportView public class QReportView implements Cloneable
{ {
private String name; private String name;
private String label; private String label;
private String dataSourceName;
private String varianceDataSourceName;
private ReportType type; private ReportType type;
private String titleFormat; private String titleFormat;
private List<String> titleFields; private List<String> titleFields;
@ -42,6 +46,13 @@ public class QReportView
private List<QReportField> columns; private List<QReportField> columns;
private List<QFilterOrderBy> orderByFields; private List<QFilterOrderBy> orderByFields;
private QCodeReference recordTransformStep;
private QCodeReference viewCustomizer;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Note: This class is Cloneable - think about if new fields added here need deep-copied in the clone method! //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/******************************************************************************* /*******************************************************************************
@ -112,6 +123,74 @@ public class QReportView
/*******************************************************************************
** Getter for dataSourceName
**
*******************************************************************************/
public String getDataSourceName()
{
return dataSourceName;
}
/*******************************************************************************
** Setter for dataSourceName
**
*******************************************************************************/
public void setDataSourceName(String dataSourceName)
{
this.dataSourceName = dataSourceName;
}
/*******************************************************************************
** Fluent setter for dataSourceName
**
*******************************************************************************/
public QReportView withDataSourceName(String dataSourceName)
{
this.dataSourceName = dataSourceName;
return (this);
}
/*******************************************************************************
** Getter for varianceDataSourceName
**
*******************************************************************************/
public String getVarianceDataSourceName()
{
return varianceDataSourceName;
}
/*******************************************************************************
** Setter for varianceDataSourceName
**
*******************************************************************************/
public void setVarianceDataSourceName(String varianceDataSourceName)
{
this.varianceDataSourceName = varianceDataSourceName;
}
/*******************************************************************************
** Fluent setter for varianceDataSourceName
**
*******************************************************************************/
public QReportView withVarianceDataSourceName(String varianceDataSourceName)
{
this.varianceDataSourceName = varianceDataSourceName;
return (this);
}
/******************************************************************************* /*******************************************************************************
** Getter for type ** Getter for type
** **
@ -382,4 +461,114 @@ public class QReportView
return (this); return (this);
} }
/*******************************************************************************
** Getter for recordTransformerStep
**
*******************************************************************************/
public QCodeReference getRecordTransformStep()
{
return recordTransformStep;
}
/*******************************************************************************
** Setter for recordTransformerStep
**
*******************************************************************************/
public void setRecordTransformStep(QCodeReference recordTransformStep)
{
this.recordTransformStep = recordTransformStep;
}
/*******************************************************************************
** Fluent setter for recordTransformerStep
**
*******************************************************************************/
public QReportView withRecordTransformStep(QCodeReference recordTransformerStep)
{
this.recordTransformStep = recordTransformerStep;
return (this);
}
/*******************************************************************************
** Getter for viewCustomizer
**
*******************************************************************************/
public QCodeReference getViewCustomizer()
{
return viewCustomizer;
}
/*******************************************************************************
** Setter for viewCustomizer
**
*******************************************************************************/
public void setViewCustomizer(QCodeReference viewCustomizer)
{
this.viewCustomizer = viewCustomizer;
}
/*******************************************************************************
** Fluent setter for viewCustomizer
**
*******************************************************************************/
public QReportView withViewCustomizer(QCodeReference viewCustomizer)
{
this.viewCustomizer = viewCustomizer;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QReportView clone()
{
try
{
QReportView clone = (QReportView) super.clone();
/////////////////////////
// copy any lists, etc //
/////////////////////////
if(titleFields != null)
{
clone.setTitleFields(new ArrayList<>(titleFields));
}
if(pivotFields != null)
{
clone.setPivotFields(new ArrayList<>(pivotFields));
}
if(columns != null)
{
clone.setColumns(new ArrayList<>(columns));
}
if(orderByFields != null)
{
clone.setOrderByFields(new ArrayList<>(orderByFields));
}
return clone;
}
catch(CloneNotSupportedException e)
{
throw new AssertionError();
}
}
} }

View File

@ -27,6 +27,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting;
*******************************************************************************/ *******************************************************************************/
public enum ReportType public enum ReportType
{ {
PIVOT, TABLE, // e.g., raw data in tabular form.
TABLE SUMMARY, // e.g., summaries computed within QQQ
PIVOT // e.g., a true spreadsheet pivot. Not initially supported...
} }

View File

@ -398,7 +398,7 @@ public class CollectionUtils
else else
{ {
endAt = startAt + limit; endAt = startAt + limit;
if (endAt > list.size()) if(endAt > list.size())
{ {
endAt = list.size(); endAt = list.size();
} }
@ -406,4 +406,20 @@ public class CollectionUtils
return list.subList(startAt, endAt); return list.subList(startAt, endAt);
} }
/*******************************************************************************
** Returns the input list, unless it was null - in which case a new array list is returned.
**
** Meant to help avoid null checks on foreach loops.
*******************************************************************************/
public static <T> List<T> nonNullList(List<T> list)
{
if(list == null)
{
return (new ArrayList<>());
}
return (list);
}
} }

View File

@ -93,12 +93,12 @@ class FormulaInterpreterTest
QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter(); QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter();
vi.addValueMap("input", Map.of("i", 5, "c", 'c')); vi.addValueMap("input", Map.of("i", 5, "c", 'c'));
assertThatThrownBy(() -> interpretFormula(vi, "")).hasMessageContaining("No results"); assertThatThrownBy(() -> interpretFormula(vi, "")).hasRootCauseMessage("No results from formula");
assertThatThrownBy(() -> interpretFormula(vi, "NOT-A-FUN(1,2)")).hasMessageContaining("unrecognized expression"); assertThatThrownBy(() -> interpretFormula(vi, "NOT-A-FUN(1,2)")).hasRootCauseMessage("Unable to evaluate unrecognized expression: NOT-A-FUN");
assertThatThrownBy(() -> interpretFormula(vi, "ADD(1)")).hasMessageContaining("Wrong number of arguments"); assertThatThrownBy(() -> interpretFormula(vi, "ADD(1)")).hasRootCauseMessage("Wrong number of arguments (required: 2, received: 1)");
assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,2,3)")).hasMessageContaining("Wrong number of arguments"); assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,2,3)")).hasRootCauseMessage("Wrong number of arguments (required: 2, received: 3)");
assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,A)")).hasMessageContaining("[A] as a number"); assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,A)")).hasRootCauseMessage("Could not process [A] as a number");
assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,${input.c})")).hasMessageContaining("[c] as a number"); assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,${input.c})")).hasRootCauseMessage("Could not process [c] as a number");
// todo - bad syntax (e.g., missing ')' // todo - bad syntax (e.g., missing ')'
} }
@ -168,7 +168,7 @@ class FormulaInterpreterTest
assertTrue((Boolean) interpretFormula(vi, "GTE(${input.two},${input.one})")); assertTrue((Boolean) interpretFormula(vi, "GTE(${input.two},${input.one})"));
// todo - google sheets compares strings differently... // todo - google sheets compares strings differently...
assertThatThrownBy(() -> interpretFormula(vi, "LT(${input.foo},${input.one})")).hasMessageContaining("[bar] as a number"); assertThatThrownBy(() -> interpretFormula(vi, "LT(${input.foo},${input.one})")).hasRootCauseMessage("Could not process [bar] as a number");
} }

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.Month; import java.time.Month;
@ -41,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
@ -72,7 +74,7 @@ public class GenerateReportActionTest
@AfterEach @AfterEach
void beforeAndAfterEach() void beforeAndAfterEach()
{ {
ListOfMapsExportStreamer.getList().clear(); ListOfMapsExportStreamer.reset();
MemoryRecordStore.getInstance().reset(); MemoryRecordStore.getInstance().reset();
} }
@ -85,11 +87,11 @@ public class GenerateReportActionTest
void testPivot1() throws QException void testPivot1() throws QException
{ {
QInstance qInstance = TestUtils.defineInstance(); QInstance qInstance = TestUtils.defineInstance();
qInstance.addReport(defineReport(true)); qInstance.addReport(definePersonShoesPivotReport(true));
insertPersonRecords(qInstance); insertPersonRecords(qInstance);
runReport(qInstance, LocalDate.of(1980, Month.JANUARY, 1), LocalDate.of(1980, Month.DECEMBER, 31)); runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1), "endDate", LocalDate.of(1980, Month.DECEMBER, 31)));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList(); List<Map<String, String>> list = ListOfMapsExportStreamer.getList("pivot");
Iterator<Map<String, String>> iterator = list.iterator(); Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next(); Map<String, String> row = iterator.next();
assertEquals(3, list.size()); assertEquals(3, list.size());
@ -140,7 +142,7 @@ public class GenerateReportActionTest
void testPivot2() throws QException void testPivot2() throws QException
{ {
QInstance qInstance = TestUtils.defineInstance(); QInstance qInstance = TestUtils.defineInstance();
QReportMetaData report = defineReport(false); QReportMetaData report = definePersonShoesPivotReport(false);
////////////////////////////////////////////// //////////////////////////////////////////////
// change from the default to sort reversed // // change from the default to sort reversed //
@ -148,9 +150,9 @@ public class GenerateReportActionTest
report.getViews().get(0).getOrderByFields().get(0).setIsAscending(false); report.getViews().get(0).getOrderByFields().get(0).setIsAscending(false);
qInstance.addReport(report); qInstance.addReport(report);
insertPersonRecords(qInstance); insertPersonRecords(qInstance);
runReport(qInstance, LocalDate.of(1980, Month.JANUARY, 1), LocalDate.of(1980, Month.DECEMBER, 31)); runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1), "endDate", LocalDate.of(1980, Month.DECEMBER, 31)));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList(); List<Map<String, String>> list = ListOfMapsExportStreamer.getList("pivot");
Iterator<Map<String, String>> iterator = list.iterator(); Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next(); Map<String, String> row = iterator.next();
assertEquals(2, list.size()); assertEquals(2, list.size());
@ -172,19 +174,19 @@ public class GenerateReportActionTest
void testPivot3() throws QException void testPivot3() throws QException
{ {
QInstance qInstance = TestUtils.defineInstance(); QInstance qInstance = TestUtils.defineInstance();
QReportMetaData report = defineReport(false); QReportMetaData report = definePersonShoesPivotReport(false);
////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////
// remove the filters, change to sort by personCount (to get some ties), then sumPrice desc // // remove the filters, change to sort by personCount (to get some ties), then sumPrice desc //
// this also shows the behavior of a null value in an order by // // this also shows the behavior of a null value in an order by //
////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////
report.setQueryFilter(null); report.getDataSources().get(0).getQueryFilter().setCriteria(null);
report.getViews().get(0).setOrderByFields(List.of(new QFilterOrderBy("personCount"), new QFilterOrderBy("sumPrice", false))); report.getViews().get(0).setOrderByFields(List.of(new QFilterOrderBy("personCount"), new QFilterOrderBy("sumPrice", false)));
qInstance.addReport(report); qInstance.addReport(report);
insertPersonRecords(qInstance); insertPersonRecords(qInstance);
runReport(qInstance, LocalDate.now(), LocalDate.now()); runReport(qInstance, Map.of("startDate", LocalDate.now(), "endDate", LocalDate.now()));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList(); List<Map<String, String>> list = ListOfMapsExportStreamer.getList("pivot");
Iterator<Map<String, String>> iterator = list.iterator(); Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next(); Map<String, String> row = iterator.next();
@ -224,21 +226,21 @@ public class GenerateReportActionTest
void testPivot4() throws QException void testPivot4() throws QException
{ {
QInstance qInstance = TestUtils.defineInstance(); QInstance qInstance = TestUtils.defineInstance();
QReportMetaData report = defineReport(false); QReportMetaData report = definePersonShoesPivotReport(false);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// remove the filter, change to have 2 pivot columns - homeStateId and lastName - we should get no roll-up like this. // // remove the filter, change to have 2 pivot columns - homeStateId and lastName - we should get no roll-up like this. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
report.setQueryFilter(null); report.getDataSources().get(0).getQueryFilter().setCriteria(null);
report.getViews().get(0).setPivotFields(List.of( report.getViews().get(0).setPivotFields(List.of(
"homeStateId", "homeStateId",
"lastName" "lastName"
)); ));
qInstance.addReport(report); qInstance.addReport(report);
insertPersonRecords(qInstance); insertPersonRecords(qInstance);
runReport(qInstance, LocalDate.now(), LocalDate.now()); runReport(qInstance, Map.of("startDate", LocalDate.now(), "endDate", LocalDate.now()));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList(); List<Map<String, String>> list = ListOfMapsExportStreamer.getList("pivot");
Iterator<Map<String, String>> iterator = list.iterator(); Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next(); Map<String, String> row = iterator.next();
assertEquals(6, list.size()); assertEquals(6, list.size());
@ -282,18 +284,18 @@ public class GenerateReportActionTest
void testPivot5() throws QException void testPivot5() throws QException
{ {
QInstance qInstance = TestUtils.defineInstance(); QInstance qInstance = TestUtils.defineInstance();
QReportMetaData report = defineReport(false); QReportMetaData report = definePersonShoesPivotReport(false);
///////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
// remove the filter, and just pivot on homeStateId - should aggregate differently // // remove the filter, and just pivot on homeStateId - should aggregate differently //
///////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
report.setQueryFilter(null); report.getDataSources().get(0).getQueryFilter().setCriteria(null);
report.getViews().get(0).setPivotFields(List.of("homeStateId")); report.getViews().get(0).setPivotFields(List.of("homeStateId"));
qInstance.addReport(report); qInstance.addReport(report);
insertPersonRecords(qInstance); insertPersonRecords(qInstance);
runReport(qInstance, LocalDate.now(), LocalDate.now()); runReport(qInstance, Map.of("startDate", LocalDate.now(), "endDate", LocalDate.now()));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList(); List<Map<String, String>> list = ListOfMapsExportStreamer.getList("pivot");
Iterator<Map<String, String>> iterator = list.iterator(); Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next(); Map<String, String> row = iterator.next();
assertEquals(2, list.size()); assertEquals(2, list.size());
@ -319,7 +321,7 @@ public class GenerateReportActionTest
try(FileOutputStream fileOutputStream = new FileOutputStream(name)) try(FileOutputStream fileOutputStream = new FileOutputStream(name))
{ {
QInstance qInstance = TestUtils.defineInstance(); QInstance qInstance = TestUtils.defineInstance();
qInstance.addReport(defineReport(true)); qInstance.addReport(definePersonShoesPivotReport(true));
insertPersonRecords(qInstance); insertPersonRecords(qInstance);
ReportInput reportInput = new ReportInput(qInstance); ReportInput reportInput = new ReportInput(qInstance);
@ -345,7 +347,7 @@ public class GenerateReportActionTest
try(FileOutputStream fileOutputStream = new FileOutputStream(name)) try(FileOutputStream fileOutputStream = new FileOutputStream(name))
{ {
QInstance qInstance = TestUtils.defineInstance(); QInstance qInstance = TestUtils.defineInstance();
qInstance.addReport(defineReport(true)); qInstance.addReport(definePersonShoesPivotReport(true));
insertPersonRecords(qInstance); insertPersonRecords(qInstance);
ReportInput reportInput = new ReportInput(qInstance); ReportInput reportInput = new ReportInput(qInstance);
@ -364,14 +366,14 @@ public class GenerateReportActionTest
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private void runReport(QInstance qInstance, LocalDate startDate, LocalDate endDate) throws QException private void runReport(QInstance qInstance, Map<String, Serializable> inputValues) throws QException
{ {
ReportInput reportInput = new ReportInput(qInstance); ReportInput reportInput = new ReportInput(qInstance);
reportInput.setSession(new QSession()); reportInput.setSession(new QSession());
reportInput.setReportName(REPORT_NAME); reportInput.setReportName(REPORT_NAME);
reportInput.setReportFormat(ReportFormat.LIST_OF_MAPS); reportInput.setReportFormat(ReportFormat.LIST_OF_MAPS);
reportInput.setReportOutputStream(new ByteArrayOutputStream()); reportInput.setReportOutputStream(new ByteArrayOutputStream());
reportInput.setInputValues(Map.of("startDate", startDate, "endDate", endDate)); reportInput.setInputValues(inputValues);
new GenerateReportAction().execute(reportInput); new GenerateReportAction().execute(reportInput);
} }
@ -397,23 +399,29 @@ public class GenerateReportActionTest
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public static QReportMetaData defineReport(boolean includeTotalRow) public static QReportMetaData definePersonShoesPivotReport(boolean includeTotalRow)
{ {
return new QReportMetaData() return new QReportMetaData()
.withName(REPORT_NAME) .withName(REPORT_NAME)
.withDataSources(List.of(
new QReportDataSource()
.withName("persons")
.withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY) .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withInputFields(List.of(
new QFieldMetaData("startDate", QFieldType.DATE_TIME),
new QFieldMetaData("endDate", QFieldType.DATE_TIME)
))
.withQueryFilter(new QQueryFilter() .withQueryFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.STARTS_WITH, List.of("K"))) .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.STARTS_WITH, List.of("K")))
.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.BETWEEN, List.of("${input.startDate}", "${input.endDate}"))) .withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.BETWEEN, List.of("${input.startDate}", "${input.endDate}")))
) )
))
.withInputFields(List.of(
new QFieldMetaData("startDate", QFieldType.DATE_TIME),
new QFieldMetaData("endDate", QFieldType.DATE_TIME)
))
.withViews(List.of( .withViews(List.of(
new QReportView() new QReportView()
.withName("pivot") .withName("pivot")
.withType(ReportType.PIVOT) .withLabel("pivot")
.withDataSourceName("persons")
.withType(ReportType.SUMMARY)
.withPivotFields(List.of("lastName")) .withPivotFields(List.of("lastName"))
.withTotalRow(includeTotalRow) .withTotalRow(includeTotalRow)
.withTitleFormat("Number of shoes - people born between %s and %s - pivot on LastName, sort by Quantity, Revenue DESC") .withTitleFormat("Number of shoes - people born between %s and %s - pivot on LastName, sort by Quantity, Revenue DESC")
@ -438,4 +446,112 @@ public class GenerateReportActionTest
)); ));
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableOnlyReport() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QReportMetaData report = new QReportMetaData()
.withName(REPORT_NAME)
.withDataSources(List.of(
new QReportDataSource()
.withName("persons")
.withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, List.of("${input.startDate}")))
)
))
.withInputFields(List.of(
new QFieldMetaData("startDate", QFieldType.DATE_TIME)
))
.withViews(List.of(
new QReportView()
.withName("table1")
.withLabel("table1")
.withDataSourceName("persons")
.withType(ReportType.TABLE)
.withColumns(List.of(
new QReportField().withName("id"),
new QReportField().withName("firstName"),
new QReportField().withName("lastName")
))
));
qInstance.addReport(report);
insertPersonRecords(qInstance);
runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1)));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("table1");
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
assertEquals(5, list.size());
assertThat(row).containsOnlyKeys("Id", "First Name", "Last Name");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTwoTableViewsOneDataSourceReport() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QReportMetaData report = new QReportMetaData()
.withName(REPORT_NAME)
.withDataSources(List.of(
new QReportDataSource()
.withName("persons")
.withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, List.of("${input.startDate}")))
)
))
.withInputFields(List.of(
new QFieldMetaData("startDate", QFieldType.DATE_TIME)
))
.withViews(List.of(
new QReportView()
.withName("table1")
.withLabel("table1")
.withDataSourceName("persons")
.withType(ReportType.TABLE)
.withColumns(List.of(
new QReportField().withName("id"),
new QReportField().withName("firstName"),
new QReportField().withName("lastName")
)),
new QReportView()
.withName("table2")
.withLabel("table2")
.withDataSourceName("persons")
.withType(ReportType.TABLE)
.withColumns(List.of(
new QReportField().withName("birthDate")
))
));
qInstance.addReport(report);
insertPersonRecords(qInstance);
runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1)));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("table1");
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
assertEquals(5, list.size());
assertThat(row).containsOnlyKeys("Id", "First Name", "Last Name");
list = ListOfMapsExportStreamer.getList("table2");
iterator = list.iterator();
row = iterator.next();
assertEquals(5, list.size());
assertThat(row).containsOnlyKeys("Birth Date");
}
} }

View File

@ -49,7 +49,7 @@ class BasicRunReportProcessTest
void testRunReport() throws QException void testRunReport() throws QException
{ {
QInstance instance = TestUtils.defineInstance(); QInstance instance = TestUtils.defineInstance();
QReportMetaData report = GenerateReportActionTest.defineReport(true); QReportMetaData report = GenerateReportActionTest.definePersonShoesPivotReport(true);
QProcessMetaData runReportProcess = BasicRunReportProcess.defineProcessMetaData(); QProcessMetaData runReportProcess = BasicRunReportProcess.defineProcessMetaData();
instance.addReport(report); instance.addReport(report);