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

This commit is contained in:
2022-09-20 12:49:37 -05:00
43 changed files with 1716 additions and 193 deletions

View File

@ -33,10 +33,12 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode; import com.kingsrook.qqq.backend.core.model.metadata.frontend.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 //
/////////////////////////////////// ///////////////////////////////////

View File

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

View File

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

View File

@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.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();

View File

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

View File

@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.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);
}
} }

View File

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

View File

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

View File

@ -32,7 +32,7 @@ import com.kingsrook.qqq.backend.core.utils.Pair;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public class PivotKey public class PivotKey implements Cloneable
{ {
private List<Pair<String, Serializable>> keys = new ArrayList<>(); 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);
}
} }

View File

@ -0,0 +1,71 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
import org.dhatim.fastexcel.BorderSide;
import org.dhatim.fastexcel.BorderStyle;
import org.dhatim.fastexcel.StyleSetter;
/*******************************************************************************
**
*******************************************************************************/
public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void styleTitleRow(StyleSetter titleRowStyle)
{
titleRowStyle
.bold()
.fontSize(14)
.horizontalAlignment("center");
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void styleHeaderRow(StyleSetter headerRowStyle)
{
headerRowStyle
.bold()
.borderStyle(BorderSide.BOTTOM, BorderStyle.THIN);
}
@Override
public void styleTotalsRow(StyleSetter totalsRowStyle)
{
totalsRowStyle
.bold()
.borderStyle(BorderSide.TOP, BorderStyle.THIN)
.borderStyle(BorderSide.BOTTOM, BorderStyle.DOUBLE);
}
}

View File

@ -0,0 +1,58 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
import org.dhatim.fastexcel.StyleSetter;
/*******************************************************************************
**
*******************************************************************************/
public interface ExcelStylerInterface
{
/*******************************************************************************
**
*******************************************************************************/
default void styleTitleRow(StyleSetter titleRowStyle)
{
}
/*******************************************************************************
**
*******************************************************************************/
default void styleHeaderRow(StyleSetter headerRowStyle)
{
}
/*******************************************************************************
**
*******************************************************************************/
default void styleTotalsRow(StyleSetter totalsRowStyle)
{
}
}

View File

@ -0,0 +1,31 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
/*******************************************************************************
**
*******************************************************************************/
public class PlainExcelStyler implements ExcelStylerInterface
{
}

View File

@ -294,11 +294,26 @@ public class QPossibleValueTranslator
{ {
for(QFieldMetaData field : fieldsByPvsTable.get(tableName)) for(QFieldMetaData field : fieldsByPvsTable.get(tableName))
{ {
values.add(record.getValue(field.getName())); Serializable fieldValue = record.getValue(field.getName());
//////////////////////////////////////
// check if value is already cached //
//////////////////////////////////////
QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0);
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
Map<Serializable, String> cacheForPvs = possibleValueCache.get(possibleValueSource.getName());
if(!cacheForPvs.containsKey(fieldValue))
{
values.add(fieldValue);
}
} }
} }
primePvsCache(tableName, pvsesByTable.get(tableName), values); if(!values.isEmpty())
{
primePvsCache(tableName, pvsesByTable.get(tableName), values);
}
} }
} }

View File

@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponen
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.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;
}
} }
} }

View File

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

View File

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

View File

@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode; import com.kingsrook.qqq.backend.core.model.metadata.frontend.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
** **

View File

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

View File

@ -27,6 +27,7 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.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;

View File

@ -23,11 +23,12 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend;
/******************************************************************************* /*******************************************************************************
** Type for an Node in the an app tree. ** Type for an Node in the app tree.
*******************************************************************************/ *******************************************************************************/
public enum AppTreeNodeType public enum AppTreeNodeType
{ {
TABLE, TABLE,
PROCESS, PROCESS,
REPORT,
APP APP
} }

View File

@ -0,0 +1,122 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.frontend;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
/*******************************************************************************
* Version of QReportMetaData that's meant for transmitting to a frontend.
* e.g., it excludes backend-only details.
*
*******************************************************************************/
@JsonInclude(Include.NON_NULL)
public class QFrontendReportMetaData
{
private String name;
private String label;
private String tableName;
private String processName;
private String iconName;
//////////////////////////////////////////////////////////////////////////////////
// do not add setters. take values from the source-object in the constructor!! //
//////////////////////////////////////////////////////////////////////////////////
/*******************************************************************************
**
*******************************************************************************/
public QFrontendReportMetaData(QReportMetaData reportMetaData, boolean includeSteps)
{
this.name = reportMetaData.getName();
this.label = reportMetaData.getLabel();
this.tableName = reportMetaData.getSourceTable();
this.processName = reportMetaData.getProcessName();
if(reportMetaData.getIcon() != null)
{
this.iconName = reportMetaData.getIcon().getName();
}
}
/*******************************************************************************
** Getter for name
**
*******************************************************************************/
public String getName()
{
return name;
}
/*******************************************************************************
** Getter for label
**
*******************************************************************************/
public String getLabel()
{
return label;
}
/*******************************************************************************
** Getter for primaryKeyField
**
*******************************************************************************/
public String getTableName()
{
return tableName;
}
/*******************************************************************************
** Getter for processName
**
*******************************************************************************/
public String getProcessName()
{
return processName;
}
/*******************************************************************************
** Getter for iconName
**
*******************************************************************************/
public String getIconName()
{
return iconName;
}
}

View File

