Merge branch 'feature/QQQ-42-reports' into feature/sprint-11

This commit is contained in:
2022-09-20 12:49:37 -05:00
43 changed files with 1716 additions and 193 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;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -81,6 +83,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

@ -31,6 +31,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 +44,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,11 +61,13 @@ 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;
private int row = 0;
private int row = 0;
private int sheetCount = 0;
@ -99,17 +103,39 @@ 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;
this.sheetCount++;
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(Objects.requireNonNullElse(label, "Sheet " + sheetCount));
writeReportHeaderRow();
}
catch(Exception e)
{
throw (new QReportingException("Error starting worksheet", e));
}
}
@ -121,18 +147,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 +172,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 +192,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 +233,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 +299,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

@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -154,17 +155,17 @@ public class FormulaInterpreter
case "ADD":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).add(numbers.get(1)));
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).add(numbers.get(1)));
}
case "MINUS":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).subtract(numbers.get(1)));
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).subtract(numbers.get(1)));
}
case "MULTIPLY":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).multiply(numbers.get(1)));
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).multiply(numbers.get(1)));
}
case "DIVIDE":
{
@ -173,7 +174,7 @@ public class FormulaInterpreter
{
return null;
}
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).divide(numbers.get(1), 4, RoundingMode.HALF_UP));
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).divide(numbers.get(1), 4, RoundingMode.HALF_UP));
}
case "DIVIDE_SCALE":
{
@ -182,17 +183,82 @@ public class FormulaInterpreter
{
return null;
}
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).divide(numbers.get(1), numbers.get(2).intValue(), RoundingMode.HALF_UP));
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).divide(numbers.get(1), numbers.get(2).intValue(), RoundingMode.HALF_UP));
}
case "ROUND":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).round(new MathContext(numbers.get(1).intValue())));
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).round(new MathContext(numbers.get(1).intValue())));
}
case "SCALE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).setScale(numbers.get(1).intValue(), RoundingMode.HALF_UP));
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).setScale(numbers.get(1).intValue(), RoundingMode.HALF_UP));
}
case "NVL":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return Objects.requireNonNullElse(numbers.get(0), numbers.get(1));
}
case "IF":
{
// IF(CONDITION,TRUE,ELSE)
List<Serializable> actualArgs = getArgumentList(args, 3, variableInterpreter);
Serializable condition = actualArgs.get(0);
boolean conditionBoolean;
if(condition == null)
{
conditionBoolean = false;
}
else if(condition instanceof Boolean b)
{
conditionBoolean = b;
}
else if(condition instanceof BigDecimal bd)
{
conditionBoolean = (bd.compareTo(BigDecimal.ZERO) != 0);
}
else if(condition instanceof String s)
{
if("true".equalsIgnoreCase(s))
{
conditionBoolean = true;
}
else if("false".equalsIgnoreCase(s))
{
conditionBoolean = false;
}
else
{
conditionBoolean = StringUtils.hasContent(s);
}
}
else
{
conditionBoolean = false;
}
return conditionBoolean ? actualArgs.get(1) : actualArgs.get(2);
}
case "LT":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) < 0);
}
case "LTE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) <= 0);
}
case "GT":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) > 0);
}
case "GTE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) >= 0);
}
default:
{
@ -230,7 +296,21 @@ public class FormulaInterpreter
/*******************************************************************************
**
*******************************************************************************/
private static Serializable nullIfAnyNullArgsElse(List<BigDecimal> numbers, Supplier<BigDecimal> supplier)
private static Serializable nullIfAnyNullArgsElseBigDecimal(List<BigDecimal> numbers, Supplier<BigDecimal> supplier)
{
if(numbers.stream().anyMatch(Objects::isNull))
{
return (null);
}
return supplier.get();
}
/*******************************************************************************
**
*******************************************************************************/
private static Serializable nullIfAnyNullArgsElseBoolean(List<BigDecimal> numbers, Supplier<Boolean> supplier)
{
if(numbers.stream().anyMatch(Objects::isNull))
{
@ -259,7 +339,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)
@ -270,4 +350,35 @@ public class FormulaInterpreter
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private static List<Serializable> getArgumentList(List<Serializable> originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
{
if(howMany != null)
{
if(!howMany.equals(originalArgs.size()))
{
throw (new QFormulaException("Wrong number of arguments (required: " + howMany + ", received: " + originalArgs.size() + ")"));
}
}
List<Serializable> rs = new ArrayList<>();
for(Serializable originalArg : originalArgs)
{
try
{
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null);
rs.add(interpretedArg);
}
catch(QValueException e)
{
throw (new QFormulaException("Could not process [" + originalArg + "] as a number"));
}
}
return (rs);
}
}

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,17 +469,44 @@ public class GenerateReportAction
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue();
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(fieldAggregates));
HashMap<String, Serializable> thisRowValues = new HashMap<>();
variableInterpreter.addValueMap("thisRow", thisRowValues);
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);
pivotRow.setValue(column.getName(), serializable);
thisRowValues.put(column.getName(), serializable);
}
}
@ -406,6 +563,8 @@ public class GenerateReportAction
}
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(totalAggregates));
variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceTotalAggregates));
for(QReportField column : view.getColumns())
{
Serializable serializable = getValueForColumn(variableInterpreter, column);
@ -428,14 +587,17 @@ public class GenerateReportAction
*******************************************************************************/
private Serializable getValueForColumn(QMetaDataVariableInterpreter variableInterpreter, QReportField column) throws QFormulaException
{
String formula = column.getFormula();
Serializable serializable = variableInterpreter.interpretForObject(formula);
String formula = column.getFormula();
Serializable result;
if(formula.startsWith("=") && formula.length() > 1)
{
// serializable = interpretFormula(variableInterpreter, formula);
serializable = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1));
result = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1));
}
return serializable;
else
{
result = variableInterpreter.interpretForObject(formula, null);
}
return (result);
}

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

