QQQ-42 checkpoint of qqq reports

This commit is contained in:
2022-09-19 13:52:43 -05:00
parent b05c5749b4
commit 525389e62e
33 changed files with 1346 additions and 143 deletions

View File

@ -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<String, QFrontendReportMetaData> reports = new LinkedHashMap<>();
for(Map.Entry<String, QReportMetaData> 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 //
///////////////////////////////////

View File

@ -66,7 +66,7 @@ public class CsvExportStreamer implements ExportStreamerInterface
**
*******************************************************************************/
@Override
public void start(ExportInput exportInput, List<QFieldMetaData> fields) throws QReportingException
public void start(ExportInput exportInput, List<QFieldMetaData> 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<QRecord> qRecords) throws QReportingException
{
List<QRecord> qRecords = recordPipe.consumeAvailableRecords();
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
for(QRecord qRecord : qRecords)

View File

@ -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<QFieldMetaData> fields;
private OutputStream outputStream;
private Map<String, String> excelCellFormats;
private ExcelStylerInterface excelStylerInterface = new PlainExcelStyler();
private Map<String, String> excelCellFormats;
private Workbook workbook;
private Worksheet worksheet;
@ -99,17 +103,38 @@ public class ExcelExportStreamer implements ExportStreamerInterface
**
*******************************************************************************/
@Override
public void start(ExportInput exportInput, List<QFieldMetaData> fields) throws QReportingException
public void start(ExportInput exportInput, List<QFieldMetaData> 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<QRecord> qRecords) throws QReportingException
{
List<QRecord> 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();
}

View File

@ -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<QRecord> 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<QRecord> records = recordPipe.consumeAvailableRecords();
int recordsConsumed = reportStreamer.addRecords(records);
recordCount += recordsConsumed;
long reportEndTime = System.currentTimeMillis();

View File

@ -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<QFieldMetaData> fields) throws QReportingException;
void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException;
/*******************************************************************************
** Called as records flow into the pipe.
******************************************************************************/
int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException;
int addRecords(List<QRecord> 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));
}
}

View File

@ -194,6 +194,11 @@ public class FormulaInterpreter
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).setScale(numbers.get(1).intValue(), RoundingMode.HALF_UP));
}
case "NVL":
{
List<BigDecimal> 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)

View File

@ -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<String, Map<PivotKey, Map<String, AggregatesInterface<?>>>> pivotAggregates = new HashMap<>();
Map<String, Map<PivotKey, Map<String, AggregatesInterface<?>>>> pivotAggregates = new HashMap<>();
Map<String, Map<PivotKey, Map<String, AggregatesInterface<?>>>> variancePivotAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> totalAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> totalAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> 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<QReportView> 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<QFieldMetaData> 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<QRecord> 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<QRecord> records)
private void addRecordsToPivotAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<PivotKey, Map<String, AggregatesInterface<?>>>> aggregatesMap)
{
Map<PivotKey, Map<String, AggregatesInterface<?>>> viewAggregates = pivotAggregates.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
Map<PivotKey, Map<String, AggregatesInterface<?>>> 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<String, AggregatesInterface<?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
for(QFieldMetaData field : table.getFields().values())
{
if(field.getType().equals(QFieldType.INTEGER))
{
@SuppressWarnings("unchecked")
AggregatesInterface<Integer> fieldAggregates = (AggregatesInterface<Integer>) keyAggregates.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
fieldAggregates.add(record.getValueInteger(field.getName()));
@SuppressWarnings("unchecked")
AggregatesInterface<Integer> fieldTotalAggregates = (AggregatesInterface<Integer>) totalAggregates.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
fieldTotalAggregates.add(record.getValueInteger(field.getName()));
}
else if(field.getType().equals(QFieldType.DECIMAL))
{
@SuppressWarnings("unchecked")
AggregatesInterface<BigDecimal> fieldAggregates = (AggregatesInterface<BigDecimal>) keyAggregates.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates());
fieldAggregates.add(record.getValueBigDecimal(field.getName()));
@SuppressWarnings("unchecked")
AggregatesInterface<BigDecimal> fieldTotalAggregates = (AggregatesInterface<BigDecimal>) 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<PivotKey, Map<String, AggregatesInterface<?>>> viewAggregates, PivotKey key)
{
QReportMetaData report = reportInput.getInstance().getReport(reportInput.getReportName());
QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable());
Map<String, AggregatesInterface<?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
addRecordToAggregatesMap(table, record, keyAggregates);
}
/*******************************************************************************
**
*******************************************************************************/
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?>> aggregatesMap)
{
for(QFieldMetaData field : table.getFields().values())
{
if(field.getType().equals(QFieldType.INTEGER))
{
@SuppressWarnings("unchecked")
AggregatesInterface<Integer> fieldAggregates = (AggregatesInterface<Integer>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
fieldAggregates.add(record.getValueInteger(field.getName()));
}
else if(field.getType().equals(QFieldType.DECIMAL))
{
@SuppressWarnings("unchecked")
AggregatesInterface<BigDecimal> fieldAggregates = (AggregatesInterface<BigDecimal>) 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<QReportView> 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<String, String> getDisplayFormatMap(List<QFieldMetaData> 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<String, AggregatesInterface<?>> fieldAggregates = entry.getValue();
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(fieldAggregates));
if(!variancePivotAggregates.isEmpty())
{
Map<PivotKey, Map<String, AggregatesInterface<?>>> varianceMap = variancePivotAggregates.getOrDefault(view.getName(), Collections.emptyMap());
Map<String, AggregatesInterface<?>> varianceSubMap = varianceMap.getOrDefault(pivotKey, Collections.emptyMap());
variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceSubMap));
}
QRecord pivotRow = new QRecord();
pivotRows.add(pivotRow);
//////////////////////////
// add the pivot values //
//////////////////////////
for(Pair<String, Serializable> 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);

View File

@ -75,7 +75,7 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
**
*******************************************************************************/
@Override
public void start(ExportInput exportInput, List<QFieldMetaData> fields) throws QReportingException
public void start(ExportInput exportInput, List<QFieldMetaData> 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<QRecord> qRecords) throws QReportingException
{
List<QRecord> qRecords = recordPipe.consumeAvailableRecords();
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
for(QRecord qRecord : qRecords)

View File

@ -32,7 +32,7 @@ import com.kingsrook.qqq.backend.core.utils.Pair;
/*******************************************************************************
**
*******************************************************************************/
public class PivotKey
public class PivotKey implements Cloneable
{
private List<Pair<String, Serializable>> keys = new ArrayList<>();
@ -108,4 +108,21 @@ public class PivotKey
return Objects.hash(keys);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public PivotKey clone()
{
PivotKey clone = new PivotKey();
for(Pair<String, Serializable> key : keys)
{
clone.add(key.getA(), key.getB());
}
return (clone);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
{
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
/*******************************************************************************
**
*******************************************************************************/
public class PlainExcelStyler implements ExcelStylerInterface
{
}

View File

@ -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<Serializable, String> 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);
}
}
}

View File

@ -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);
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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<String, Map<String, Serializable>> 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);

