CE-1115 pre-QA commit on saved report UI, including:

- adding pre-insert/update validation
- move json field value formatting from post-query customizer to a FieldDisplayBehavior instead (works for audits this way :)
This commit is contained in:
2024-04-14 20:14:32 -05:00
parent 32021fc36e
commit c89fe958c3
9 changed files with 800 additions and 42 deletions

View File

@ -303,7 +303,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
} }
else else
{ {
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value); String formattedValue = getFormattedValueForAuditDetail(table, record, fieldName, field, value);
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue); detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
detailRecord.withValue("newValue", formattedValue); detailRecord.withValue("newValue", formattedValue);
} }
@ -329,8 +329,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
} }
else else
{ {
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value); String formattedValue = getFormattedValueForAuditDetail(table, record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue); String formattedOldValue = getFormattedValueForAuditDetail(table, oldRecord, fieldName, field, oldValue);
if(oldValue == null) if(oldValue == null)
{ {
@ -464,7 +464,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private static String getFormattedValueForAuditDetail(QRecord record, String fieldName, QFieldMetaData field, Serializable value) private static String getFormattedValueForAuditDetail(QTableMetaData table, QRecord record, String fieldName, QFieldMetaData field, Serializable value)
{ {
String formattedValue = null; String formattedValue = null;
if(value != null) if(value != null)
@ -479,7 +479,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
} }
else else
{ {
formattedValue = QValueFormatter.formatValue(field, value); QValueFormatter.setDisplayValuesInRecord(table, table.getFields(), record);
formattedValue = record.getDisplayValue(fieldName);
} }
} }

View File

@ -531,7 +531,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException public static FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException
{ {
if(fieldName.indexOf('.') > -1) if(fieldName.indexOf('.') > -1)
{ {
@ -1097,5 +1097,22 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable) {} public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable)
{
/*******************************************************************************
**
*******************************************************************************/
public String getLabel(QTableMetaData mainTable)
{
if(mainTable.getName().equals(joinTable.getName()))
{
return (field.getLabel());
}
else
{
return (joinTable.getLabel() + ": " + field.getLabel());
}
}
}
} }

View File

@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.model.savedreports;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/******************************************************************************* /*******************************************************************************
@ -35,6 +37,7 @@ public class ReportColumns implements Serializable
private List<ReportColumn> columns; private List<ReportColumn> columns;
/******************************************************************************* /*******************************************************************************
** Getter for columns ** Getter for columns
*******************************************************************************/ *******************************************************************************/
@ -45,6 +48,23 @@ public class ReportColumns implements Serializable
/*******************************************************************************
**
*******************************************************************************/
public List<ReportColumn> extractVisibleColumns()
{
return CollectionUtils.nonNullList(getColumns()).stream()
//////////////////////////////////////////////////////
// if isVisible is missing, we assume it to be true //
//////////////////////////////////////////////////////
.filter(rc -> rc.getIsVisible() == null || rc.getIsVisible())
.filter(rc -> StringUtils.hasContent(rc.getName()))
.filter(rc -> !rc.getName().startsWith("__check"))
.toList();
}
/******************************************************************************* /*******************************************************************************
** Setter for columns ** Setter for columns
*******************************************************************************/ *******************************************************************************/
@ -65,6 +85,7 @@ public class ReportColumns implements Serializable
} }
/******************************************************************************* /*******************************************************************************
** Fluent setter to add 1 column ** Fluent setter to add 1 column
*******************************************************************************/ *******************************************************************************/
@ -79,6 +100,7 @@ public class ReportColumns implements Serializable
} }
/******************************************************************************* /*******************************************************************************
** Fluent setter to add 1 column w/ just a name ** Fluent setter to add 1 column w/ just a name
*******************************************************************************/ *******************************************************************************/

View File