@ -45,6 +45,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;
@ -99,6 +100,11 @@ public class QInstanceEnricher
{
qInstance.getApps().values().forEach(this::enrich);
}
if(qInstance.getReports() != null)
{
qInstance.getReports().values().forEach(this::enrich);
}
}
@ -239,6 +245,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);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -551,17 +575,17 @@ public class QInstanceEnricher
//////////////////////////////////////////////////////////////////////////////
// create an identity section for the id and any fields in the record label //
//////////////////////////////////////////////////////////////////////////////
QAppSection defaultSection = new QAppSection(app.getName(), app.getLabel(), new QIcon("badge"), new ArrayList<>(), new ArrayList<>());
QAppSection defaultSection = new QAppSection(app.getName(), app.getLabel(), new QIcon("badge"), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
boolean foundNonAppChild = false;
if(CollectionUtils.nullSafeHasContents(app.getChildren()))
{
for(QAppChildMetaData child : app.getChildren())
{
////////////////////////////////////////////////////////////////////////////////
// only tables and processes are allowed to be in sections at this time, apps //
// might be children but not in sections so keep track if we find any non-app //
////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// only tables, processes, and reports are allowed to be in sections at this time, apps //
// might be children but not in sections so keep track if we find any non-app //
//////////////////////////////////////////////////////////////////////////////////////////
if(child.getClass().equals(QTableMetaData.class))
{
defaultSection.getTables().add(child.getName());
@ -572,6 +596,11 @@ public class QInstanceEnricher
defaultSection.getProcesses().add(child.getName());
foundNonAppChild = true;
}
else if(child.getClass().equals(QReportMetaData.class))
{
defaultSection.getReports().add(child.getName());
foundNonAppChild = true;
}
}
}

View File

@ -510,7 +510,8 @@ public class QInstanceValidator
assertCondition(StringUtils.hasContent(section.getLabel()), "Missing a label for a section in app " + app.getLabel() + ".");
boolean hasTables = CollectionUtils.nullSafeHasContents(section.getTables());
boolean hasProcesses = CollectionUtils.nullSafeHasContents(section.getProcesses());
if(assertCondition(hasTables || hasProcesses, "App " + app.getName() + " section " + section.getName() + " does not have any children."))
boolean hasReports = CollectionUtils.nullSafeHasContents(section.getReports());
if(assertCondition(hasTables || hasProcesses || hasReports, "App " + app.getName() + " section " + section.getName() + " does not have any children."))
{
if(hasTables)
{
@ -532,6 +533,16 @@ public class QInstanceValidator
childNamesInSections.add(processName);
}
}
if(hasReports)
{
for(String reportName : section.getReports())
{
assertCondition(app.getChildren().stream().anyMatch(c -> c.getName().equals(reportName)), "App " + app.getName() + " section " + section.getName() + " specifies report " + reportName + ", which is not a child of this app.");
assertCondition(!childNamesInSections.contains(reportName), "App " + app.getName() + " has report " + reportName + " listed more than once in its sections.");
childNamesInSections.add(reportName);
}
}
}
}

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

