mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
CE-1405 Updates to qqq-reports: support for ReportCustomRecordSourceInterface
This commit is contained in:
@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.DataSourceQueryInputCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportViewCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
@ -302,10 +303,19 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
JoinsContext joinsContext = null;
|
||||
if(dataSource != null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// count records, if applicable, from the data source - for populating into the //
|
||||
// countByDataSource map, as well as for checking if too many rows (e.g., for excel) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
countDataSourceRecords(reportInput, dataSource, reportFormat);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there's a source table, set up a joins context, to use below for looking up fields //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
{
|
||||
joinsContext = new JoinsContext(QContext.getQInstance(), dataSource.getSourceTable(), cloneDataSourceQueryJoins(dataSource), dataSource.getQueryFilter() == null ? null : dataSource.getQueryFilter().clone());
|
||||
countDataSourceRecords(reportInput, dataSource, reportFormat);
|
||||
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
|
||||
joinsContext = new JoinsContext(QContext.getQInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryFilter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -329,6 +339,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
field.setName(column.getName());
|
||||
if(StringUtils.hasContent(column.getLabel()))
|
||||
{
|
||||
|
||||
field.setLabel(column.getLabel());
|
||||
}
|
||||
fields.add(field);
|
||||
@ -345,6 +356,13 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void countDataSourceRecords(ReportInput reportInput, QReportDataSource dataSource, ReportFormat reportFormat) throws QException
|
||||
{
|
||||
Integer count = null;
|
||||
if(dataSource.getCustomRecordSource() != null)
|
||||
{
|
||||
// todo - add `count` method to interface?
|
||||
}
|
||||
else if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
{
|
||||
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
|
||||
setInputValuesInQueryFilter(reportInput, queryFilter);
|
||||
@ -355,14 +373,17 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
countInput.setQueryJoins(cloneDataSourceQueryJoins(dataSource));
|
||||
CountOutput countOutput = new CountAction().execute(countInput);
|
||||
|
||||
if(countOutput.getCount() != null)
|
||||
{
|
||||
countByDataSource.put(dataSource.getName(), countOutput.getCount());
|
||||
count = countOutput.getCount();
|
||||
}
|
||||
|
||||
if(reportFormat.getMaxRows() != null && countOutput.getCount() > reportFormat.getMaxRows())
|
||||
if(count != null)
|
||||
{
|
||||
countByDataSource.put(dataSource.getName(), count);
|
||||
|
||||
if(reportFormat.getMaxRows() != null && count > reportFormat.getMaxRows())
|
||||
{
|
||||
throw (new QUserFacingException("The requested report would include more rows ("
|
||||
+ String.format("%,d", countOutput.getCount()) + ") than the maximum allowed ("
|
||||
+ String.format("%,d", count) + ") than the maximum allowed ("
|
||||
+ String.format("%,d", reportFormat.getMaxRows()) + ") for the selected file format (" + reportFormat + ")."));
|
||||
}
|
||||
}
|
||||
@ -423,13 +444,19 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
String tableLabel = ObjectUtils.tryElse(() -> QContext.getQInstance().getTable(dataSource.getSourceTable()).getLabel(), Objects.requireNonNullElse(dataSource.getSourceTable(), ""));
|
||||
AtomicInteger consumedCount = new AtomicInteger(0);
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// run a record pipe loop, over the query for this data source //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// run a record pipe loop, over the query (or other data-supplier/source) for this data source //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
RecordPipe recordPipe = new BufferedRecordPipe(1000);
|
||||
new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) ->
|
||||
{
|
||||
if(dataSource.getSourceTable() != null)
|
||||
if(dataSource.getCustomRecordSource() != null)
|
||||
{
|
||||
ReportCustomRecordSourceInterface recordSource = QCodeLoader.getAdHoc(ReportCustomRecordSourceInterface.class, dataSource.getCustomRecordSource());
|
||||
recordSource.execute(reportInput, recordPipe);
|
||||
return (true);
|
||||
}
|
||||
else if(dataSource.getSourceTable() != null)
|
||||
{
|
||||
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
|
||||
setInputValuesInQueryFilter(reportInput, queryFilter);
|
||||
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.customizers;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Interface to be implemented to do a custom source of data for a report
|
||||
** (instead of just a query against a table).
|
||||
*******************************************************************************/
|
||||
public interface ReportCustomRecordSourceInterface
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
** Given the report input, put records into the pipe, for the report.
|
||||
***************************************************************************/
|
||||
void execute(ReportInput reportInput, RecordPipe recordPipe) throws QException;
|
||||
|
||||
}
|
@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
@ -1659,9 +1660,12 @@ public class QInstanceValidator
|
||||
|
||||
String dataSourceErrorPrefix = "Report " + reportName + " data source " + dataSource.getName() + " ";
|
||||
|
||||
boolean hasASource = false;
|
||||
|
||||
if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
{
|
||||
assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (exactly 1 is required).");
|
||||
hasASource = true;
|
||||
assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (not compatible together).");
|
||||
if(assertCondition(qInstance.getTable(dataSource.getSourceTable()) != null, dataSourceErrorPrefix + "source table " + dataSource.getSourceTable() + " is not a table in this instance."))
|
||||
{
|
||||
if(dataSource.getQueryFilter() != null)
|
||||
@ -1670,14 +1674,21 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(dataSource.getStaticDataSupplier() != null)
|
||||
|
||||
if(dataSource.getStaticDataSupplier() != null)
|
||||
{
|
||||
assertCondition(dataSource.getCustomRecordSource() == null, dataSourceErrorPrefix + "has both a staticDataSupplier and a customRecordSource (not compatible together).");
|
||||
hasASource = true;
|
||||
validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getStaticDataSupplier(), Supplier.class);
|
||||
}
|
||||
else
|
||||
|
||||
if(dataSource.getCustomRecordSource() != null)
|
||||
{
|
||||
errors.add(dataSourceErrorPrefix + "does not have a sourceTable or a staticDataSupplier (exactly 1 is required).");
|
||||
hasASource = true;
|
||||
validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getCustomRecordSource(), ReportCustomRecordSourceInterface.class);
|
||||
}
|
||||
|
||||
assertCondition(hasASource, dataSourceErrorPrefix + "does not have a sourceTable, customRecordSource, or a staticDataSupplier.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
/*******************************************************************************
|
||||
** Meta-data definition of a source of data for a report (e.g., a table and query
|
||||
** filter or custom-code reference).
|
||||
**
|
||||
** Runs in 3 modes:
|
||||
**
|
||||
** - If a customRecordSource is specified, then that code is executed to get the records.
|
||||
** - else, if a sourceTable is specified, then the corresponding queryFilter
|
||||
** (optionally along with queryJoins and queryInputCustomizer) is used.
|
||||
** - else a staticDataSupplier is used.
|
||||
*******************************************************************************/
|
||||
public class QReportDataSource
|
||||
{
|
||||
@ -44,6 +51,7 @@ public class QReportDataSource
|
||||
|
||||
private QCodeReference queryInputCustomizer;
|
||||
private QCodeReference staticDataSupplier;
|
||||
private QCodeReference customRecordSource;
|
||||
|
||||
|
||||
|
||||
@ -265,4 +273,35 @@ public class QReportDataSource
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for customRecordSource
|
||||
*******************************************************************************/
|
||||
public QCodeReference getCustomRecordSource()
|
||||
{
|
||||
return (this.customRecordSource);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for customRecordSource
|
||||
*******************************************************************************/
|
||||
public void setCustomRecordSource(QCodeReference customRecordSource)
|
||||
{
|
||||
this.customRecordSource = customRecordSource;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for customRecordSource
|
||||
*******************************************************************************/
|
||||
public QReportDataSource withCustomRecordSource(QCodeReference customRecordSource)
|
||||
{
|
||||
this.customRecordSource = customRecordSource;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ public class BasicRunReportProcess
|
||||
public static final String STEP_NAME_ACCESS = "accessReport";
|
||||
|
||||
public static final String FIELD_REPORT_NAME = "reportName";
|
||||
public static final String FIELD_REPORT_FORMAT = "reportFormat";
|
||||
|
||||
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.instances;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@ -38,6 +39,8 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarCh
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ParentWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.CancelProcessActionTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
@ -45,6 +48,7 @@ import com.kingsrook.qqq.backend.core.instances.validation.plugins.AlwaysFailsPr
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
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.ReportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
@ -1698,7 +1702,7 @@ public class QInstanceValidatorTest extends BaseTest
|
||||
@Test
|
||||
void testReportDataSourceStaticDataSupplier()
|
||||
{
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).withStaticDataSupplier(new QCodeReference()),
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).withStaticDataSupplier(new QCodeReference(TestReportStaticDataSupplier.class)),
|
||||
"has both a sourceTable and a staticDataSupplier");
|
||||
|
||||
assertValidationFailureReasons((qInstance) ->
|
||||
@ -1706,16 +1710,43 @@ public class QInstanceValidatorTest extends BaseTest
|
||||
QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0);
|
||||
dataSource.setSourceTable(null);
|
||||
dataSource.setStaticDataSupplier(new QCodeReference(null, QCodeType.JAVA));
|
||||
},
|
||||
"missing a code reference name");
|
||||
}, "missing a code reference name");
|
||||
|
||||
assertValidationFailureReasons((qInstance) ->
|
||||
{
|
||||
QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0);
|
||||
dataSource.setSourceTable(null);
|
||||
dataSource.setStaticDataSupplier(new QCodeReference(ArrayList.class));
|
||||
},
|
||||
"is not of the expected type");
|
||||
}, "is not of the expected type");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testReportDataSourceCustomRecordSource()
|
||||
{
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0)
|
||||
.withSourceTable(null)
|
||||
.withStaticDataSupplier(new QCodeReference(TestReportStaticDataSupplier.class))
|
||||
.withCustomRecordSource(new QCodeReference(TestReportCustomRecordSource.class)),
|
||||
"has both a staticDataSupplier and a customRecordSource");
|
||||
|
||||
assertValidationFailureReasons((qInstance) ->
|
||||
{
|
||||
QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0);
|
||||
dataSource.setSourceTable(null);
|
||||
dataSource.setCustomRecordSource(new QCodeReference(null, QCodeType.JAVA));
|
||||
}, "missing a code reference name");
|
||||
|
||||
assertValidationFailureReasons((qInstance) ->
|
||||
{
|
||||
QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0);
|
||||
dataSource.setSourceTable(null);
|
||||
dataSource.setCustomRecordSource(new QCodeReference(ArrayList.class));
|
||||
}, "is not of the expected type");
|
||||
}
|
||||
|
||||
|
||||
@ -2339,5 +2370,38 @@ public class QInstanceValidatorTest extends BaseTest
|
||||
*******************************************************************************/
|
||||
public static class ValidAuthCustomizer implements QAuthenticationModuleCustomizerInterface {}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class TestReportStaticDataSupplier implements Supplier<List<List<Serializable>>>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<List<Serializable>> get()
|
||||
{
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class TestReportCustomRecordSource implements ReportCustomRecordSourceInterface
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void execute(ReportInput reportInput, RecordPipe recordPipe) throws QException
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user