@ -0,0 +1,150 @@
/*
* 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.model.savedreports;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldDisplayBehavior;
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.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
**
*******************************************************************************/
public class SavedReportJsonFieldDisplayValueFormatter implements FieldDisplayBehavior<SavedReportJsonFieldDisplayValueFormatter>
{
private static SavedReportJsonFieldDisplayValueFormatter savedReportJsonFieldDisplayValueFormatter = null;
/*******************************************************************************
** Singleton constructor
*******************************************************************************/
private SavedReportJsonFieldDisplayValueFormatter()
{
}
/*******************************************************************************
** Singleton accessor
*******************************************************************************/
public static SavedReportJsonFieldDisplayValueFormatter getInstance()
{
if(savedReportJsonFieldDisplayValueFormatter == null)
{
savedReportJsonFieldDisplayValueFormatter = new SavedReportJsonFieldDisplayValueFormatter();
}
return (savedReportJsonFieldDisplayValueFormatter);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public SavedReportJsonFieldDisplayValueFormatter getDefault()
{
return getInstance();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
for(QRecord record : CollectionUtils.nonNullList(recordList))
{
if(field.getName().equals("queryFilterJson"))
{
String queryFilterJson = record.getValueString("queryFilterJson");
if(StringUtils.hasContent(queryFilterJson))
{
try
{
QQueryFilter qQueryFilter = SavedReportToReportMetaDataAdapter.getQQueryFilter(queryFilterJson);
int criteriaCount = CollectionUtils.nonNullList(qQueryFilter.getCriteria()).size();
record.setDisplayValue("queryFilterJson", criteriaCount + " Filter" + StringUtils.plural(criteriaCount));
}
catch(Exception e)
{
record.setDisplayValue("queryFilterJson", "Invalid Filter...");
}
}
}
if(field.getName().equals("columnsJson"))
{
String columnsJson = record.getValueString("columnsJson");
if(StringUtils.hasContent(columnsJson))
{
try
{
ReportColumns reportColumns = SavedReportToReportMetaDataAdapter.getReportColumns(columnsJson);
int columnCount = reportColumns.extractVisibleColumns().size();
record.setDisplayValue("columnsJson", columnCount + " Column" + StringUtils.plural(columnCount));
}
catch(Exception e)
{
record.setDisplayValue("columnsJson", "Invalid Columns...");
}
}
}
if(field.getName().equals("pivotTableJson"))
{
String pivotTableJson = record.getValueString("pivotTableJson");
if(StringUtils.hasContent(pivotTableJson))
{
try
{
PivotTableDefinition pivotTableDefinition = SavedReportToReportMetaDataAdapter.getPivotTableDefinition(pivotTableJson);
int rowCount = CollectionUtils.nonNullList(pivotTableDefinition.getRows()).size();
int columnCount = CollectionUtils.nonNullList(pivotTableDefinition.getColumns()).size();
int valueCount = CollectionUtils.nonNullList(pivotTableDefinition.getValues()).size();
record.setDisplayValue("pivotTableJson", rowCount + " Row" + StringUtils.plural(rowCount) + ", " + columnCount + " Column" + StringUtils.plural(columnCount) + ", and " + valueCount + " Value" + StringUtils.plural(valueCount));
}
catch(Exception e)
{
record.setDisplayValue("pivotTableJson", "Invalid Pivot Table...");
}
}
}
}
}
}

View File

@ -22,17 +22,26 @@
package com.kingsrook.qqq.backend.core.model.savedreports; package com.kingsrook.qqq.backend.core.model.savedreports;
import java.io.IOException;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition; import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface; import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableGroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableValue;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter; import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter;
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.StringUtils;
import org.apache.commons.lang.BooleanUtils;
/******************************************************************************* /*******************************************************************************
@ -45,63 +54,229 @@ public class SavedReportTableCustomizer implements TableCustomizerInterface
** **
*******************************************************************************/ *******************************************************************************/
@Override @Override
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
return (preInsertOrUpdate(records));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
return (preInsertOrUpdate(records));
}
/*******************************************************************************
**
*******************************************************************************/
private List<QRecord> preInsertOrUpdate(List<QRecord> records)
{ {
for(QRecord record : CollectionUtils.nonNullList(records)) for(QRecord record : CollectionUtils.nonNullList(records))
{ {
preValidateRecord(record);
}
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
void preValidateRecord(QRecord record)
{
try
{
String tableName = record.getValueString("tableName");
String queryFilterJson = record.getValueString("queryFilterJson"); String queryFilterJson = record.getValueString("queryFilterJson");
String columnsJson = record.getValueString("columnsJson"); String columnsJson = record.getValueString("columnsJson");
String pivotTableJson = record.getValueString("pivotTableJson"); String pivotTableJson = record.getValueString("pivotTableJson");
Set<String> usedColumns = new HashSet<>();
QTableMetaData table = QContext.getQInstance().getTable(tableName);
if(table == null)
{
record.addError(new BadInputStatusMessage("Unrecognized table name: " + tableName));
}
if(StringUtils.hasContent(queryFilterJson)) if(StringUtils.hasContent(queryFilterJson))
{ {
try try
{ {
QQueryFilter qQueryFilter = SavedReportToReportMetaDataAdapter.getQQueryFilter(queryFilterJson); ////////////////////////////////////////////////////////////////
int criteriaCount = CollectionUtils.nonNullList(qQueryFilter.getCriteria()).size(); // nothing to validate on filter, other than, we can parse it //
record.setDisplayValue("queryFilterJson", criteriaCount + " Filter" + StringUtils.plural(criteriaCount)); ////////////////////////////////////////////////////////////////
SavedReportToReportMetaDataAdapter.getQQueryFilter(queryFilterJson);
} }
catch(Exception e) catch(IOException e)
{ {
record.setDisplayValue("queryFilterJson", "Invalid Filter..."); record.addError(new BadInputStatusMessage("Unable to parse queryFilterJson: " + e.getMessage()));
} }
} }
boolean hadColumnParseError = false;
if(StringUtils.hasContent(columnsJson)) if(StringUtils.hasContent(columnsJson))
{ {
try try
{ {
/////////////////////////////////////////////////////////////////////////
// make sure we can parse columns, and that we have at least 1 visible //
/////////////////////////////////////////////////////////////////////////
ReportColumns reportColumns = SavedReportToReportMetaDataAdapter.getReportColumns(columnsJson); ReportColumns reportColumns = SavedReportToReportMetaDataAdapter.getReportColumns(columnsJson);
long columnCount = CollectionUtils.nonNullList(reportColumns.getColumns()) for(ReportColumn column : reportColumns.extractVisibleColumns())
.stream().filter(rc -> BooleanUtils.isTrue(rc.getIsVisible()))
.count();
record.setDisplayValue("columnsJson", columnCount + " Column" + StringUtils.plural((int) columnCount));
}
catch(Exception e)
{ {
record.setDisplayValue("columnsJson", "Invalid Columns..."); usedColumns.add(column.getName());
} }
} }
catch(IOException e)
{
record.addError(new BadInputStatusMessage("Unable to parse columnsJson: " + e.getMessage()));
hadColumnParseError = true;
}
}
if(usedColumns.isEmpty() && !hadColumnParseError)
{
record.addError(new BadInputStatusMessage("A Report must contain at least 1 column"));
}
if(StringUtils.hasContent(pivotTableJson)) if(StringUtils.hasContent(pivotTableJson))
{ {
try try
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// make sure we can parse pivot table, and we have ... at least 1 ... row? maybe that's all that's needed //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
PivotTableDefinition pivotTableDefinition = SavedReportToReportMetaDataAdapter.getPivotTableDefinition(pivotTableJson); PivotTableDefinition pivotTableDefinition = SavedReportToReportMetaDataAdapter.getPivotTableDefinition(pivotTableJson);
int rowCount = CollectionUtils.nonNullList(pivotTableDefinition.getRows()).size(); boolean anyRows = false;
int columnCount = CollectionUtils.nonNullList(pivotTableDefinition.getColumns()).size(); boolean missingAnyFieldNamesInRows = false;
int valueCount = CollectionUtils.nonNullList(pivotTableDefinition.getValues()).size(); boolean missingAnyFieldNamesInColumns = false;
record.setDisplayValue("pivotTableJson", rowCount + " Row" + StringUtils.plural(rowCount) + ", " + columnCount + " Column" + StringUtils.plural(columnCount) + ", and " + valueCount + " Value" + StringUtils.plural(valueCount)); boolean missingAnyFieldNamesInValues = false;
boolean missingAnyFunctionsInValues = false;
//////////////////
// look at rows //
//////////////////
for(PivotTableGroupBy row : CollectionUtils.nonNullList(pivotTableDefinition.getRows()))
{
anyRows = true;
if(StringUtils.hasContent(row.getFieldName()))
{
if(!usedColumns.contains(row.getFieldName()) && !hadColumnParseError)
{
record.addError(new BadInputStatusMessage("A pivot table row is using field (" + getFieldLabelElseName(table, row.getFieldName()) + ") which is not an active column on this report."));
}
}
else
{
missingAnyFieldNamesInRows = true;
}
}
if(!anyRows)
{
record.addError(new BadInputStatusMessage("A Pivot Table must contain at least 1 row"));
}
/////////////////////
// look at columns //
/////////////////////
for(PivotTableGroupBy column : CollectionUtils.nonNullList(pivotTableDefinition.getColumns()))
{
if(StringUtils.hasContent(column.getFieldName()))
{
if(!usedColumns.contains(column.getFieldName()) && !hadColumnParseError)
{
record.addError(new BadInputStatusMessage("A pivot table column is using field (" + getFieldLabelElseName(table, column.getFieldName()) + ") which is not an active column on this report."));
}
}
else
{
missingAnyFieldNamesInColumns = true;
}
}
////////////////////
// look at values //
////////////////////
for(PivotTableValue value : CollectionUtils.nonNullList(pivotTableDefinition.getValues()))
{
if(StringUtils.hasContent(value.getFieldName()))
{
if(!usedColumns.contains(value.getFieldName()) && !hadColumnParseError)
{
record.addError(new BadInputStatusMessage("A pivot table value is using field (" + getFieldLabelElseName(table, value.getFieldName()) + ") which is not an active column on this report."));
}
}
else
{
missingAnyFieldNamesInValues = true;
}
if(value.getFunction() == null)
{
missingAnyFunctionsInValues = true;
}
}
////////////////////////////////////////////////
// errors based on missing things found above //
////////////////////////////////////////////////
if(missingAnyFieldNamesInRows)
{
record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table row."));
}
if(missingAnyFieldNamesInColumns)
{
record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table column."));
}
if(missingAnyFieldNamesInValues)
{
record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table value."));
}
if(missingAnyFunctionsInValues)
{
record.addError(new BadInputStatusMessage("Missing function for at least one pivot table value."));
}
}
catch(IOException e)
{
record.addError(new BadInputStatusMessage("Unable to parse pivotTableJson: " + e.getMessage()));
}
}
} }
catch(Exception e) catch(Exception e)
{ {
record.setDisplayValue("pivotTableJson", "Invalid Pivot Table..."); LOG.warn("Error validating a savedReport");
}
} }
} }
return (records);
/*******************************************************************************
**
*******************************************************************************/
private String getFieldLabelElseName(QTableMetaData table, String fieldName)
{
try
{
GenerateReportAction.FieldAndJoinTable fieldAndJoinTable = GenerateReportAction.getFieldAndJoinTable(table, fieldName);
return (fieldAndJoinTable.getLabel(table));
}
catch(Exception e)
{
return (fieldName);
}
} }
} }