@ -36,6 +36,7 @@ public class QAppSection
private List<String> tables;
private List<String> processes;
private List<String> reports;
@ -51,13 +52,14 @@ public class QAppSection
/*******************************************************************************
**
*******************************************************************************/
public QAppSection(String name, String label, QIcon icon, List<String> tables, List<String> processes)
public QAppSection(String name, String label, QIcon icon, List<String> tables, List<String> processes, List<String> reports)
{
this.name = name;
this.label = label;
this.icon = icon;
this.tables = tables;
this.processes = processes;
this.reports = reports;
}
@ -198,6 +200,40 @@ public class QAppSection
/*******************************************************************************
** Getter for reports
**
*******************************************************************************/
public List<String> getReports()
{
return reports;
}
/*******************************************************************************
** Setter for reports
**
*******************************************************************************/
public void setReports(List<String> reports)
{
this.reports = reports;
}
/*******************************************************************************
** Fluent setter for reports
**
*******************************************************************************/
public QAppSection withReports(List<String> reports)
{
this.reports = reports;
return (this);
}
/*******************************************************************************
** Getter for icon
**

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,85 @@
/*
* 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;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
/*******************************************************************************
** 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");
QReportMetaData report = runBackendStepInput.getInstance().getReport(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", report.getLabel() + " " + 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

@ -45,7 +45,7 @@ class WidgetDataLoaderTest
Object widgetData = new WidgetDataLoader().execute(TestUtils.defineInstance(), TestUtils.getMockSession(), PersonsByCreateDateBarChart.class.getSimpleName());
assertThat(widgetData).isInstanceOf(ChartData.class);
ChartData chartData = (ChartData) widgetData;
assertEquals("chartData", chartData.getType());
assertEquals("barChart", chartData.getType());
assertThat(chartData.getTitle()).isNotBlank();
assertNotNull(chartData.getChartData());
}

View File

@ -32,7 +32,9 @@ import static com.kingsrook.qqq.backend.core.actions.reporting.FormulaInterprete
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
@ -140,4 +142,55 @@ class FormulaInterpreterTest
assertEquals(new BigDecimal("27.78"), interpretFormula(vi, "SCALE(MULTIPLY(100,DIVIDE_SCALE(${pivot.sum.noOfShoes},${total.sum.noOfShoes},6)),2)"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testComparisons() throws QFormulaException
{
QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter();
vi.addValueMap("input", Map.of("one", 1, "two", 2, "foo", "bar"));
assertTrue((Boolean) interpretFormula(vi, "LT(${input.one},${input.two})"));
assertFalse((Boolean) interpretFormula(vi, "LT(${input.two},${input.one})"));
assertFalse((Boolean) interpretFormula(vi, "GT(${input.one},${input.two})"));
assertTrue((Boolean) interpretFormula(vi, "GT(${input.two},${input.one})"));
assertTrue((Boolean) interpretFormula(vi, "LTE(${input.one},${input.two})"));
assertTrue((Boolean) interpretFormula(vi, "LTE(${input.one},${input.one})"));
assertFalse((Boolean) interpretFormula(vi, "LTE(${input.two},${input.one})"));
assertFalse((Boolean) interpretFormula(vi, "GTE(${input.one},${input.two})"));
assertTrue((Boolean) interpretFormula(vi, "GTE(${input.one},${input.one})"));
assertTrue((Boolean) interpretFormula(vi, "GTE(${input.two},${input.one})"));
// todo - google sheets compares strings differently...
assertThatThrownBy(() -> interpretFormula(vi, "LT(${input.foo},${input.one})")).hasMessageContaining("[bar] as a number");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testConditionals() throws QFormulaException
{
QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter();
vi.addValueMap("input", Map.of("one", 1, "two", 2, "three", 3, "foo", "bar"));
assertEquals("A", interpretFormula(vi, "IF(LT(${input.one},${input.two}),A,B)"));
assertEquals("B", interpretFormula(vi, "IF(GT(${input.one},${input.two}),A,B)"));
assertEquals("C", interpretFormula(vi, "IF(GT(${input.one},${input.two}),A,IF(GT(${input.two},${input.three}),B,C))"));
assertEquals("B", interpretFormula(vi, "IF(GT(${input.one},${input.two}),A,IF(LT(${input.two},${input.three}),B,C))"));
assertEquals("A", interpretFormula(vi, "IF(GT(${input.two},${input.one}),A,IF(LT(${input.two},${input.three}),B,C))"));
assertEquals("Yes", interpretFormula(vi, "IF(GT(${input.one},0),Yes,No)"));
assertEquals("No", interpretFormula(vi, "IF(LT(${input.one},0),Yes,No)"));
}
}

View File

@ -59,7 +59,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for GenerateReportAction
*******************************************************************************/
class GenerateReportActionTest
public class GenerateReportActionTest
{
private static final String REPORT_NAME = "personReport1";
@ -243,32 +243,32 @@ class GenerateReportActionTest
Map<String, String> row = iterator.next();
assertEquals(6, list.size());
assertThat(row.get("Home State Id")).isEqualTo("1");
assertThat(row.get("Home State Id")).isEqualTo("IL");
assertThat(row.get("Last Name")).isEqualTo("Jonson");
assertThat(row.get("Quantity")).isNull();
row = iterator.next();
assertThat(row.get("Home State Id")).isEqualTo("1");
assertThat(row.get("Home State Id")).isEqualTo("IL");
assertThat(row.get("Last Name")).isEqualTo("Jones");
assertThat(row.get("Quantity")).isEqualTo("3");
row = iterator.next();
assertThat(row.get("Home State Id")).isEqualTo("1");
assertThat(row.get("Home State Id")).isEqualTo("IL");
assertThat(row.get("Last Name")).isEqualTo("Kelly");
assertThat(row.get("Quantity")).isEqualTo("4");
row = iterator.next();
assertThat(row.get("Home State Id")).isEqualTo("1");
assertThat(row.get("Home State Id")).isEqualTo("IL");
assertThat(row.get("Last Name")).isEqualTo("Keller");
assertThat(row.get("Quantity")).isEqualTo("5");
row = iterator.next();
assertThat(row.get("Home State Id")).isEqualTo("1");
assertThat(row.get("Home State Id")).isEqualTo("IL");
assertThat(row.get("Last Name")).isEqualTo("Kelkhoff");
assertThat(row.get("Quantity")).isEqualTo("6");
row = iterator.next();
assertThat(row.get("Home State Id")).isEqualTo("2");
assertThat(row.get("Home State Id")).isEqualTo("MO");
assertThat(row.get("Last Name")).isEqualTo("Kelkhoff");
assertThat(row.get("Quantity")).isEqualTo("7");
}
@ -297,15 +297,14 @@ class GenerateReportActionTest
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
assertEquals(2, list.size());
assertThat(row.get("Home State Id")).isEqualTo("2");
assertThat(row.get("Home State Id")).isEqualTo("MO");
assertThat(row.get("Last Name")).isNull();
assertThat(row.get("Quantity")).isEqualTo("7");
row = iterator.next();
assertThat(row.get("Home State Id")).isEqualTo("1");
assertThat(row.get("Home State Id")).isEqualTo("IL");
assertThat(row.get("Last Name")).isNull();
assertThat(row.get("Quantity")).isEqualTo("18");
}
@ -398,7 +397,7 @@ class GenerateReportActionTest
/*******************************************************************************
**
*******************************************************************************/
private QReportMetaData defineReport(boolean includeTotalRow)
public static QReportMetaData defineReport(boolean includeTotalRow)
{
return new QReportMetaData()
.withName(REPORT_NAME)

View File

@ -584,7 +584,7 @@ class QInstanceValidatorTest
{
QAppMetaData app = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("test"))
.withSection(new QAppSection(null, "Section 1", new QIcon("person"), List.of("test"), null));
.withSection(new QAppSection(null, "Section 1", new QIcon("person"), List.of("test"), null, null));
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "Missing a name");
}
@ -598,7 +598,7 @@ class QInstanceValidatorTest
{
QAppMetaData app = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("test"))
.withSection(new QAppSection("Section 1", null, new QIcon("person"), List.of("test"), null));
.withSection(new QAppSection("Section 1", null, new QIcon("person"), List.of("test"), null, null));
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "Missing a label");
}
@ -612,12 +612,12 @@ class QInstanceValidatorTest
{
QAppMetaData app1 = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("test"))
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of(), List.of()));
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of(), List.of(), null));
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app1), "section1 does not have any children", "child test is not listed in any app sections");
QAppMetaData app2 = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("test"))
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), null, null));
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), null, null, null));
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app2), "section1 does not have any children", "child test is not listed in any app sections");
}
@ -631,11 +631,11 @@ class QInstanceValidatorTest
{
QAppMetaData app1 = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("test"))
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test", "tset"), null));
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test", "tset"), null, null));
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app1), "not a child of this app");
QAppMetaData app2 = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("test"))
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), List.of("tset")));
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), List.of("tset"), null));
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app2), "not a child of this app");
}
@ -649,23 +649,23 @@ class QInstanceValidatorTest
{
QAppMetaData app1 = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("test"))
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test", "test"), null));
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test", "test"), null, null));
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app1), "more than once");
QAppMetaData app2 = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("test"))
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), null))
.withSection(new QAppSection("section2", "Section 2", new QIcon("person"), List.of("test"), null));
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), null, null))
.withSection(new QAppSection("section2", "Section 2", new QIcon("person"), List.of("test"), null, null));
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app2), "more than once");
QAppMetaData app3 = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("test"))
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), List.of("test")));
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), List.of("test"), null));
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app3), "more than once");
QAppMetaData app4 = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("test"))
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), null, List.of("test", "test")));
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), null, List.of("test", "test"), null));
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app4), "more than once");
}
@ -687,7 +687,7 @@ class QInstanceValidatorTest
QAppMetaData app = new QAppMetaData().withName("test")
.withChild(new QTableMetaData().withName("tset"))
.withChild(new QTableMetaData().withName("test"))
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), null));
.withSection(new QAppSection("section1", "Section 1", new QIcon("person"), List.of("test"), null, null));
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "not listed in any app sections");
}