View File

@ -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<String, QFrontendTableMetaData> tables;
private Map<String, QFrontendProcessMetaData> processes;
private Map<String, QFrontendReportMetaData> reports;
private Map<String, QFrontendAppMetaData> apps;
private List<AppTreeNode> appTree;
@ -92,6 +94,28 @@ public class MetaDataOutput extends AbstractActionOutput
/*******************************************************************************
** Getter for reports
**
*******************************************************************************/
public Map<String, QFrontendReportMetaData> getReports()
{
return reports;
}
/*******************************************************************************
** Setter for reports
**
*******************************************************************************/
public void setReports(Map<String, QFrontendReportMetaData> reports)
{
this.reports = reports;
}
/*******************************************************************************
** Getter for appTree
**

View File

@ -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());
}
}
}
}

View File

@ -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;

View File

@ -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
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -32,6 +32,7 @@ public enum QComponentType
VALIDATION_REVIEW_SCREEN,
EDIT_FORM,
VIEW_FORM,
DOWNLOAD_FORM,
RECORD_LIST,
PROCESS_SUMMARY_RESULTS;
///////////////////////////////////////////////////////////////////////////

View File

@ -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
**

View File

@ -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<QFieldMetaData> inputFields;
private String sourceTable;
private String processName;
private QQueryFilter queryFilter;
private QQueryFilter varianceQueryFilter;
private List<QReportView> 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);
}
}

View File

@ -37,7 +37,8 @@ public class QReportView
private String titleFormat;
private List<String> titleFields;
private List<String> pivotFields;
private boolean totalRow = false;
private boolean totalRow = false;
private boolean pivotSubTotals = false;
private List<QReportField> columns;
private List<QFilterOrderBy> 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
**

View File

@ -28,5 +28,5 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting;
public enum ReportType
{
PIVOT,
SIMPLE
TABLE
}

View File

@ -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<ProcessSummaryLine> 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<ProcessSummaryLine> doGetProcessSummary(boolean isForResultScreen)
{
ArrayList<ProcessSummaryLine> processSummary = getProcessSummary(isForResultScreen);
if(processSummary == null)
{
return (null);
}
for(ProcessSummaryLine processSummaryLine : processSummary)
{
if(!StringUtils.hasContent(processSummaryLine.getMessage()))
{
processSummaryLine.pickMessage(isForResultScreen);
}
}
return (processSummary);
}
}

View File

@ -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);
/////////////////////////////////////////////////////

View File

@ -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);
/////////////////////////////////////////////////////

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, Serializable> 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));
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QFieldMetaData> inputFieldList = new ArrayList<>(report.getInputFields());
runBackendStepOutput.addValue("inputFieldList", inputFieldList);
}
else
{
//////////////////////////////////////////////////////////////
// no input? re-route the process to skip the input screen //
//////////////////////////////////////////////////////////////
List<String> stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList());
stepList.removeIf(s -> s.equals(BasicRunReportProcess.STEP_NAME_INPUT));
runBackendStepOutput.getProcessState().setStepList(stepList);
}
}
}

View File

@ -28,7 +28,7 @@ import java.util.Objects;
/*******************************************************************************
** Simple container for two objects
*******************************************************************************/
public class Pair<A, B>
public class Pair<A, B> implements Cloneable
{
private A a;
private B b;
@ -46,6 +46,9 @@ public class Pair<A, B>
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
@ -104,4 +107,23 @@ public class Pair<A, B>
{
return Objects.hash(a, b);
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
@Override
public Pair<A, B> clone()
{
try
{
return (Pair<A, B>) super.clone();
}
catch(CloneNotSupportedException e)
{
throw new AssertionError();
}
}
}

View File

@ -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)
**