View File

@ -30,6 +30,8 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType; import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
@ -147,6 +149,7 @@ public class SavedReportsMetaDataProvider
.withBackendName(backendName) .withBackendName(backendName)
.withPrimaryKeyField("id") .withPrimaryKeyField("id")
.withFieldsFromEntity(SavedReport.class) .withFieldsFromEntity(SavedReport.class)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label", "tableName"))) .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label", "tableName")))
.withSection(new QFieldSection("filtersAndColumns", new QIcon().withName("table_chart"), Tier.T2).withLabel("Filters and Columns").withWidgetName("reportSetupWidget")) .withSection(new QFieldSection("filtersAndColumns", new QIcon().withName("table_chart"), Tier.T2).withLabel("Filters and Columns").withWidgetName("reportSetupWidget"))
.withSection(new QFieldSection("pivotTable", new QIcon().withName("pivot_table_chart"), Tier.T2).withLabel("Pivot Table").withWidgetName("pivotTableSetupWidget")) .withSection(new QFieldSection("pivotTable", new QIcon().withName("pivot_table_chart"), Tier.T2).withLabel("Pivot Table").withWidgetName("pivotTableSetupWidget"))
@ -154,7 +157,12 @@ public class SavedReportsMetaDataProvider
.withSection(new QFieldSection("hidden", new QIcon().withName("text_snippet"), Tier.T2, List.of("inputFieldsJson", "userId")).withIsHidden(true)) .withSection(new QFieldSection("hidden", new QIcon().withName("text_snippet"), Tier.T2, List.of("inputFieldsJson", "userId")).withIsHidden(true))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
table.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(SavedReportTableCustomizer.class)); table.getField("queryFilterJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance());
table.getField("columnsJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance());
table.getField("pivotTableJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance());
table.withCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(SavedReportTableCustomizer.class));
table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(SavedReportTableCustomizer.class));
if(backendDetailEnricher != null) if(backendDetailEnricher != null)
{ {

View File

@ -57,7 +57,6 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
import org.apache.commons.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -122,16 +121,8 @@ public class SavedReportToReportMetaDataAdapter
Set<String> neededJoinTables = new HashSet<>(); Set<String> neededJoinTables = new HashSet<>();
for(ReportColumn column : columnsObject.getColumns()) for(ReportColumn column : columnsObject.extractVisibleColumns())
{ {
////////////////////////////////////////////////////////////////////////////////////////////////////
// if isVisible is missing, we assume it to be true - so only if it isFalse do we skip the column //
////////////////////////////////////////////////////////////////////////////////////////////////////
if(BooleanUtils.isFalse(column.getIsVisible()))
{
continue;
}
//////////////////////////////////////////////////// ////////////////////////////////////////////////////
// figure out the field being named by the column // // figure out the field being named by the column //
//////////////////////////////////////////////////// ////////////////////////////////////////////////////

View File

@ -0,0 +1,166 @@
/*
* 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.model.savedreports;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableGroupBy;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableValue;
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.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for SavedReportJsonFieldDisplayValueFormatter
*******************************************************************************/
class SavedReportJsonFieldDisplayValueFormatterTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws QException
{
QContext.getQInstance().add(new SavedReportsMetaDataProvider().defineSavedReportTable(TestUtils.MEMORY_BACKEND_NAME, null));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPostQuery() throws QException
{
UnsafeFunction<SavedReport, QRecord, QException> customize = savedReport ->
{
QInstance qInstance = QContext.getQInstance();
QTableMetaData table = qInstance.getTable(SavedReport.TABLE_NAME);
QRecord record = savedReport.toQRecord();
for(String fieldName : List.of("queryFilterJson", "columnsJson", "pivotTableJson"))
{
SavedReportJsonFieldDisplayValueFormatter.getInstance().apply(ValueBehaviorApplier.Action.FORMATTING, List.of(record), qInstance, table, table.getField(fieldName));
}
return (record);
};
{
QRecord record = customize.apply(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
.withColumnsJson(JsonUtils.toJson(new ReportColumns()))
.withPivotTableJson(JsonUtils.toJson(new PivotTableDefinition())));
assertEquals("0 Filters", record.getDisplayValue("queryFilterJson"));
assertEquals("0 Columns", record.getDisplayValue("columnsJson"));
assertEquals("0 Rows, 0 Columns, and 0 Values", record.getDisplayValue("pivotTableJson"));
}
{
QRecord record = customize.apply(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
.withColumnsJson(JsonUtils.toJson(new ReportColumns())));
assertEquals("0 Filters", record.getDisplayValue("queryFilterJson"));
assertEquals("0 Columns", record.getDisplayValue("columnsJson"));
assertNull(record.getDisplayValue("pivotTableJson"));
}
{
QRecord record = customize.apply(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()
.withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.IS_NOT_BLANK))))
.withColumnsJson(JsonUtils.toJson(new ReportColumns()
.withColumn(new ReportColumn().withName("birthDate"))))
.withPivotTableJson(JsonUtils.toJson(new PivotTableDefinition()
.withRow(new PivotTableGroupBy())
.withValue(new PivotTableValue())
)));
assertEquals("1 Filter", record.getDisplayValue("queryFilterJson"));
assertEquals("1 Column", record.getDisplayValue("columnsJson"));
assertEquals("1 Row, 0 Columns, and 1 Value", record.getDisplayValue("pivotTableJson"));
}
{
QRecord record = customize.apply(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()
.withCriteria(new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN, 1))
.withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.IS_NOT_BLANK))))
.withColumnsJson(JsonUtils.toJson(new ReportColumns()
.withColumn(new ReportColumn().withName("__check__").withIsVisible(true))
.withColumn(new ReportColumn().withName("id"))
.withColumn(new ReportColumn().withName("firstName").withIsVisible(true))
.withColumn(new ReportColumn().withName("lastName").withIsVisible(false))
.withColumn(new ReportColumn().withName("birthDate"))))
.withPivotTableJson(JsonUtils.toJson(new PivotTableDefinition()
.withRow(new PivotTableGroupBy())
.withRow(new PivotTableGroupBy())
.withColumn(new PivotTableGroupBy())
.withValue(new PivotTableValue())
.withValue(new PivotTableValue())
.withValue(new PivotTableValue())
)));
assertEquals("2 Filters", record.getDisplayValue("queryFilterJson"));
assertEquals("3 Columns", record.getDisplayValue("columnsJson"));
assertEquals("2 Rows, 1 Column, and 3 Values", record.getDisplayValue("pivotTableJson"));
}
{
QRecord record = customize.apply(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson("blah")
.withColumnsJson("<xml?>")
.withPivotTableJson("{]"));
assertEquals("Invalid Filter...", record.getDisplayValue("queryFilterJson"));
assertEquals("Invalid Columns...", record.getDisplayValue("columnsJson"));
assertEquals("Invalid Pivot Table...", record.getDisplayValue("pivotTableJson"));
}
}
}