View File

@ -200,6 +200,29 @@ class QMetaDataVariableInterpreterTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testLooksLikeVariableButNotFound()
{
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", Map.of("x", 1, "y", 2));
variableInterpreter.addValueMap("others", Map.of("foo", "bar"));
assertNull(variableInterpreter.interpretForObject("${input.notFound}", null));
assertEquals(0, variableInterpreter.interpretForObject("${input.notFound}", 0));
assertEquals("--", variableInterpreter.interpretForObject("${input.notFound}", "--"));
assertEquals("--", variableInterpreter.interpretForObject("${others.notFound}", "--"));
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this one doesn't count as "looking like a variable" - because the "prefix" (notValid) isn't a value map... //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
assertEquals("${notValid.notFound}", variableInterpreter.interpretForObject("${notValid.notFound}", "--"));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,77 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.implementations.reports;
import java.time.LocalDate;
import java.time.Month;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportActionTest;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Unit test for BasicRunReportProcess
*******************************************************************************/
class BasicRunReportProcessTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRunReport() throws QException
{
QInstance instance = TestUtils.defineInstance();
QReportMetaData report = GenerateReportActionTest.defineReport(true);
QProcessMetaData runReportProcess = BasicRunReportProcess.defineProcessMetaData();
instance.addReport(report);
report.setProcessName(runReportProcess.getName());
instance.addProcess(runReportProcess);
RunProcessInput runProcessInput = new RunProcessInput(instance);
runProcessInput.setSession(TestUtils.getMockSession());
runProcessInput.setProcessName(report.getProcessName());
runProcessInput.addValue(BasicRunReportProcess.FIELD_REPORT_NAME, report.getName());
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
String processUUID = runProcessOutput.getProcessUUID();
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo(BasicRunReportProcess.STEP_NAME_INPUT);
runProcessInput.addValue("startDate", LocalDate.of(1980, Month.JANUARY, 1));
runProcessInput.addValue("endDate", LocalDate.of(2099, Month.DECEMBER, 31));
runProcessInput.setStartAfterStep(BasicRunReportProcess.STEP_NAME_INPUT);
runProcessInput.setProcessUUID(processUUID);
runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo(BasicRunReportProcess.STEP_NAME_ACCESS);
assertThat(runProcessOutput.getValues()).containsKeys("downloadFileName", "serverFilePath");
}
}

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

