Updates to allow validations on bulk-edit, with warnings and errors coming back on review & result screens.

This commit is contained in:
2023-05-10 10:09:36 -05:00
parent 88e24a08fc
commit 33555701a4
8 changed files with 853 additions and 49 deletions

View File

@ -52,6 +52,7 @@ public abstract class AbstractPreUpdateCustomizer
{ {
protected UpdateInput updateInput; protected UpdateInput updateInput;
protected List<QRecord> oldRecordList; protected List<QRecord> oldRecordList;
protected boolean isPreview = false;
private Map<Serializable, QRecord> oldRecordMap = null; private Map<Serializable, QRecord> oldRecordMap = null;
@ -123,4 +124,35 @@ public abstract class AbstractPreUpdateCustomizer
return (oldRecordMap); return (oldRecordMap);
} }
/*******************************************************************************
** Getter for isPreview
*******************************************************************************/
public boolean getIsPreview()
{
return (this.isPreview);
}
/*******************************************************************************
** Setter for isPreview
*******************************************************************************/
public void setIsPreview(boolean isPreview)
{
this.isPreview = isPreview;
}
/*******************************************************************************
** Fluent setter for isPreview
*******************************************************************************/
public AbstractPreUpdateCustomizer withIsPreview(boolean isPreview)
{
this.isPreview = isPreview;
return (this);
}
} }

View File