View File

@ -0,0 +1,228 @@
/*
* 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.model.savedreports;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableFunction;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableGroupBy;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableValue;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Unit test for SavedReportTableCustomizer
*******************************************************************************/
class SavedReportTableCustomizerTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws QException
{
QContext.getQInstance().add(new SavedReportsMetaDataProvider().defineSavedReportTable(TestUtils.MEMORY_BACKEND_NAME, null));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPreInsertAndPreUpdateAreWired() throws QException
{
SavedReport badRecord = new SavedReport()
.withLabel("My Report")
.withTableName("notATable");
/////////////////////////////////////////////////////////////////////
// assertions to apply both to a failed insert and a failed update //
/////////////////////////////////////////////////////////////////////
Consumer<QRecord> asserter = record -> assertThat(record.getErrors())
.hasSizeGreaterThanOrEqualTo(2)
.anyMatch(e -> e.getMessage().contains("Unrecognized table name"))
.anyMatch(e -> e.getMessage().contains("must contain at least 1 column"));
////////////////////////////////////////////////////////////
// go through insert action, to ensure wired-up correctly //
////////////////////////////////////////////////////////////
InsertOutput insertOutput = new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(badRecord));
asserter.accept(insertOutput.getRecords().get(0));
////////////////////////////////
// likewise for update action //
////////////////////////////////
UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(SavedReport.TABLE_NAME).withRecordEntity(badRecord));
asserter.accept(updateOutput.getRecords().get(0));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testParseFails()
{
QRecord record = new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson("...")
.withColumnsJson("x")
.withPivotTableJson("[")
.toQRecord();
new SavedReportTableCustomizer().preValidateRecord(record);
assertThat(record.getErrors())
.hasSize(3)
.anyMatch(e -> e.getMessage().contains("Unable to parse queryFilterJson"))
.anyMatch(e -> e.getMessage().contains("Unable to parse columnsJson"))
.anyMatch(e -> e.getMessage().contains("Unable to parse pivotTableJson"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testNoColumns()
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// given a reportColumns object, serialize it to json, put it in a saved report record, and run the pre-validator //
// then assert we got error saying there were no columns. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Consumer<ReportColumns> asserter = reportColumns ->
{
SavedReport savedReport = new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
.withColumnsJson(JsonUtils.toJson(reportColumns));
QRecord record = savedReport.toQRecord();
new SavedReportTableCustomizer().preValidateRecord(record);
assertThat(record.getErrors())
.hasSize(1)
.anyMatch(e -> e.getMessage().contains("must contain at least 1 column"));
};
asserter.accept(new ReportColumns());
asserter.accept(new ReportColumns().withColumns(null));
asserter.accept(new ReportColumns().withColumns(new ArrayList<>()));
asserter.accept(new ReportColumns().withColumn(new ReportColumn()
.withName("id").withIsVisible(false)));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPivotTables()
{
BiConsumer<PivotTableDefinition, List<String>> asserter = (PivotTableDefinition ptd, List<String> expectedAnyMessageToContain) ->
{
SavedReport savedReport = new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
.withColumnsJson(JsonUtils.toJson(new ReportColumns()
.withColumn("id")
.withColumn("firstName")
.withColumn("lastName")
.withColumn("birthDate")))
.withPivotTableJson(JsonUtils.toJson(ptd));
QRecord record = savedReport.toQRecord();
new SavedReportTableCustomizer().preValidateRecord(record);
assertThat(record.getErrors()).hasSize(expectedAnyMessageToContain.size());
for(String expected : expectedAnyMessageToContain)
{
assertThat(record.getErrors())
.anyMatch(e -> e.getMessage().contains(expected));
}
};
asserter.accept(new PivotTableDefinition(), List.of("must contain at least 1 row"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withRow(new PivotTableGroupBy()),
List.of("Missing field name for at least one pivot table row"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withRow(new PivotTableGroupBy().withFieldName("createDate")),
List.of("row is using field (Create Date) which is not an active column"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withColumn(new PivotTableGroupBy()),
List.of("Missing field name for at least one pivot table column"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withColumn(new PivotTableGroupBy().withFieldName("createDate")),
List.of("column is using field (Create Date) which is not an active column"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withValue(new PivotTableValue().withFunction(PivotTableFunction.SUM)),
List.of("Missing field name for at least one pivot table value"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withValue(new PivotTableValue().withFieldName("createDate").withFunction(PivotTableFunction.SUM)),
List.of("value is using field (Create Date) which is not an active column"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withValue(new PivotTableValue().withFieldName("firstName")),
List.of("Missing function for at least one pivot table value"));
}
}