@ -36,6 +36,7 @@ public class QAppSection
private List<String> tables; private List<String> 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
** **

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -90,7 +90,7 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
//////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////
// get the process summary from the ... transform step? the load step? each knows some... todo? // // 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);
///////////////////////////////////////////////////// /////////////////////////////////////////////////////

View File

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

View File

@ -0,0 +1,89 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.implementations.reports;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
/*******************************************************************************
** Definition for Basic process to run a report.
*******************************************************************************/
public class BasicRunReportProcess
{
public static final String PROCESS_NAME = "reports.basic";
public static final String STEP_NAME_PREPARE = "prepare";
public static final String STEP_NAME_INPUT = "input";
public static final String STEP_NAME_EXECUTE = "execute";
public static final String STEP_NAME_ACCESS = "accessReport";
public static final String FIELD_REPORT_NAME = "reportName";
/*******************************************************************************
**
*******************************************************************************/
public static QProcessMetaData defineProcessMetaData()
{
QStepMetaData prepareStep = new QBackendStepMetaData()
.withName(STEP_NAME_PREPARE)
.withCode(new QCodeReference(PrepareReportStep.class))
.withInputData(new QFunctionInputMetaData()
.withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING)));
QStepMetaData inputStep = new QFrontendStepMetaData()
.withName(STEP_NAME_INPUT)
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
QStepMetaData executeStep = new QBackendStepMetaData()
.withName(STEP_NAME_EXECUTE)
.withCode(new QCodeReference(ExecuteReportStep.class))
.withInputData(new QFunctionInputMetaData()
.withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING)));
QStepMetaData accessStep = new QFrontendStepMetaData()
.withName(STEP_NAME_ACCESS)
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.DOWNLOAD_FORM));
// .withViewField(new QFieldMetaData("outputFile", QFieldType.STRING))
// .withViewField(new QFieldMetaData("message", QFieldType.STRING));
return new QProcessMetaData()
.withName(PROCESS_NAME)
.withIsHidden(true)
.addStep(prepareStep)
.addStep(inputStep)
.addStep(executeStep)
.addStep(accessStep);
}
}

View File

@ -0,0 +1,85 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.implementations.reports;
import java.io.File;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
/*******************************************************************************
** Process step to execute a report.
**
** Writes it to a temp file... Returns that file name in process output.
*******************************************************************************/
public class ExecuteReportStep implements BackendStep
{
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
try
{
String reportName = runBackendStepInput.getValueString("reportName");
QReportMetaData report = runBackendStepInput.getInstance().getReport(reportName);
File tmpFile = File.createTempFile(reportName, ".xlsx", new File("/tmp/"));
runBackendStepInput.getAsyncJobCallback().updateStatus("Generating Report");
try(FileOutputStream reportOutputStream = new FileOutputStream(tmpFile))
{
ReportInput reportInput = new ReportInput(runBackendStepInput.getInstance());
reportInput.setSession(runBackendStepInput.getSession());
reportInput.setReportName(reportName);
reportInput.setReportFormat(ReportFormat.XLSX); // todo - variable
reportInput.setReportOutputStream(reportOutputStream);
Map<String, Serializable> values = runBackendStepInput.getValues();
reportInput.setInputValues(values);
new GenerateReportAction().execute(reportInput);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmm").withZone(ZoneId.systemDefault());
String datePart = formatter.format(Instant.now());
runBackendStepOutput.addValue("downloadFileName", report.getLabel() + " " + datePart + ".xlsx");
runBackendStepOutput.addValue("serverFilePath", tmpFile.getCanonicalPath());
}
}
catch(Exception e)
{
throw (new QException("Error running report", e));
}
}
}

View File

@ -0,0 +1,79 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.implementations.reports;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Process step to prepare for running a report.
**
** Checks for input fields - if there are any, it puts them in process value output
** as inputFieldList (QFieldMetaData objects).
** If there aren't any input fields, re-routes the process to skip the input screen.
*******************************************************************************/
public class PrepareReportStep implements BackendStep
{
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
String reportName = runBackendStepInput.getValueString("reportName");
if(!StringUtils.hasContent(reportName))
{
throw (new QException("Process value [reportName] was not given."));
}
QReportMetaData report = runBackendStepInput.getInstance().getReport(reportName);
if(report == null)
{
throw (new QException("Process named [" + reportName + "] was not found in this instance."));
}
/////////////////////////////////////////////////////////////////
// if there are input fields, communicate them to the frontend //
/////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(report.getInputFields()))
{
ArrayList<QFieldMetaData> inputFieldList = new ArrayList<>(report.getInputFields());
runBackendStepOutput.addValue("inputFieldList", inputFieldList);
}
else
{
//////////////////////////////////////////////////////////////
// no input? re-route the process to skip the input screen //
//////////////////////////////////////////////////////////////
List<String> stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList());
stepList.removeIf(s -> s.equals(BasicRunReportProcess.STEP_NAME_INPUT));
runBackendStepOutput.getProcessState().setStepList(stepList);
}
}
}

View File

@ -28,7 +28,7 @@ import java.util.Objects;
/******************************************************************************* /*******************************************************************************
** Simple container for two objects ** 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();
}
}
} }

View File

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

View File

@ -32,7 +32,9 @@ import static com.kingsrook.qqq.backend.core.actions.reporting.FormulaInterprete
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.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)"));
}
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,8 @@
package com.kingsrook.qqq.backend.javalin; 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)
** **

View File

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

View File

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