@ -105,30 +105,7 @@ public class UpdateAction
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
Optional<List<QRecord>> oldRecordList = fetchOldRecords(updateInput, updateInterface); Optional<List<QRecord>> oldRecordList = fetchOldRecords(updateInput, updateInterface);
///////////////////////////// performValidations(updateInput, oldRecordList, false);
// run standard validators //
/////////////////////////////
ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), table, updateInput.getRecords());
validatePrimaryKeysAreGiven(updateInput);
if(oldRecordList.isPresent())
{
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get());
}
validateRequiredFields(updateInput);
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-update customizer, if there is one //
///////////////////////////////////////////////////////////////////////////
Optional<AbstractPreUpdateCustomizer> preUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPreUpdateCustomizer.class, table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
if(preUpdateCustomizer.isPresent())
{
preUpdateCustomizer.get().setUpdateInput(updateInput);
oldRecordList.ifPresent(l -> preUpdateCustomizer.get().setOldRecordList(l));
updateInput.setRecords(preUpdateCustomizer.get().apply(updateInput.getRecords()));
}
//////////////////////////////////// ////////////////////////////////////
// have the backend do the update // // have the backend do the update //
@ -191,6 +168,42 @@ public class UpdateAction
/*******************************************************************************
**
*******************************************************************************/
public void performValidations(UpdateInput updateInput, Optional<List<QRecord>> oldRecordList, boolean isPreview) throws QException
{
QTableMetaData table = updateInput.getTable();
/////////////////////////////
// run standard validators //
/////////////////////////////
ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), table, updateInput.getRecords());
validatePrimaryKeysAreGiven(updateInput);
if(oldRecordList.isPresent())
{
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get());
}
validateRequiredFields(updateInput);
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-update customizer, if there is one //
///////////////////////////////////////////////////////////////////////////
Optional<AbstractPreUpdateCustomizer> preUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPreUpdateCustomizer.class, table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
if(preUpdateCustomizer.isPresent())
{
preUpdateCustomizer.get().setUpdateInput(updateInput);
preUpdateCustomizer.get().setIsPreview(isPreview);
oldRecordList.ifPresent(l -> preUpdateCustomizer.get().setOldRecordList(l));
updateInput.setRecords(preUpdateCustomizer.get().apply(updateInput.getRecords()));
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -318,7 +331,7 @@ public class UpdateAction
///////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////
if(record.getValues().containsKey(requiredField.getName())) if(record.getValues().containsKey(requiredField.getName()))
{ {
if(record.getValue(requiredField.getName()) == null || (requiredField.getType().isStringLike() && record.getValueString(requiredField.getName()).trim().equals(""))) if(record.getValue(requiredField.getName()) == null || record.getValueString(requiredField.getName()).trim().equals(""))
{ {
record.addError(new BadInputStatusMessage("Missing value in required field: " + requiredField.getLabel())); record.addError(new BadInputStatusMessage("Missing value in required field: " + requiredField.getLabel()));
} }

View File

@ -66,13 +66,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMeta
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;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteTransformStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditLoadStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertExtractStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertExtractStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaDeleteStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaDeleteStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
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;
@ -761,7 +761,7 @@ public class QInstanceEnricher
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData( QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(
ExtractViaQueryStep.class, ExtractViaQueryStep.class,
BulkEditTransformStep.class, BulkEditTransformStep.class,
LoadViaUpdateStep.class, BulkEditLoadStep.class,
values values
) )
.withName(processName) .withName(processName)

View File

@ -0,0 +1,130 @@
/*
* 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.bulk.edit;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
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.processes.Status;
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.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface;
import com.kingsrook.qqq.backend.core.processes.implementations.general.ProcessSummaryWarningsAndErrorsRollup;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import static com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep.buildInfoSummaryLines;
import static com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep.getProcessSummaryWarningsAndErrorsRollup;
/*******************************************************************************
** Load step for generic table bulk-edit ETL process
*******************************************************************************/
public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummaryProviderInterface
{
public static final String FIELD_ENABLED_FIELDS = "bulkEditEnabledFields";
private ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK);
private List<ProcessSummaryLine> infoSummaries = new ArrayList<>();
private ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = getProcessSummaryWarningsAndErrorsRollup();
private String tableLabel;
/*******************************************************************************
**
*******************************************************************************/
@Override
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
{
ArrayList<ProcessSummaryLineInterface> rs = new ArrayList<>();
String noWarningsSuffix = processSummaryWarningsAndErrorsRollup.countWarnings() == 0 ? "" : " with no warnings";
okSummary.setSingularPastMessage(tableLabel + " record was edited" + noWarningsSuffix + ".");
okSummary.setPluralPastMessage(tableLabel + " records were edited" + noWarningsSuffix + ".");
okSummary.pickMessage(isForResultScreen);
okSummary.addSelfToListIfAnyCount(rs);
processSummaryWarningsAndErrorsRollup.addToList(rs);
rs.addAll(infoSummaries);
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void preRun(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
super.preRun(runBackendStepInput, runBackendStepOutput);
QTableMetaData table = runBackendStepInput.getInstance().getTable(runBackendStepInput.getTableName());
if(table != null)
{
tableLabel = table.getLabel();
}
buildInfoSummaryLines(runBackendStepInput, table, infoSummaries, true);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
QTableMetaData table = runBackendStepInput.getInstance().getTable(runBackendStepInput.getTableName());
super.run(runBackendStepInput, runBackendStepOutput);
for(QRecord record : runBackendStepOutput.getRecords())
{
Serializable recordPrimaryKey = record.getValue(table.getPrimaryKeyField());
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
{
String message = record.getErrors().get(0).getMessage();
processSummaryWarningsAndErrorsRollup.addError(message, recordPrimaryKey);
}
else if(CollectionUtils.nullSafeHasContents(record.getWarnings()))
{
String message = record.getWarnings().get(0).getMessage();
processSummaryWarningsAndErrorsRollup.addWarning(message, recordPrimaryKey);
}
else
{
okSummary.incrementCountAndAddPrimaryKey(recordPrimaryKey);
}
}
}
}

View File

@ -24,7 +24,11 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -33,11 +37,14 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; 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.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.processes.Status; import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
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.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.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.general.ProcessSummaryWarningsAndErrorsRollup;
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 com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -52,6 +59,8 @@ public class BulkEditTransformStep extends AbstractTransformStep
private ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK); private ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK);
private List<ProcessSummaryLine> infoSummaries = new ArrayList<>(); private List<ProcessSummaryLine> infoSummaries = new ArrayList<>();
private ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = getProcessSummaryWarningsAndErrorsRollup();
private QTableMetaData table; private QTableMetaData table;
private String tableLabel; private String tableLabel;
private String[] enabledFields; private String[] enabledFields;
@ -62,6 +71,36 @@ public class BulkEditTransformStep extends AbstractTransformStep
/*******************************************************************************
** used by Load step too
*******************************************************************************/
static ProcessSummaryWarningsAndErrorsRollup getProcessSummaryWarningsAndErrorsRollup()
{
return new ProcessSummaryWarningsAndErrorsRollup()
.withErrorTemplate(new ProcessSummaryLine(Status.ERROR)
.withSingularFutureMessage("record has an error: ")
.withPluralFutureMessage("records have an error: ")
.withSingularPastMessage("record had an error: ")
.withPluralPastMessage("records had an error: "))
.withWarningTemplate(new ProcessSummaryLine(Status.WARNING)
.withSingularFutureMessage("record will be edited, but has a warning: ")
.withPluralFutureMessage("records will be edited, but have a warning: ")
.withSingularPastMessage("record was edited, but had a warning: ")
.withPluralPastMessage("records were edited, but had a warning: "))
.withOtherErrorsSummary(new ProcessSummaryLine(Status.ERROR)
.withSingularFutureMessage("record has an other error.")
.withPluralFutureMessage("records have other errors.")
.withSingularPastMessage("record had an other error.")
.withPluralPastMessage("records had other errors."))
.withOtherWarningsSummary(new ProcessSummaryLine(Status.WARNING)
.withSingularFutureMessage("record will be edited, but has an other warning.")
.withPluralFutureMessage("records will be edited, but have other warnings.")
.withSingularPastMessage("record was edited, but had other warnings.")
.withPluralPastMessage("records were edited, but had other warnings."));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -77,14 +116,14 @@ public class BulkEditTransformStep extends AbstractTransformStep
tableLabel = table.getLabel(); tableLabel = table.getLabel();
} }
String enabledFieldsString = runBackendStepInput.getValueString(FIELD_ENABLED_FIELDS);
enabledFields = enabledFieldsString.split(",");
isValidateStep = runBackendStepInput.getStepName().equals(StreamedETLWithFrontendProcess.STEP_NAME_VALIDATE); isValidateStep = runBackendStepInput.getStepName().equals(StreamedETLWithFrontendProcess.STEP_NAME_VALIDATE);
isExecuteStep = runBackendStepInput.getStepName().equals(StreamedETLWithFrontendProcess.STEP_NAME_EXECUTE); isExecuteStep = runBackendStepInput.getStepName().equals(StreamedETLWithFrontendProcess.STEP_NAME_EXECUTE);
haveRecordCount = runBackendStepInput.getValue(StreamedETLWithFrontendProcess.FIELD_RECORD_COUNT) != null; haveRecordCount = runBackendStepInput.getValue(StreamedETLWithFrontendProcess.FIELD_RECORD_COUNT) != null;
buildInfoSummaryLines(runBackendStepInput, enabledFields); String enabledFieldsString = runBackendStepInput.getValueString(FIELD_ENABLED_FIELDS);
enabledFields = enabledFieldsString.split(",");
buildInfoSummaryLines(runBackendStepInput, table, infoSummaries, isExecuteStep);
} }
@ -129,22 +168,63 @@ public class BulkEditTransformStep extends AbstractTransformStep
outputRecords.add(recordToUpdate); outputRecords.add(recordToUpdate);
setUpdatedFieldsInRecord(runBackendStepInput, enabledFields, recordToUpdate); setUpdatedFieldsInRecord(runBackendStepInput, enabledFields, recordToUpdate);
} }
okSummary.incrementCount(runBackendStepInput.getRecords().size());
} }
else else
{ {
//////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
// put the value in all the records (note, this is just for display on the review screen, // // Build records-to-update for passing into the validation method of the Update action //
// and/or if we wanted to do some validation - this is NOT what will be store, as the // /////////////////////////////////////////////////////////////////////////////////////////
// Update action only wants fields that are being changed. // List<QRecord> recordsForValidation = new ArrayList<>();
//////////////////////////////////////////////////////////////////////////////////////////// Map<Serializable, QRecord> pkeyToFullRecordMap = new HashMap<>();
for(QRecord record : runBackendStepInput.getRecords()) for(QRecord record : runBackendStepInput.getRecords())
{ {
outputRecords.add(record); QRecord recordToUpdate = new QRecord();
recordToUpdate.setValue(table.getPrimaryKeyField(), record.getValue(table.getPrimaryKeyField()));
setUpdatedFieldsInRecord(runBackendStepInput, enabledFields, recordToUpdate);
recordsForValidation.add(recordToUpdate);
/////////////////////////////////////////////////////////////
// put the full record (with updated values) in the output //
/////////////////////////////////////////////////////////////
setUpdatedFieldsInRecord(runBackendStepInput, enabledFields, record); setUpdatedFieldsInRecord(runBackendStepInput, enabledFields, record);
pkeyToFullRecordMap.put(record.getValue(table.getPrimaryKeyField()), record);
}
///////////////////////////////////////////////////////////////////////
// run the validation - critically - in preview mode (boolean param) //
///////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(table.getName());
updateInput.setRecords(recordsForValidation);
new UpdateAction().performValidations(updateInput, Optional.of(runBackendStepInput.getRecords()), true);
/////////////////////////////////////////////////////////////
// look at the update input to build process summary lines //
/////////////////////////////////////////////////////////////
for(QRecord record : updateInput.getRecords())
{
Serializable recordPrimaryKey = record.getValue(table.getPrimaryKeyField());
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
{
String message = record.getErrors().get(0).getMessage();
processSummaryWarningsAndErrorsRollup.addError(message, recordPrimaryKey);
}
else if(CollectionUtils.nullSafeHasContents(record.getWarnings()))
{
String message = record.getWarnings().get(0).getMessage();
processSummaryWarningsAndErrorsRollup.addWarning(message, recordPrimaryKey);
outputRecords.add(pkeyToFullRecordMap.get(recordPrimaryKey));
}
else
{
okSummary.incrementCountAndAddPrimaryKey(recordPrimaryKey);
outputRecords.add(pkeyToFullRecordMap.get(recordPrimaryKey));
}
} }
} }
runBackendStepOutput.setRecords(outputRecords); runBackendStepOutput.setRecords(outputRecords);
okSummary.incrementCount(runBackendStepInput.getRecords().size());
} }
@ -152,9 +232,11 @@ public class BulkEditTransformStep extends AbstractTransformStep
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private void buildInfoSummaryLines(RunBackendStepInput runBackendStepInput, String[] enabledFields) static void buildInfoSummaryLines(RunBackendStepInput runBackendStepInput, QTableMetaData table, List<ProcessSummaryLine> infoSummaries, boolean isExecuteStep)
{ {
QValueFormatter qValueFormatter = new QValueFormatter(); String enabledFieldsString = runBackendStepInput.getValueString(FIELD_ENABLED_FIELDS);
String[] enabledFields = enabledFieldsString.split(",");
for(String fieldName : enabledFields) for(String fieldName : enabledFields)
{ {
QFieldMetaData field = table.getField(fieldName); QFieldMetaData field = table.getField(fieldName);
@ -168,7 +250,7 @@ public class BulkEditTransformStep extends AbstractTransformStep
String verb = isExecuteStep ? "was" : "will be"; String verb = isExecuteStep ? "was" : "will be";
if(StringUtils.hasContent(ValueUtils.getValueAsString(value))) if(StringUtils.hasContent(ValueUtils.getValueAsString(value)))
{ {
String formattedValue = qValueFormatter.formatValue(field, value); String formattedValue = QValueFormatter.formatValue(field, value);
if(field.getPossibleValueSourceName() != null) if(field.getPossibleValueSourceName() != null)
{ {
@ -211,15 +293,19 @@ public class BulkEditTransformStep extends AbstractTransformStep
@Override @Override
public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen) public ArrayList<ProcessSummaryLineInterface> getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
{ {
okSummary.setSingularFutureMessage(tableLabel + " record will be edited.");
okSummary.setPluralFutureMessage(tableLabel + " records will be edited.");
okSummary.setSingularPastMessage(tableLabel + " record was edited.");
okSummary.setPluralPastMessage(tableLabel + " records were edited.");
okSummary.pickMessage(isForResultScreen);
ArrayList<ProcessSummaryLineInterface> rs = new ArrayList<>(); ArrayList<ProcessSummaryLineInterface> rs = new ArrayList<>();
rs.add(okSummary);
String noWarningsSuffix = processSummaryWarningsAndErrorsRollup.countWarnings() == 0 ? "" : " with no warnings";
okSummary.setSingularFutureMessage(tableLabel + " record will be edited" + noWarningsSuffix + ".");
okSummary.setPluralFutureMessage(tableLabel + " records will be edited" + noWarningsSuffix + ".");
okSummary.pickMessage(isForResultScreen);
okSummary.addSelfToListIfAnyCount(rs);
processSummaryWarningsAndErrorsRollup.addToList(rs);
rs.addAll(infoSummaries); rs.addAll(infoSummaries);
return (rs); return (rs);
} }
} }

View File

@ -0,0 +1,309 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.general;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessSummaryWarningsAndErrorsRollup
{
private Map<String, ProcessSummaryLine> errorSummaries = new HashMap<>();
private Map<String, ProcessSummaryLine> warningSummaries = new HashMap<>();
private ProcessSummaryLine otherErrorsSummary;
private ProcessSummaryLine otherWarningsSummary;
private ProcessSummaryLine errorTemplate;
private ProcessSummaryLine warningTemplate;
/*******************************************************************************
**
*******************************************************************************/
public void addToList(ArrayList<ProcessSummaryLineInterface> list)
{
addProcessSummaryLinesFromMap(list, errorSummaries);
if(otherErrorsSummary != null)
{
otherErrorsSummary.addSelfToListIfAnyCount(list);
}
addProcessSummaryLinesFromMap(list, warningSummaries);
if(otherWarningsSummary != null)
{
otherWarningsSummary.addSelfToListIfAnyCount(list);
}
}
/*******************************************************************************
**
*******************************************************************************/
public void addError(String message, Serializable primaryKey)
{
add(Status.ERROR, errorSummaries, errorTemplate, message, primaryKey);
}
/*******************************************************************************
**
*******************************************************************************/
public void addWarning(String message, Serializable primaryKey)
{
add(Status.WARNING, warningSummaries, warningTemplate, message, primaryKey);
}
/*******************************************************************************
**
*******************************************************************************/
public int countWarnings()
{
int sum = 0;
for(ProcessSummaryLine processSummaryLine : warningSummaries.values())
{
sum += Objects.requireNonNullElse(processSummaryLine.getCount(), 0);
}
if(otherWarningsSummary != null)
{
sum += Objects.requireNonNullElse(otherWarningsSummary.getCount(), 0);
}
return (sum);
}
/*******************************************************************************
**
*******************************************************************************/
public int countErrors()
{
int sum = 0;
for(ProcessSummaryLine processSummaryLine : errorSummaries.values())
{
sum += Objects.requireNonNullElse(processSummaryLine.getCount(), 0);
}
if(otherErrorsSummary != null)
{
sum += Objects.requireNonNullElse(otherErrorsSummary.getCount(), 0);
}
return (sum);
}
/*******************************************************************************
**
*******************************************************************************/
private void add(Status status, Map<String, ProcessSummaryLine> summaryLineMap, ProcessSummaryLine templateLine, String message, Serializable primaryKey)
{
ProcessSummaryLine processSummaryLine = summaryLineMap.get(message);
if(processSummaryLine == null)
{
if(summaryLineMap.size() < 50)
{
processSummaryLine = new ProcessSummaryLine(status)
.withMessageSuffix(message)
.withSingularFutureMessage(templateLine.getSingularFutureMessage())
.withPluralFutureMessage(templateLine.getPluralFutureMessage())
.withSingularPastMessage(templateLine.getSingularPastMessage())
.withPluralPastMessage(templateLine.getPluralPastMessage());
summaryLineMap.put(message, processSummaryLine);
}
else
{
if(status.equals(Status.ERROR))
{
if(otherErrorsSummary == null)
{
otherErrorsSummary = new ProcessSummaryLine(Status.ERROR).withMessageSuffix("records had an other error.");
}
processSummaryLine = otherErrorsSummary;
}
else
{
if(otherWarningsSummary == null)
{
otherWarningsSummary = new ProcessSummaryLine(Status.WARNING).withMessageSuffix("records had an other warning.");
}
processSummaryLine = otherWarningsSummary;
}
}
}
processSummaryLine.incrementCountAndAddPrimaryKey(primaryKey);
}
/*******************************************************************************
** sort the process summary lines by count desc
*******************************************************************************/
private static void addProcessSummaryLinesFromMap(ArrayList<ProcessSummaryLineInterface> rs, Map<String, ProcessSummaryLine> summaryMap)
{
summaryMap.values().stream()
.sorted(Comparator.comparing((ProcessSummaryLine psl) -> Objects.requireNonNullElse(psl.getCount(), 0)).reversed()
.thenComparing((ProcessSummaryLine psl) -> Objects.requireNonNullElse(psl.getMessage(), ""))
.thenComparing((ProcessSummaryLine psl) -> Objects.requireNonNullElse(psl.getMessageSuffix(), ""))
)
.forEach(psl -> psl.addSelfToListIfAnyCount(rs));
}
/*******************************************************************************
** Getter for otherErrorsSummary
*******************************************************************************/
public ProcessSummaryLine getOtherErrorsSummary()
{
return (this.otherErrorsSummary);
}
/*******************************************************************************
** Setter for otherErrorsSummary
*******************************************************************************/
public void setOtherErrorsSummary(ProcessSummaryLine otherErrorsSummary)
{
this.otherErrorsSummary = otherErrorsSummary;
}
/*******************************************************************************
** Fluent setter for otherErrorsSummary
*******************************************************************************/
public ProcessSummaryWarningsAndErrorsRollup withOtherErrorsSummary(ProcessSummaryLine otherErrorsSummary)
{
this.otherErrorsSummary = otherErrorsSummary;
return (this);
}
/*******************************************************************************
** Getter for otherWarningsSummary
*******************************************************************************/
public ProcessSummaryLine getOtherWarningsSummary()
{
return (this.otherWarningsSummary);
}
/*******************************************************************************
** Setter for otherWarningsSummary
*******************************************************************************/
public void setOtherWarningsSummary(ProcessSummaryLine otherWarningsSummary)
{
this.otherWarningsSummary = otherWarningsSummary;
}
/*******************************************************************************
** Fluent setter for otherWarningsSummary
*******************************************************************************/
public ProcessSummaryWarningsAndErrorsRollup withOtherWarningsSummary(ProcessSummaryLine otherWarningsSummary)
{
this.otherWarningsSummary = otherWarningsSummary;
return (this);
}
/*******************************************************************************
** Getter for errorTemplate
*******************************************************************************/
public ProcessSummaryLine getErrorTemplate()
{
return (this.errorTemplate);
}
/*******************************************************************************
** Setter for errorTemplate
*******************************************************************************/
public void setErrorTemplate(ProcessSummaryLine errorTemplate)
{
this.errorTemplate = errorTemplate;
}
/*******************************************************************************
** Fluent setter for errorTemplate
*******************************************************************************/
public ProcessSummaryWarningsAndErrorsRollup withErrorTemplate(ProcessSummaryLine errorTemplate)
{
this.errorTemplate = errorTemplate;
return (this);
}
/*******************************************************************************
** Getter for warningTemplate
*******************************************************************************/
public ProcessSummaryLine getWarningTemplate()
{
return (this.warningTemplate);
}
/*******************************************************************************
** Setter for warningTemplate
*******************************************************************************/
public void setWarningTemplate(ProcessSummaryLine warningTemplate)
{
this.warningTemplate = warningTemplate;
}
/*******************************************************************************
** Fluent setter for warningTemplate
*******************************************************************************/
public ProcessSummaryWarningsAndErrorsRollup withWarningTemplate(ProcessSummaryLine warningTemplate)
{
this.warningTemplate = warningTemplate;
return (this);
}
}

View File

@ -22,9 +22,12 @@
package com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit; package com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -37,6 +40,10 @@ 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.actions.tables.query.QQueryFilter;
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.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
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.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.utils.TestUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils;
@ -143,4 +150,231 @@ class BulkEditTest extends BaseTest
assertEquals("james.maes@kingsrook.com", records.get(2).getValue("email")); assertEquals("james.maes@kingsrook.com", records.get(2).getValue("email"));
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testWarningsAndErrors() throws QException
{
//////////////////////////////
// insert some test records //
//////////////////////////////
QInstance qInstance = QContext.getQInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
List<QRecord> personsToLoad = new ArrayList<>();
for(int i = 0; i < 100; i++)
{
personsToLoad.add(new QRecord().withValue("id", i).withValue("firstName", "Darin" + i));
}
TestUtils.insertRecords(table, personsToLoad);
table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(PersonPreUpdateReusedMessages.class));
//////////////////////////////////
// set up the run-process input //
//////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(TestUtils.TABLE_NAME_PERSON_MEMORY + ".bulkEdit");
runProcessInput.addValue(StreamedETLWithFrontendProcess.FIELD_DEFAULT_QUERY_FILTER,
new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, 100)));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
String processUUID = runProcessOutput.getProcessUUID();
runProcessInput.addValue(BulkEditTransformStep.FIELD_ENABLED_FIELDS, "firstName");
runProcessInput.addValue("firstName", "Johnny");
runProcessInput.setProcessUUID(processUUID);
runProcessInput.setStartAfterStep("edit");
runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertThat(runProcessOutput.getRecords()).hasSize(0);
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("review");
runProcessInput.addValue(StreamedETLWithFrontendProcess.FIELD_DO_FULL_VALIDATION, true);
runProcessInput.setStartAfterStep("review");
runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertThat(runProcessOutput.getRecords()).hasSize(20);
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("review");
assertThat(runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_VALIDATION_SUMMARY)).isNotNull().isInstanceOf(List.class);
runProcessInput.setStartAfterStep("review");
runProcessOutput = new RunProcessAction().execute(runProcessInput);
assertThat(runProcessOutput.getRecords()).hasSize(20);
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("result");
assertThat(runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY)).isNotNull().isInstanceOf(List.class);
assertThat(runProcessOutput.getException()).isEmpty();
@SuppressWarnings("unchecked")
List<ProcessSummaryLine> processSummaryLines = (List<ProcessSummaryLine>) runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY);
assertThat(processSummaryLines).hasSize(4);
assertThat(processSummaryLines.get(0))
.hasFieldOrPropertyWithValue("status", Status.OK)
.hasFieldOrPropertyWithValue("count", 10)
.matches(psl -> psl.getMessage().contains("edited with no warnings"), "expected message");
assertThat(processSummaryLines.get(1))
.hasFieldOrPropertyWithValue("status", Status.ERROR)
.hasFieldOrPropertyWithValue("count", 60)
.matches(psl -> psl.getMessage().contains("Id less than 60 is error"), "expected message");
assertThat(processSummaryLines.get(2))
.hasFieldOrPropertyWithValue("status", Status.WARNING)
.hasFieldOrPropertyWithValue("count", 30)
.matches(psl -> psl.getMessage().contains("Id less than 90 is warning"), "expected message");
List<ProcessSummaryLine> infoLines = processSummaryLines.stream().filter(psl -> psl.getStatus().equals(Status.INFO)).collect(Collectors.toList());
assertThat(infoLines).hasSize(1);
assertThat(infoLines.stream().map(ProcessSummaryLine::getMessage)).anyMatch(m -> m.matches("(?s).*First Name.*Johnny.*"));
}
/*******************************************************************************
**
*******************************************************************************/
public static class PersonPreUpdateReusedMessages extends AbstractPreUpdateCustomizer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> apply(List<QRecord> records) throws QException
{
for(QRecord record : records)
{
Integer id = record.getValueInteger("id");
if(id < 60)
{
record.addError(new BadInputStatusMessage("Id less than 60 is error."));
}
else if(id < 90)
{
record.addWarning(new QWarningMessage("Id less than 90 is warning."));
}
}
return (records);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUniqueWarningsAndErrors() throws QException
{
//////////////////////////////
// insert some test records //
//////////////////////////////
QInstance qInstance = QContext.getQInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
List<QRecord> personsToLoad = new ArrayList<>();
for(int i = 0; i < 100; i++)
{
personsToLoad.add(new QRecord().withValue("id", i).withValue("firstName", "Darin" + i));
}
TestUtils.insertRecords(table, personsToLoad);
table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(PersonPreUpdateUniqueMessages.class));
//////////////////////////////////
// set up the run-process input //
//////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(TestUtils.TABLE_NAME_PERSON_MEMORY + ".bulkEdit");
runProcessInput.addValue(StreamedETLWithFrontendProcess.FIELD_DEFAULT_QUERY_FILTER,
new QQueryFilter().withCriteria(new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, 100)));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
String processUUID = runProcessOutput.getProcessUUID();
runProcessInput.addValue(BulkEditTransformStep.FIELD_ENABLED_FIELDS, "firstName");
runProcessInput.addValue("firstName", "Johnny");
runProcessInput.setProcessUUID(processUUID);
runProcessInput.setStartAfterStep("edit");
runProcessOutput = new RunProcessAction().execute(runProcessInput);
runProcessInput.addValue(StreamedETLWithFrontendProcess.FIELD_DO_FULL_VALIDATION, true);
runProcessInput.setStartAfterStep("review");
runProcessOutput = new RunProcessAction().execute(runProcessInput);
runProcessInput.setStartAfterStep("review");
runProcessOutput = new RunProcessAction().execute(runProcessInput);
@SuppressWarnings("unchecked")
List<ProcessSummaryLine> processSummaryLines = (List<ProcessSummaryLine>) runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY);
assertThat(processSummaryLines).hasSize(1 + 50 + 1 + 30 + 1);
int index = 0;
assertThat(processSummaryLines.get(index++))
.hasFieldOrPropertyWithValue("status", Status.OK)
.hasFieldOrPropertyWithValue("count", 10)
.matches(psl -> psl.getMessage().contains("edited with no warnings"), "expected message");
for(int i = 0; i < 50; i++)
{
assertThat(processSummaryLines.get(index++))
.hasFieldOrPropertyWithValue("status", Status.ERROR)
.hasFieldOrPropertyWithValue("count", 1)
.matches(psl -> psl.getMessage().contains("less than 60 is error"), "expected message");
}
assertThat(processSummaryLines.get(index++))
.hasFieldOrPropertyWithValue("status", Status.ERROR)
.hasFieldOrPropertyWithValue("count", 10)
.matches(psl -> psl.getMessage().contains("had other errors"), "expected message");
for(int i = 0; i < 30; i++)
{
assertThat(processSummaryLines.get(index++))
.hasFieldOrPropertyWithValue("status", Status.WARNING)
.hasFieldOrPropertyWithValue("count", 1)
.matches(psl -> psl.getMessage().contains("less than 90 is warning"), "expected message");
}
assertThat(processSummaryLines.get(index++))
.hasFieldOrPropertyWithValue("status", Status.INFO)
.matches(psl -> psl.getMessage().matches("(?s).*First Name.*Johnny.*"), "expected message");
}
/*******************************************************************************
**
*******************************************************************************/
public static class PersonPreUpdateUniqueMessages extends AbstractPreUpdateCustomizer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> apply(List<QRecord> records) throws QException
{
for(QRecord record : records)
{
Integer id = record.getValueInteger("id");
if(id < 60)
{
record.addError(new BadInputStatusMessage("Id [" + id + "] less than 60 is error."));
}
else if(id < 90)
{
record.addWarning(new QWarningMessage("Id [" + id + "] less than 90 is warning."));
}
}
return (records);
}
}
} }

View File

@ -289,7 +289,7 @@ public class TestUtils
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@Deprecated @Deprecated(since = "better to call the one without qInstance param")
public static void insertRecords(QInstance qInstance, QTableMetaData table, List<QRecord> records) throws QException public static void insertRecords(QInstance qInstance, QTableMetaData table, List<QRecord> records) throws QException
{ {
insertRecords(table, records); insertRecords(table, records);