mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Merge branch 'feature/QQQ-42-reports' into feature/sprint-11
This commit is contained in:
@ -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.AppTreeNode;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
|
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.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.frontend.QFrontendTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
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.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.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
|
||||||
@ -81,6 +83,17 @@ public class MetaDataAction
|
|||||||
}
|
}
|
||||||
metaDataOutput.setProcesses(processes);
|
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 //
|
// map apps to frontend metadata //
|
||||||
///////////////////////////////////
|
///////////////////////////////////
|
||||||
|
@ -66,7 +66,7 @@ public class CsvExportStreamer implements ExportStreamerInterface
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Override
|
@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.exportInput = exportInput;
|
||||||
this.fields = fields;
|
this.fields = fields;
|
||||||
@ -87,7 +87,7 @@ public class CsvExportStreamer implements ExportStreamerInterface
|
|||||||
{
|
{
|
||||||
if(StringUtils.hasContent(exportInput.getTitleRow()))
|
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;
|
int col = 0;
|
||||||
@ -114,9 +114,8 @@ public class CsvExportStreamer implements ExportStreamerInterface
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Override
|
@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");
|
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
|
||||||
|
|
||||||
for(QRecord qRecord : qRecords)
|
for(QRecord qRecord : qRecords)
|
||||||
|
@ -31,6 +31,9 @@ import java.util.Date;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.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.exceptions.QReportingException;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
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 com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.dhatim.fastexcel.BorderSide;
|
import org.dhatim.fastexcel.StyleSetter;
|
||||||
import org.dhatim.fastexcel.BorderStyle;
|
|
||||||
import org.dhatim.fastexcel.Workbook;
|
import org.dhatim.fastexcel.Workbook;
|
||||||
import org.dhatim.fastexcel.Worksheet;
|
import org.dhatim.fastexcel.Worksheet;
|
||||||
|
|
||||||
@ -59,11 +61,13 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
|||||||
private List<QFieldMetaData> fields;
|
private List<QFieldMetaData> fields;
|
||||||
private OutputStream outputStream;
|
private OutputStream outputStream;
|
||||||
|
|
||||||
private Map<String, String> excelCellFormats;
|
private ExcelStylerInterface excelStylerInterface = new PlainExcelStyler();
|
||||||
|
private Map<String, String> excelCellFormats;
|
||||||
|
|
||||||
private Workbook workbook;
|
private Workbook workbook;
|
||||||
private Worksheet worksheet;
|
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
|
@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;
|
try
|
||||||
this.fields = fields;
|
{
|
||||||
table = exportInput.getTable();
|
this.exportInput = exportInput;
|
||||||
outputStream = this.exportInput.getReportOutputStream();
|
this.fields = fields;
|
||||||
|
table = exportInput.getTable();
|
||||||
|
outputStream = this.exportInput.getReportOutputStream();
|
||||||
|
this.row = 0;
|
||||||
|
this.sheetCount++;
|
||||||
|
|
||||||
workbook = new Workbook(outputStream, "QQQ", null);
|
if(workbook == null)
|
||||||
worksheet = workbook.newWorksheet("Sheet 1");
|
{
|
||||||
|
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
|
try
|
||||||
{
|
{
|
||||||
|
///////////////
|
||||||
|
// title row //
|
||||||
|
///////////////
|
||||||
if(StringUtils.hasContent(exportInput.getTitleRow()))
|
if(StringUtils.hasContent(exportInput.getTitleRow()))
|
||||||
{
|
{
|
||||||
worksheet.value(row, 0, 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).merge();
|
||||||
worksheet.range(row, 0, row, fields.size() - 1).style()
|
|
||||||
.bold()
|
StyleSetter titleStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
|
||||||
.fontSize(14)
|
excelStylerInterface.styleTitleRow(titleStyle);
|
||||||
.horizontalAlignment("center")
|
titleStyle.set();
|
||||||
.set();
|
|
||||||
row++;
|
row++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////
|
||||||
|
// header row //
|
||||||
|
////////////////
|
||||||
int col = 0;
|
int col = 0;
|
||||||
for(QFieldMetaData column : fields)
|
for(QFieldMetaData column : fields)
|
||||||
{
|
{
|
||||||
@ -140,10 +172,9 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
|||||||
col++;
|
col++;
|
||||||
}
|
}
|
||||||
|
|
||||||
worksheet.range(row, 0, row, fields.size() - 1).style()
|
StyleSetter headerStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
|
||||||
.bold()
|
excelStylerInterface.styleHeaderRow(headerStyle);
|
||||||
.borderStyle(BorderSide.BOTTOM, BorderStyle.THIN)
|
headerStyle.set();
|
||||||
.set();
|
|
||||||
|
|
||||||
row++;
|
row++;
|
||||||
|
|
||||||
@ -161,9 +192,8 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Override
|
@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");
|
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -203,6 +233,11 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
|||||||
for(QFieldMetaData field : fields)
|
for(QFieldMetaData field : fields)
|
||||||
{
|
{
|
||||||
Serializable value = qRecord.getValue(field.getName());
|
Serializable value = qRecord.getValue(field.getName());
|
||||||
|
if(field.getPossibleValueSourceName() != null)
|
||||||
|
{
|
||||||
|
value = Objects.requireNonNullElse(qRecord.getDisplayValue(field.getName()), value);
|
||||||
|
}
|
||||||
|
|
||||||
if(value != null)
|
if(value != null)
|
||||||
{
|
{
|
||||||
if(value instanceof String s)
|
if(value instanceof String s)
|
||||||
@ -264,11 +299,9 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
|||||||
{
|
{
|
||||||
writeRecord(record);
|
writeRecord(record);
|
||||||
|
|
||||||
worksheet.range(row, 0, row, fields.size() - 1).style()
|
StyleSetter totalsRowStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
|
||||||
.bold()
|
excelStylerInterface.styleTotalsRow(totalsRowStyle);
|
||||||
.borderStyle(BorderSide.TOP, BorderStyle.THIN)
|
totalsRowStyle.set();
|
||||||
.borderStyle(BorderSide.BOTTOM, BorderStyle.DOUBLE)
|
|
||||||
.set();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.CountInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
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.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.fields.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||||
@ -164,7 +165,7 @@ public class ExportAction
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
ReportFormat reportFormat = exportInput.getReportFormat();
|
ReportFormat reportFormat = exportInput.getReportFormat();
|
||||||
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
|
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
|
||||||
reportStreamer.start(exportInput, getFields(exportInput));
|
reportStreamer.start(exportInput, getFields(exportInput), "Sheet 1");
|
||||||
|
|
||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
// run the query action as an async job //
|
// run the query action as an async job //
|
||||||
@ -207,7 +208,8 @@ public class ExportAction
|
|||||||
lastReceivedRecordsAt = System.currentTimeMillis();
|
lastReceivedRecordsAt = System.currentTimeMillis();
|
||||||
nextSleepMillis = INIT_SLEEP_MS;
|
nextSleepMillis = INIT_SLEEP_MS;
|
||||||
|
|
||||||
int recordsConsumed = reportStreamer.takeRecordsFromPipe(recordPipe);
|
List<QRecord> records = recordPipe.consumeAvailableRecords();
|
||||||
|
int recordsConsumed = reportStreamer.addRecords(records);
|
||||||
recordCount += recordsConsumed;
|
recordCount += recordsConsumed;
|
||||||
|
|
||||||
LOG.info(countFromPreExecute != null
|
LOG.info(countFromPreExecute != null
|
||||||
@ -235,7 +237,8 @@ public class ExportAction
|
|||||||
///////////////////////////////////////////////////
|
///////////////////////////////////////////////////
|
||||||
// send the final records to the report streamer //
|
// 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;
|
recordCount += recordsConsumed;
|
||||||
|
|
||||||
long reportEndTime = System.currentTimeMillis();
|
long reportEndTime = System.currentTimeMillis();
|
||||||
|
@ -38,12 +38,12 @@ public interface ExportStreamerInterface
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Called once, before any rows are available. Meant to write a header, for example.
|
** 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.
|
** 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.
|
** 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
|
default void addTotalsRow(QRecord record) throws QReportingException
|
||||||
{
|
{
|
||||||
RecordPipe recordPipe = new RecordPipe();
|
addRecords(List.of(record));
|
||||||
recordPipe.addRecord(record);
|
|
||||||
takeRecordsFromPipe(recordPipe);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.exceptions.QValueException;
|
||||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
|
|
||||||
|
|
||||||
@ -154,17 +155,17 @@ public class FormulaInterpreter
|
|||||||
case "ADD":
|
case "ADD":
|
||||||
{
|
{
|
||||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
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":
|
case "MINUS":
|
||||||
{
|
{
|
||||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
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":
|
case "MULTIPLY":
|
||||||
{
|
{
|
||||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
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":
|
case "DIVIDE":
|
||||||
{
|
{
|
||||||
@ -173,7 +174,7 @@ public class FormulaInterpreter
|
|||||||
{
|
{
|
||||||
return null;
|
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":
|
case "DIVIDE_SCALE":
|
||||||
{
|
{
|
||||||
@ -182,17 +183,82 @@ public class FormulaInterpreter
|
|||||||
{
|
{
|
||||||
return null;
|
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":
|
case "ROUND":
|
||||||
{
|
{
|
||||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
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":
|
case "SCALE":
|
||||||
{
|
{
|
||||||
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
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:
|
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))
|
if(numbers.stream().anyMatch(Objects::isNull))
|
||||||
{
|
{
|
||||||
@ -259,7 +339,7 @@ public class FormulaInterpreter
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg));
|
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null);
|
||||||
rs.add(ValueUtils.getValueAsBigDecimal(interpretedArg));
|
rs.add(ValueUtils.getValueAsBigDecimal(interpretedArg));
|
||||||
}
|
}
|
||||||
catch(QValueException e)
|
catch(QValueException e)
|
||||||
@ -270,4 +350,35 @@ public class FormulaInterpreter
|
|||||||
return (rs);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ import java.util.Collections;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
|
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||||
@ -69,9 +70,16 @@ public class GenerateReportAction
|
|||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// viewName > PivotKey > fieldName > Aggregates //
|
// 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
|
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);
|
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
|
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);
|
setInputValuesInQueryFilter(reportInput, queryFilter);
|
||||||
|
|
||||||
RecordPipe recordPipe = new RecordPipe();
|
RecordPipe recordPipe = new RecordPipe();
|
||||||
@ -104,8 +158,37 @@ public class GenerateReportAction
|
|||||||
queryInput.setRecordPipe(recordPipe);
|
queryInput.setRecordPipe(recordPipe);
|
||||||
queryInput.setTableName(report.getSourceTable());
|
queryInput.setTableName(report.getSourceTable());
|
||||||
queryInput.setFilter(queryFilter);
|
queryInput.setFilter(queryFilter);
|
||||||
|
queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this?
|
||||||
return (new QueryAction().execute(queryInput));
|
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();
|
List<QRecord> records = recordPipe.consumeAvailableRecords();
|
||||||
|
|
||||||
|
if(includeTableView && !isForVariance)
|
||||||
|
{
|
||||||
|
reportStreamer.addRecords(records);
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
// do aggregates for pivots //
|
// do aggregates for pivots //
|
||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable());
|
QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable());
|
||||||
report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).forEach((view) ->
|
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());
|
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)
|
for(QRecord record : records)
|
||||||
{
|
{
|
||||||
PivotKey key = new PivotKey();
|
PivotKey key = new PivotKey();
|
||||||
for(String pivotField : view.getPivotFields())
|
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<>());
|
addRecordToPivotKeyAggregates(table, record, viewAggregates, key);
|
||||||
|
|
||||||
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?)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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());
|
Map<String, AggregatesInterface<?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
|
||||||
QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable());
|
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();
|
List<QReportView> reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).toList();
|
||||||
for(QReportView view : reportViews)
|
for(QReportView view : reportViews)
|
||||||
{
|
{
|
||||||
PivotOutput pivotOutput = outputPivot(reportInput, view, table);
|
PivotOutput pivotOutput = computePivotRowsForView(reportInput, view, table);
|
||||||
ReportFormat reportFormat = reportInput.getReportFormat();
|
|
||||||
|
|
||||||
ExportInput exportInput = new ExportInput(reportInput.getInstance());
|
ExportInput exportInput = new ExportInput(reportInput.getInstance());
|
||||||
exportInput.setSession(reportInput.getSession());
|
exportInput.setSession(reportInput.getSession());
|
||||||
@ -230,21 +351,18 @@ public class GenerateReportAction
|
|||||||
exportInput.setTitleRow(pivotOutput.titleRow);
|
exportInput.setTitleRow(pivotOutput.titleRow);
|
||||||
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
|
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
|
||||||
|
|
||||||
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
|
|
||||||
reportStreamer.setDisplayFormats(getDisplayFormatMap(view));
|
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...
|
reportStreamer.addRecords(pivotOutput.pivotRows); // todo - what if this set is huge?
|
||||||
recordPipe.addRecords(pivotOutput.pivotRows);
|
|
||||||
reportStreamer.takeRecordsFromPipe(recordPipe);
|
|
||||||
|
|
||||||
if(pivotOutput.totalRow != null)
|
if(pivotOutput.totalRow != null)
|
||||||
{
|
{
|
||||||
reportStreamer.addTotalsRow(pivotOutput.totalRow);
|
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();
|
QValueFormatter valueFormatter = new QValueFormatter();
|
||||||
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
|
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
|
||||||
@ -339,17 +469,44 @@ public class GenerateReportAction
|
|||||||
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue();
|
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue();
|
||||||
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(fieldAggregates));
|
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();
|
QRecord pivotRow = new QRecord();
|
||||||
pivotRows.add(pivotRow);
|
pivotRows.add(pivotRow);
|
||||||
|
|
||||||
|
//////////////////////////
|
||||||
|
// add the pivot values //
|
||||||
|
//////////////////////////
|
||||||
for(Pair<String, Serializable> key : pivotKey.getKeys())
|
for(Pair<String, Serializable> key : pivotKey.getKeys())
|
||||||
{
|
{
|
||||||
pivotRow.setValue(key.getA(), key.getB());
|
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())
|
for(QReportField column : view.getColumns())
|
||||||
{
|
{
|
||||||
Serializable serializable = getValueForColumn(variableInterpreter, column);
|
Serializable serializable = getValueForColumn(variableInterpreter, column);
|
||||||
pivotRow.setValue(column.getName(), serializable);
|
pivotRow.setValue(column.getName(), serializable);
|
||||||
|
thisRowValues.put(column.getName(), serializable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,6 +563,8 @@ public class GenerateReportAction
|
|||||||
}
|
}
|
||||||
|
|
||||||
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(totalAggregates));
|
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(totalAggregates));
|
||||||
|
variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceTotalAggregates));
|
||||||
|
|
||||||
for(QReportField column : view.getColumns())
|
for(QReportField column : view.getColumns())
|
||||||
{
|
{
|
||||||
Serializable serializable = getValueForColumn(variableInterpreter, column);
|
Serializable serializable = getValueForColumn(variableInterpreter, column);
|
||||||
@ -428,14 +587,17 @@ public class GenerateReportAction
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private Serializable getValueForColumn(QMetaDataVariableInterpreter variableInterpreter, QReportField column) throws QFormulaException
|
private Serializable getValueForColumn(QMetaDataVariableInterpreter variableInterpreter, QReportField column) throws QFormulaException
|
||||||
{
|
{
|
||||||
String formula = column.getFormula();
|
String formula = column.getFormula();
|
||||||
Serializable serializable = variableInterpreter.interpretForObject(formula);
|
Serializable result;
|
||||||
if(formula.startsWith("=") && formula.length() > 1)
|
if(formula.startsWith("=") && formula.length() > 1)
|
||||||
{
|
{
|
||||||
// serializable = interpretFormula(variableInterpreter, formula);
|
result = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1));
|
||||||
serializable = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1));
|
|
||||||
}
|
}
|
||||||
return serializable;
|
else
|
||||||
|
{
|
||||||
|
result = variableInterpreter.interpretForObject(formula, null);
|
||||||
|
}
|
||||||
|
return (result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Override
|
@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.exportInput = exportInput;
|
||||||
this.fields = fields;
|
this.fields = fields;
|
||||||
@ -93,9 +93,8 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Override
|
@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");
|
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
|
||||||
|
|
||||||
for(QRecord qRecord : qRecords)
|
for(QRecord qRecord : qRecords)
|
||||||
|
@ -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<>();
|
private List<Pair<String, Serializable>> keys = new ArrayList<>();
|
||||||
|
|
||||||
@ -108,4 +108,21 @@ public class PivotKey
|
|||||||
return Objects.hash(keys);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
@ -294,11 +294,26 @@ public class QPossibleValueTranslator
|
|||||||
{
|
{
|
||||||
for(QFieldMetaData field : fieldsByPvsTable.get(tableName))
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.QFrontendStepMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
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.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.QFieldSection;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||||
@ -99,6 +100,11 @@ public class QInstanceEnricher
|
|||||||
{
|
{
|
||||||
qInstance.getApps().values().forEach(this::enrich);
|
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 //
|
// 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;
|
boolean foundNonAppChild = false;
|
||||||
if(CollectionUtils.nullSafeHasContents(app.getChildren()))
|
if(CollectionUtils.nullSafeHasContents(app.getChildren()))
|
||||||
{
|
{
|
||||||
for(QAppChildMetaData child : app.getChildren())
|
for(QAppChildMetaData child : app.getChildren())
|
||||||
{
|
{
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// only tables and processes are allowed to be in sections at this time, apps //
|
// 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 //
|
// might be children but not in sections so keep track if we find any non-app //
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if(child.getClass().equals(QTableMetaData.class))
|
if(child.getClass().equals(QTableMetaData.class))
|
||||||
{
|
{
|
||||||
defaultSection.getTables().add(child.getName());
|
defaultSection.getTables().add(child.getName());
|
||||||
@ -572,6 +596,11 @@ public class QInstanceEnricher
|
|||||||
defaultSection.getProcesses().add(child.getName());
|
defaultSection.getProcesses().add(child.getName());
|
||||||
foundNonAppChild = true;
|
foundNonAppChild = true;
|
||||||
}
|
}
|
||||||
|
else if(child.getClass().equals(QReportMetaData.class))
|
||||||
|
{
|
||||||
|
defaultSection.getReports().add(child.getName());
|
||||||
|
foundNonAppChild = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,7 +510,8 @@ public class QInstanceValidator
|
|||||||
assertCondition(StringUtils.hasContent(section.getLabel()), "Missing a label for a section in app " + app.getLabel() + ".");
|
assertCondition(StringUtils.hasContent(section.getLabel()), "Missing a label for a section in app " + app.getLabel() + ".");
|
||||||
boolean hasTables = CollectionUtils.nullSafeHasContents(section.getTables());
|
boolean hasTables = CollectionUtils.nullSafeHasContents(section.getTables());
|
||||||
boolean hasProcesses = CollectionUtils.nullSafeHasContents(section.getProcesses());
|
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)
|
if(hasTables)
|
||||||
{
|
{
|
||||||
@ -532,6 +533,16 @@ public class QInstanceValidator
|
|||||||
childNamesInSections.add(processName);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +148,23 @@ public class QMetaDataVariableInterpreter
|
|||||||
** Else the output is the input.
|
** Else the output is the input.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public Serializable interpretForObject(String value)
|
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)
|
if(value == null)
|
||||||
{
|
{
|
||||||
@ -176,6 +193,7 @@ public class QMetaDataVariableInterpreter
|
|||||||
|
|
||||||
if(valueMaps != null)
|
if(valueMaps != null)
|
||||||
{
|
{
|
||||||
|
boolean looksLikeVariable = false;
|
||||||
for(Map.Entry<String, Map<String, Serializable>> entry : valueMaps.entrySet())
|
for(Map.Entry<String, Map<String, Serializable>> entry : valueMaps.entrySet())
|
||||||
{
|
{
|
||||||
String name = entry.getKey();
|
String name = entry.getKey();
|
||||||
@ -184,6 +202,7 @@ public class QMetaDataVariableInterpreter
|
|||||||
String prefix = "${" + name + ".";
|
String prefix = "${" + name + ".";
|
||||||
if(value.startsWith(prefix) && value.endsWith("}"))
|
if(value.startsWith(prefix) && value.endsWith("}"))
|
||||||
{
|
{
|
||||||
|
looksLikeVariable = true;
|
||||||
String lookupName = value.substring(prefix.length()).replaceFirst("}$", "");
|
String lookupName = value.substring(prefix.length()).replaceFirst("}$", "");
|
||||||
if(valueMap != null && valueMap.containsKey(lookupName))
|
if(valueMap != null && valueMap.containsKey(lookupName))
|
||||||
{
|
{
|
||||||
@ -191,6 +210,11 @@ public class QMetaDataVariableInterpreter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(looksLikeVariable)
|
||||||
|
{
|
||||||
|
return (defaultIfLooksLikeVariableButNotFound);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (value);
|
return (value);
|
||||||
|
@ -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.AppTreeNode;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
|
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.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.frontend.QFrontendTableMetaData;
|
||||||
|
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ public class MetaDataOutput extends AbstractActionOutput
|
|||||||
{
|
{
|
||||||
private Map<String, QFrontendTableMetaData> tables;
|
private Map<String, QFrontendTableMetaData> tables;
|
||||||
private Map<String, QFrontendProcessMetaData> processes;
|
private Map<String, QFrontendProcessMetaData> processes;
|
||||||
|
private Map<String, QFrontendReportMetaData> reports;
|
||||||
private Map<String, QFrontendAppMetaData> apps;
|
private Map<String, QFrontendAppMetaData> apps;
|
||||||
|
|
||||||
private List<AppTreeNode> appTree;
|
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
|
** Getter for appTree
|
||||||
**
|
**
|
||||||
|
@ -38,6 +38,11 @@ public class ProcessSummaryLine implements Serializable
|
|||||||
private Integer count = 0;
|
private Integer count = 0;
|
||||||
private String message;
|
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 //
|
// using ArrayList, because we need to be Serializable, and List is not //
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
@ -240,4 +245,160 @@ public class ProcessSummaryLine implements Serializable
|
|||||||
rs.add(this);
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.QAppChildMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
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.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.model.metadata.tables.QTableMetaData;
|
||||||
|
|
||||||
|
|
||||||
@ -64,6 +65,10 @@ public class AppTreeNode
|
|||||||
{
|
{
|
||||||
this.type = AppTreeNodeType.PROCESS;
|
this.type = AppTreeNodeType.PROCESS;
|
||||||
}
|
}
|
||||||
|
else if(appChildMetaData.getClass().equals(QReportMetaData.class))
|
||||||
|
{
|
||||||
|
this.type = AppTreeNodeType.REPORT;
|
||||||
|
}
|
||||||
else if(appChildMetaData.getClass().equals(QAppMetaData.class))
|
else if(appChildMetaData.getClass().equals(QAppMetaData.class))
|
||||||
{
|
{
|
||||||
this.type = AppTreeNodeType.APP;
|
this.type = AppTreeNodeType.APP;
|
||||||
|
@ -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
|
public enum AppTreeNodeType
|
||||||
{
|
{
|
||||||
TABLE,
|
TABLE,
|
||||||
PROCESS,
|
PROCESS,
|
||||||
|
REPORT,
|
||||||
APP
|
APP
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -36,6 +36,7 @@ public class QAppSection
|
|||||||
|
|
||||||
private List<String> tables;
|
private List<String> tables;
|
||||||
private List<String> processes;
|
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.name = name;
|
||||||
this.label = label;
|
this.label = label;
|
||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
this.tables = tables;
|
this.tables = tables;
|
||||||
this.processes = processes;
|
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
|
** Getter for icon
|
||||||
**
|
**
|
||||||
|
@ -32,6 +32,7 @@ public enum QComponentType
|
|||||||
VALIDATION_REVIEW_SCREEN,
|
VALIDATION_REVIEW_SCREEN,
|
||||||
EDIT_FORM,
|
EDIT_FORM,
|
||||||
VIEW_FORM,
|
VIEW_FORM,
|
||||||
|
DOWNLOAD_FORM,
|
||||||
RECORD_LIST,
|
RECORD_LIST,
|
||||||
PROCESS_SUMMARY_RESULTS;
|
PROCESS_SUMMARY_RESULTS;
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -29,7 +29,6 @@ public class QReportField
|
|||||||
{
|
{
|
||||||
private String name;
|
private String name;
|
||||||
private String label;
|
private String label;
|
||||||
private String fieldName;
|
|
||||||
private String formula;
|
private String formula;
|
||||||
private String displayFormat;
|
private String displayFormat;
|
||||||
// todo - type?
|
// 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
|
** Getter for formula
|
||||||
**
|
**
|
||||||
|
@ -25,20 +25,27 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Meta-data definition of a report generated by QQQ
|
** Meta-data definition of a report generated by QQQ
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class QReportMetaData
|
public class QReportMetaData implements QAppChildMetaData
|
||||||
{
|
{
|
||||||
private String name;
|
private String name;
|
||||||
private String label;
|
private String label;
|
||||||
private List<QFieldMetaData> inputFields;
|
private List<QFieldMetaData> inputFields;
|
||||||
private String sourceTable;
|
private String sourceTable;
|
||||||
|
private String processName;
|
||||||
private QQueryFilter queryFilter;
|
private QQueryFilter queryFilter;
|
||||||
|
private QQueryFilter varianceQueryFilter;
|
||||||
private List<QReportView> views;
|
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
|
** 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
|
** Getter for views
|
||||||
**
|
**
|
||||||
@ -243,4 +318,60 @@ public class QReportMetaData
|
|||||||
return (this);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,8 @@ public class QReportView
|
|||||||
private String titleFormat;
|
private String titleFormat;
|
||||||
private List<String> titleFields;
|
private List<String> titleFields;
|
||||||
private List<String> pivotFields;
|
private List<String> pivotFields;
|
||||||
private boolean totalRow = false;
|
private boolean totalRow = false;
|
||||||
|
private boolean pivotSubTotals = false;
|
||||||
private List<QReportField> columns;
|
private List<QReportField> columns;
|
||||||
private List<QFilterOrderBy> orderByFields;
|
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
|
** Getter for columns
|
||||||
**
|
**
|
||||||
|
@ -28,5 +28,5 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting;
|
|||||||
public enum ReportType
|
public enum ReportType
|
||||||
{
|
{
|
||||||
PIVOT,
|
PIVOT,
|
||||||
SIMPLE
|
TABLE
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,11 @@ package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwit
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
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
|
public interface ProcessSummaryProviderInterface
|
||||||
{
|
{
|
||||||
@ -37,4 +38,29 @@ public interface ProcessSummaryProviderInterface
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
ArrayList<ProcessSummaryLine> getProcessSummary(boolean isForResultScreen);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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? //
|
// 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);
|
transformStep.postRun(runBackendStepInput, runBackendStepOutput);
|
||||||
loadStep.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 //
|
// 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);
|
StreamedBackendStepOutput streamedBackendStepOutput = new StreamedBackendStepOutput(runBackendStepOutput);
|
||||||
|
|
||||||
/////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////
|
||||||
|
@ -105,7 +105,7 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
|
|||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
// get the process summary from the validation step //
|
// 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);
|
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 //
|
// 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);
|
StreamedBackendStepOutput streamedBackendStepOutput = new StreamedBackendStepOutput(runBackendStepOutput);
|
||||||
|
|
||||||
/////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,7 @@ import java.util.Objects;
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Simple container for two objects
|
** Simple container for two objects
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class Pair<A, B>
|
public class Pair<A, B> implements Cloneable
|
||||||
{
|
{
|
||||||
private A a;
|
private A a;
|
||||||
private B b;
|
private B b;
|
||||||
@ -46,6 +46,9 @@ public class Pair<A, B>
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
@ -104,4 +107,23 @@ public class Pair<A, B>
|
|||||||
{
|
{
|
||||||
return Objects.hash(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ class WidgetDataLoaderTest
|
|||||||
Object widgetData = new WidgetDataLoader().execute(TestUtils.defineInstance(), TestUtils.getMockSession(), PersonsByCreateDateBarChart.class.getSimpleName());
|
Object widgetData = new WidgetDataLoader().execute(TestUtils.defineInstance(), TestUtils.getMockSession(), PersonsByCreateDateBarChart.class.getSimpleName());
|
||||||
assertThat(widgetData).isInstanceOf(ChartData.class);
|
assertThat(widgetData).isInstanceOf(ChartData.class);
|
||||||
ChartData chartData = (ChartData) widgetData;
|
ChartData chartData = (ChartData) widgetData;
|
||||||
assertEquals("chartData", chartData.getType());
|
assertEquals("barChart", chartData.getType());
|
||||||
assertThat(chartData.getTitle()).isNotBlank();
|
assertThat(chartData.getTitle()).isNotBlank();
|
||||||
assertNotNull(chartData.getChartData());
|
assertNotNull(chartData.getChartData());
|
||||||
}
|
}
|
||||||
|
@ -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.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.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)"));
|
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)"));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -59,7 +59,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Unit test for GenerateReportAction
|
** Unit test for GenerateReportAction
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
class GenerateReportActionTest
|
public class GenerateReportActionTest
|
||||||
{
|
{
|
||||||
private static final String REPORT_NAME = "personReport1";
|
private static final String REPORT_NAME = "personReport1";
|
||||||
|
|
||||||
@ -243,32 +243,32 @@ class GenerateReportActionTest
|
|||||||
Map<String, String> row = iterator.next();
|
Map<String, String> row = iterator.next();
|
||||||
assertEquals(6, list.size());
|
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("Last Name")).isEqualTo("Jonson");
|
||||||
assertThat(row.get("Quantity")).isNull();
|
assertThat(row.get("Quantity")).isNull();
|
||||||
|
|
||||||
row = iterator.next();
|
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("Last Name")).isEqualTo("Jones");
|
||||||
assertThat(row.get("Quantity")).isEqualTo("3");
|
assertThat(row.get("Quantity")).isEqualTo("3");
|
||||||
|
|
||||||
row = iterator.next();
|
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("Last Name")).isEqualTo("Kelly");
|
||||||
assertThat(row.get("Quantity")).isEqualTo("4");
|
assertThat(row.get("Quantity")).isEqualTo("4");
|
||||||
|
|
||||||
row = iterator.next();
|
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("Last Name")).isEqualTo("Keller");
|
||||||
assertThat(row.get("Quantity")).isEqualTo("5");
|
assertThat(row.get("Quantity")).isEqualTo("5");
|
||||||
|
|
||||||
row = iterator.next();
|
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("Last Name")).isEqualTo("Kelkhoff");
|
||||||
assertThat(row.get("Quantity")).isEqualTo("6");
|
assertThat(row.get("Quantity")).isEqualTo("6");
|
||||||
|
|
||||||
row = iterator.next();
|
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("Last Name")).isEqualTo("Kelkhoff");
|
||||||
assertThat(row.get("Quantity")).isEqualTo("7");
|
assertThat(row.get("Quantity")).isEqualTo("7");
|
||||||
}
|
}
|
||||||
@ -297,15 +297,14 @@ class GenerateReportActionTest
|
|||||||
Iterator<Map<String, String>> iterator = list.iterator();
|
Iterator<Map<String, String>> iterator = list.iterator();
|
||||||
Map<String, String> row = iterator.next();
|
Map<String, String> row = iterator.next();
|
||||||
assertEquals(2, list.size());
|
assertEquals(2, list.size());
|
||||||
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("Last Name")).isNull();
|
||||||
assertThat(row.get("Quantity")).isEqualTo("7");
|
assertThat(row.get("Quantity")).isEqualTo("7");
|
||||||
|
|
||||||
row = iterator.next();
|
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("Last Name")).isNull();
|
||||||
assertThat(row.get("Quantity")).isEqualTo("18");
|
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()
|
return new QReportMetaData()
|
||||||
.withName(REPORT_NAME)
|
.withName(REPORT_NAME)
|
||||||
|
@ -584,7 +584,7 @@ class QInstanceValidatorTest
|
|||||||
{
|
{
|
||||||
QAppMetaData app = new QAppMetaData().withName("test")
|
QAppMetaData app = new QAppMetaData().withName("test")
|
||||||
.withChild(new QTableMetaData().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");
|
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "Missing a name");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -598,7 +598,7 @@ class QInstanceValidatorTest
|
|||||||
{
|
{
|
||||||
QAppMetaData app = new QAppMetaData().withName("test")
|
QAppMetaData app = new QAppMetaData().withName("test")
|
||||||
.withChild(new QTableMetaData().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");
|
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "Missing a label");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,12 +612,12 @@ class QInstanceValidatorTest
|
|||||||
{
|
{
|
||||||
QAppMetaData app1 = new QAppMetaData().withName("test")
|
QAppMetaData app1 = new QAppMetaData().withName("test")
|
||||||
.withChild(new QTableMetaData().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");
|
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")
|
QAppMetaData app2 = new QAppMetaData().withName("test")
|
||||||
.withChild(new QTableMetaData().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");
|
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")
|
QAppMetaData app1 = new QAppMetaData().withName("test")
|
||||||
.withChild(new QTableMetaData().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");
|
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app1), "not a child of this app");
|
||||||
QAppMetaData app2 = new QAppMetaData().withName("test")
|
QAppMetaData app2 = new QAppMetaData().withName("test")
|
||||||
.withChild(new QTableMetaData().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");
|
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app2), "not a child of this app");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -649,23 +649,23 @@ class QInstanceValidatorTest
|
|||||||
{
|
{
|
||||||
QAppMetaData app1 = new QAppMetaData().withName("test")
|
QAppMetaData app1 = new QAppMetaData().withName("test")
|
||||||
.withChild(new QTableMetaData().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");
|
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app1), "more than once");
|
||||||
|
|
||||||
QAppMetaData app2 = new QAppMetaData().withName("test")
|
QAppMetaData app2 = new QAppMetaData().withName("test")
|
||||||
.withChild(new QTableMetaData().withName("test"))
|
.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))
|
||||||
.withSection(new QAppSection("section2", "Section 2", new QIcon("person"), List.of("test"), null));
|
.withSection(new QAppSection("section2", "Section 2", new QIcon("person"), List.of("test"), null, null));
|
||||||
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app2), "more than once");
|
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app2), "more than once");
|
||||||
|
|
||||||
QAppMetaData app3 = new QAppMetaData().withName("test")
|
QAppMetaData app3 = new QAppMetaData().withName("test")
|
||||||
.withChild(new QTableMetaData().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");
|
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app3), "more than once");
|
||||||
|
|
||||||
QAppMetaData app4 = new QAppMetaData().withName("test")
|
QAppMetaData app4 = new QAppMetaData().withName("test")
|
||||||
.withChild(new QTableMetaData().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");
|
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app4), "more than once");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -687,7 +687,7 @@ class QInstanceValidatorTest
|
|||||||
QAppMetaData app = new QAppMetaData().withName("test")
|
QAppMetaData app = new QAppMetaData().withName("test")
|
||||||
.withChild(new QTableMetaData().withName("tset"))
|
.withChild(new QTableMetaData().withName("tset"))
|
||||||
.withChild(new QTableMetaData().withName("test"))
|
.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");
|
assertValidationFailureReasons((qInstance) -> qInstance.addApp(app), "not listed in any app sections");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}", "--"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -22,6 +22,8 @@
|
|||||||
package com.kingsrook.qqq.backend.javalin;
|
package com.kingsrook.qqq.backend.javalin;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
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)
|
** Init a process (named in path param :process)
|
||||||
**
|
**
|
||||||
|
@ -451,8 +451,8 @@ class QJavalinImplementationTest extends QJavalinTestBase
|
|||||||
@Test
|
@Test
|
||||||
void testExportFieldsQueryParam()
|
void testExportFieldsQueryParam()
|
||||||
{
|
{
|
||||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/export/People.csv?fields=id,birthDate").asString();
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/export/People.csv?fields=id,birthDate").asString();
|
||||||
String[] csvLines = response.getBody().split("\n");
|
String[] csvLines = response.getBody().split("\n");
|
||||||
assertEquals("""
|
assertEquals("""
|
||||||
"Id","Birth Date\"""", csvLines[0]);
|
"Id","Birth Date\"""", csvLines[0]);
|
||||||
}
|
}
|
||||||
@ -484,7 +484,7 @@ class QJavalinImplementationTest extends QJavalinTestBase
|
|||||||
assertNotNull(jsonObject);
|
assertNotNull(jsonObject);
|
||||||
assertEquals("barChart", jsonObject.getString("type"));
|
assertEquals("barChart", jsonObject.getString("type"));
|
||||||
assertNotNull(jsonObject.getString("title"));
|
assertNotNull(jsonObject.getString("title"));
|
||||||
assertNotNull(jsonObject.getJSONObject("barChartData"));
|
assertNotNull(jsonObject.getJSONObject("chartData"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Unit test for PersonsByCreateDateBarChart
|
** Unit test for PersonsByCreateDateBarChart
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
class PersonsByCreateDateChartTestData
|
class PersonsByCreateDateBarChartTest
|
||||||
{
|
{
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -47,7 +47,7 @@ class PersonsByCreateDateChartTestData
|
|||||||
Object widgetData = new PersonsByCreateDateBarChart().render(SampleMetaDataProvider.defineInstance(), new QSession(), null);
|
Object widgetData = new PersonsByCreateDateBarChart().render(SampleMetaDataProvider.defineInstance(), new QSession(), null);
|
||||||
assertThat(widgetData).isInstanceOf(ChartData.class);
|
assertThat(widgetData).isInstanceOf(ChartData.class);
|
||||||
ChartData chartData = (ChartData) widgetData;
|
ChartData chartData = (ChartData) widgetData;
|
||||||
assertEquals("chartData", chartData.getType());
|
assertEquals("barChart", chartData.getType());
|
||||||
assertThat(chartData.getTitle()).isNotBlank();
|
assertThat(chartData.getTitle()).isNotBlank();
|
||||||
assertNotNull(chartData.getChartData());
|
assertNotNull(chartData.getChartData());
|
||||||
}
|
}
|
Reference in New Issue
Block a user