View File

@ -451,8 +451,8 @@ class QJavalinImplementationTest extends QJavalinTestBase
@Test
void testExportFieldsQueryParam()
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/export/People.csv?fields=id,birthDate").asString();
String[] csvLines = response.getBody().split("\n");
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/export/People.csv?fields=id,birthDate").asString();
String[] csvLines = response.getBody().split("\n");
assertEquals("""
"Id","Birth Date\"""", csvLines[0]);
}
@ -484,7 +484,7 @@ class QJavalinImplementationTest extends QJavalinTestBase
assertNotNull(jsonObject);
assertEquals("barChart", jsonObject.getString("type"));
assertNotNull(jsonObject.getString("title"));
assertNotNull(jsonObject.getJSONObject("barChartData"));
assertNotNull(jsonObject.getJSONObject("chartData"));
}
}

View File

@ -35,7 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
** Unit test for PersonsByCreateDateBarChart
*******************************************************************************/
class PersonsByCreateDateChartTestData
class PersonsByCreateDateBarChartTest
{
/*******************************************************************************
@ -47,7 +47,7 @@ class PersonsByCreateDateChartTestData
Object widgetData = new PersonsByCreateDateBarChart().render(SampleMetaDataProvider.defineInstance(), new QSession(), null);
assertThat(widgetData).isInstanceOf(ChartData.class);
ChartData chartData = (ChartData) widgetData;
assertEquals("chartData", chartData.getType());
assertEquals("barChart", chartData.getType());
assertThat(chartData.getTitle()).isNotBlank();
assertNotNull(chartData.getChartData());
}