mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
Add RunReportForRecordProcess; 1st version of AbstractProcessMetaDataBuilder
This commit is contained in:
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.dashboard;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
@ -40,6 +41,8 @@ public class RenderWidgetAction
|
||||
*******************************************************************************/
|
||||
public RenderWidgetOutput execute(RenderWidgetInput input) throws QException
|
||||
{
|
||||
ActionHelper.validateSession(input);
|
||||
|
||||
AbstractWidgetRenderer widgetRenderer = QCodeLoader.getAdHoc(AbstractWidgetRenderer.class, input.getWidgetMetaData().getCodeReference());
|
||||
return (widgetRenderer.render(input));
|
||||
}
|
||||
|
@ -31,13 +31,16 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
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.code.QCodeType;
|
||||
@ -50,6 +53,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
@ -118,6 +123,7 @@ public class QInstanceValidator
|
||||
validateAutomationProviders(qInstance);
|
||||
validateTables(qInstance);
|
||||
validateProcesses(qInstance);
|
||||
validateReports(qInstance);
|
||||
validateApps(qInstance);
|
||||
validatePossibleValueSources(qInstance);
|
||||
validateQueuesAndProviders(qInstance);
|
||||
@ -186,6 +192,8 @@ public class QInstanceValidator
|
||||
qInstance.getBackends().forEach((backendName, backend) ->
|
||||
{
|
||||
assertCondition(Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + ".");
|
||||
|
||||
backend.performValidation(this);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -258,6 +266,8 @@ public class QInstanceValidator
|
||||
//////////////////////////////////////////
|
||||
Set<String> fieldNamesInSections = new HashSet<>();
|
||||
QFieldSection tier1Section = null;
|
||||
Set<String> usedSectionNames = new HashSet<>();
|
||||
Set<String> usedSectionLabels = new HashSet<>();
|
||||
if(table.getSections() != null)
|
||||
{
|
||||
for(QFieldSection section : table.getSections())
|
||||
@ -268,6 +278,12 @@ public class QInstanceValidator
|
||||
assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1");
|
||||
tier1Section = section;
|
||||
}
|
||||
|
||||
assertCondition(!usedSectionNames.contains(section.getName()), "Table " + tableName + " has more than 1 section named " + section.getName());
|
||||
usedSectionNames.add(section.getName());
|
||||
|
||||
assertCondition(!usedSectionLabels.contains(section.getLabel()), "Table " + tableName + " has more than 1 section labeled " + section.getLabel());
|
||||
usedSectionLabels.add(section.getLabel());
|
||||
}
|
||||
}
|
||||
|
||||
@ -716,6 +732,133 @@ public class QInstanceValidator
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateReports(QInstance qInstance)
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(qInstance.getReports()))
|
||||
{
|
||||
qInstance.getReports().forEach((reportName, report) ->
|
||||
{
|
||||
assertCondition(Objects.equals(reportName, report.getName()), "Inconsistent naming for report: " + reportName + "/" + report.getName() + ".");
|
||||
validateAppChildHasValidParentAppName(qInstance, report);
|
||||
|
||||
////////////////////////////////////////
|
||||
// validate dataSources in the report //
|
||||
////////////////////////////////////////
|
||||
Set<String> usedDataSourceNames = new HashSet<>();
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(report.getDataSources()), "At least 1 data source must be defined in report " + reportName + "."))
|
||||
{
|
||||
int index = 0;
|
||||
for(QReportDataSource dataSource : report.getDataSources())
|
||||
{
|
||||
assertCondition(StringUtils.hasContent(dataSource.getName()), "Missing name for a dataSource at index " + index + " in report " + reportName);
|
||||
index++;
|
||||
|
||||
assertCondition(!usedDataSourceNames.contains(dataSource.getName()), "More than one dataSource with name " + dataSource.getName() + " in report " + reportName);
|
||||
usedDataSourceNames.add(dataSource.getName());
|
||||
|
||||
String dataSourceErrorPrefix = "Report " + reportName + " data source " + dataSource.getName() + " ";
|
||||
|
||||
if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
{
|
||||
assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (exactly 1 is required).");
|
||||
if(assertCondition(qInstance.getTable(dataSource.getSourceTable()) != null, dataSourceErrorPrefix + "source table " + dataSource.getSourceTable() + " is not a table in this instance."))
|
||||
{
|
||||
if(dataSource.getQueryFilter() != null)
|
||||
{
|
||||
validateQueryFilter("In " + dataSourceErrorPrefix + "query filter - ", qInstance.getTable(dataSource.getSourceTable()), dataSource.getQueryFilter());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(dataSource.getStaticDataSupplier() != null)
|
||||
{
|
||||
validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getStaticDataSupplier(), Supplier.class);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.add(dataSourceErrorPrefix + "does not have a sourceTable or a staticDataSupplier (exactly 1 is required).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// validate dataSources in the report //
|
||||
////////////////////////////////////////
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(report.getViews()), "At least 1 view must be defined in report " + reportName + "."))
|
||||
{
|
||||
int index = 0;
|
||||
Set<String> usedViewNames = new HashSet<>();
|
||||
for(QReportView view : report.getViews())
|
||||
{
|
||||
assertCondition(StringUtils.hasContent(view.getName()), "Missing name for a view at index " + index + " in report " + reportName);
|
||||
index++;
|
||||
|
||||
assertCondition(!usedViewNames.contains(view.getName()), "More than one view with name " + view.getName() + " in report " + reportName);
|
||||
usedViewNames.add(view.getName());
|
||||
|
||||
String viewErrorPrefix = "Report " + reportName + " view " + view.getName() + " ";
|
||||
assertCondition(view.getType() != null, viewErrorPrefix + " is missing its type.");
|
||||
if(assertCondition(StringUtils.hasContent(view.getDataSourceName()), viewErrorPrefix + " is missing a dataSourceName"))
|
||||
{
|
||||
assertCondition(usedDataSourceNames.contains(view.getDataSourceName()), viewErrorPrefix + " has an unrecognized dataSourceName: " + view.getDataSourceName());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(view.getVarianceDataSourceName()))
|
||||
{
|
||||
assertCondition(usedDataSourceNames.contains(view.getVarianceDataSourceName()), viewErrorPrefix + " has an unrecognized varianceDataSourceName: " + view.getVarianceDataSourceName());
|
||||
}
|
||||
|
||||
// actually, this is okay if there's a customizer, so...
|
||||
assertCondition(CollectionUtils.nullSafeHasContents(view.getColumns()), viewErrorPrefix + " does not have any columns.");
|
||||
|
||||
// todo - all these too...
|
||||
// view.getPivotFields();
|
||||
// view.getViewCustomizer(); // validate code ref
|
||||
// view.getRecordTransformStep(); // validate code ref
|
||||
// view.getOrderByFields(); // make sure valid field names?
|
||||
// view.getIncludePivotSubTotals(); // only for pivot type
|
||||
// view.getTitleFormat(); view.getTitleFields(); // validate these match?
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateQueryFilter(String context, QTableMetaData table, QQueryFilter queryFilter)
|
||||
{
|
||||
for(QFilterCriteria criterion : CollectionUtils.nonNullList(queryFilter.getCriteria()))
|
||||
{
|
||||
if(assertCondition(StringUtils.hasContent(criterion.getFieldName()), context + "Missing fieldName for a criteria"))
|
||||
{
|
||||
assertNoException(() -> table.getField(criterion.getFieldName()), context + "Criteria fieldName " + criterion.getFieldName() + " is not a field in this table.");
|
||||
}
|
||||
assertCondition(criterion.getOperator() != null, context + "Missing operator for a criteria on fieldName " + criterion.getFieldName());
|
||||
assertCondition(criterion.getValues() != null, context + "Missing values for a criteria on fieldName " + criterion.getFieldName()); // todo - what about ops w/ no value (BLANK)
|
||||
}
|
||||
|
||||
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys()))
|
||||
{
|
||||
if(assertCondition(StringUtils.hasContent(orderBy.getFieldName()), context + "Missing fieldName for an orderBy"))
|
||||
{
|
||||
assertNoException(() -> table.getField(orderBy.getFieldName()), context + "OrderBy fieldName " + orderBy.getFieldName() + " is not a field in this table.");
|
||||
}
|
||||
}
|
||||
|
||||
for(QQueryFilter subFilter : CollectionUtils.nonNullList(queryFilter.getSubFilters()))
|
||||
{
|
||||
validateQueryFilter(context, table, subFilter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -978,7 +1121,7 @@ public class QInstanceValidator
|
||||
** But if it's false, add the provided message to the list of errors (and return false,
|
||||
** e.g., in case you need to stop evaluating rules to avoid exceptions).
|
||||
*******************************************************************************/
|
||||
private boolean assertCondition(boolean condition, String message)
|
||||
public boolean assertCondition(boolean condition, String message)
|
||||
{
|
||||
if(!condition)
|
||||
{
|
||||
@ -1035,4 +1178,15 @@ public class QInstanceValidator
|
||||
LOG.info("Validation warning: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for errors
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<String> getErrors()
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.serialization.QBackendMetaDataDeserializer;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
@ -321,4 +322,15 @@ public class QBackendMetaData
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void performValidation(QInstanceValidator qInstanceValidator)
|
||||
{
|
||||
////////////////////////
|
||||
// noop in base class //
|
||||
////////////////////////
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class AbstractProcessMetaDataBuilder
|
||||
{
|
||||
protected QProcessMetaData processMetaData;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public AbstractProcessMetaDataBuilder(QProcessMetaData processMetaData)
|
||||
{
|
||||
this.processMetaData = processMetaData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for processMetaData
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QProcessMetaData getProcessMetaData()
|
||||
{
|
||||
return processMetaData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected void setInputFieldDefaultValue(String fieldName, Serializable value)
|
||||
{
|
||||
processMetaData.getInputFields().stream()
|
||||
.filter(f -> f.getName().equals(fieldName)).findFirst()
|
||||
.ifPresent(f -> f.setDefaultValue(value));
|
||||
}
|
||||
}
|
@ -90,10 +90,17 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
|
||||
updateRecordsWithDisplayValuesAndPossibleValues(runBackendStepInput, loadedRecordList);
|
||||
runBackendStepOutput.setRecords(loadedRecordList);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// get the process summary from the ... transform step? the load step? each knows some... todo? //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, transformStep.doGetProcessSummary(runBackendStepOutput, true));
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// get the process summary from the load step, if it's a summary-provider -- else, use the transform step (which is always a provider) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(loadStep instanceof ProcessSummaryProviderInterface provider)
|
||||
{
|
||||
runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, provider.doGetProcessSummary(runBackendStepOutput, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
runBackendStepOutput.addValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY, transformStep.doGetProcessSummary(runBackendStepOutput, true));
|
||||
}
|
||||
|
||||
transformStep.postRun(runBackendStepInput, runBackendStepOutput);
|
||||
loadStep.postRun(runBackendStepInput, runBackendStepOutput);
|
||||
|
@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwit
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.AbstractProcessMetaDataBuilder;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||
@ -131,8 +136,8 @@ public class StreamedETLWithFrontendProcess
|
||||
.withField(new QFieldMetaData(FIELD_DESTINATION_TABLE, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_DESTINATION_TABLE)))
|
||||
.withField(new QFieldMetaData(FIELD_SUPPORTS_FULL_VALIDATION, QFieldType.BOOLEAN).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_SUPPORTS_FULL_VALIDATION, true)))
|
||||
.withField(new QFieldMetaData(FIELD_DEFAULT_QUERY_FILTER, QFieldType.STRING).withDefaultValue(defaultFieldValues.get(FIELD_DEFAULT_QUERY_FILTER)))
|
||||
.withField(new QFieldMetaData(FIELD_EXTRACT_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(extractStepClass)))
|
||||
.withField(new QFieldMetaData(FIELD_TRANSFORM_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(transformStepClass)))
|
||||
.withField(new QFieldMetaData(FIELD_EXTRACT_CODE, QFieldType.STRING).withDefaultValue(extractStepClass == null ? null : new QCodeReference(extractStepClass)))
|
||||
.withField(new QFieldMetaData(FIELD_TRANSFORM_CODE, QFieldType.STRING).withDefaultValue(transformStepClass == null ? null : new QCodeReference(transformStepClass)))
|
||||
.withField(new QFieldMetaData(FIELD_PREVIEW_MESSAGE, QFieldType.STRING).withDefaultValue(defaultFieldValues.getOrDefault(FIELD_PREVIEW_MESSAGE, DEFAULT_PREVIEW_MESSAGE_FOR_INSERT)))
|
||||
);
|
||||
|
||||
@ -153,7 +158,7 @@ public class StreamedETLWithFrontendProcess
|
||||
.withName(STEP_NAME_EXECUTE)
|
||||
.withCode(new QCodeReference(StreamedETLExecuteStep.class))
|
||||
.withInputData(new QFunctionInputMetaData()
|
||||
.withField(new QFieldMetaData(FIELD_LOAD_CODE, QFieldType.STRING).withDefaultValue(new QCodeReference(loadStepClass))))
|
||||
.withField(new QFieldMetaData(FIELD_LOAD_CODE, QFieldType.STRING).withDefaultValue(loadStepClass == null ? null : new QCodeReference(loadStepClass))))
|
||||
.withOutputMetaData(new QFunctionOutputMetaData()
|
||||
.withField(new QFieldMetaData(FIELD_PROCESS_SUMMARY, QFieldType.STRING))
|
||||
);
|
||||
@ -169,4 +174,204 @@ public class StreamedETLWithFrontendProcess
|
||||
.addStep(executeStep)
|
||||
.addStep(resultStep);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Builder processMetaDataBuilder()
|
||||
{
|
||||
return (new Builder(defineProcessMetaData(null, null, null, Collections.emptyMap())));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class Builder extends AbstractProcessMetaDataBuilder
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder(QProcessMetaData processMetaData)
|
||||
{
|
||||
super(processMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for extractStepClass
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withExtractStepClass(Class<? extends AbstractExtractStep> extractStepClass)
|
||||
{
|
||||
setInputFieldDefaultValue(FIELD_EXTRACT_CODE, new QCodeReference(extractStepClass));
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for transformStepClass
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withTransformStepClass(Class<? extends AbstractTransformStep> transformStepClass)
|
||||
{
|
||||
setInputFieldDefaultValue(FIELD_TRANSFORM_CODE, new QCodeReference(transformStepClass));
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for loadStepClass
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withLoadStepClass(Class<? extends AbstractLoadStep> loadStepClass)
|
||||
{
|
||||
setInputFieldDefaultValue(FIELD_LOAD_CODE, new QCodeReference(loadStepClass));
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sourceTable
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withSourceTable(String sourceTable)
|
||||
{
|
||||
setInputFieldDefaultValue(FIELD_SOURCE_TABLE, sourceTable);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for destinationTable
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withDestinationTable(String destinationTable)
|
||||
{
|
||||
setInputFieldDefaultValue(FIELD_DESTINATION_TABLE, destinationTable);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for supportsFullValidation
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withSupportsFullValidation(Boolean supportsFullValidation)
|
||||
{
|
||||
setInputFieldDefaultValue(FIELD_SUPPORTS_FULL_VALIDATION, supportsFullValidation);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for doFullValidation
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withDoFullValidation(Boolean doFullValidation)
|
||||
{
|
||||
setInputFieldDefaultValue(FIELD_DO_FULL_VALIDATION, doFullValidation);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for defaultQueryFilter
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withDefaultQueryFilter(QQueryFilter defaultQueryFilter)
|
||||
{
|
||||
setInputFieldDefaultValue(FIELD_DEFAULT_QUERY_FILTER, defaultQueryFilter);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for previewMessage
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withPreviewMessage(String previewMessage)
|
||||
{
|
||||
setInputFieldDefaultValue(FIELD_PREVIEW_MESSAGE, previewMessage);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withName(String name)
|
||||
{
|
||||
processMetaData.setName(name);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for label
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withLabel(String name)
|
||||
{
|
||||
processMetaData.setLabel(name);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tableName
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withTableName(String tableName)
|
||||
{
|
||||
processMetaData.setTableName(tableName);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for icon
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withIcon(QIcon icon)
|
||||
{
|
||||
processMetaData.setIcon(icon);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withReviewStepRecordFields(List<QFieldMetaData> fieldList)
|
||||
{
|
||||
QFrontendStepMetaData reviewStep = processMetaData.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW);
|
||||
for(QFieldMetaData fieldMetaData : fieldList)
|
||||
{
|
||||
reviewStep.withRecordListField(fieldMetaData);
|
||||
}
|
||||
|
||||
return (this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -46,6 +47,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
*******************************************************************************/
|
||||
public class ExecuteReportStep implements BackendStep
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
@ -70,10 +75,9 @@ public class ExecuteReportStep implements BackendStep
|
||||
|
||||
new GenerateReportAction().execute(reportInput);
|
||||
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmm").withZone(ZoneId.systemDefault());
|
||||
String datePart = formatter.format(Instant.now());
|
||||
String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, report);
|
||||
|
||||
runBackendStepOutput.addValue("downloadFileName", report.getLabel() + " " + datePart + ".xlsx");
|
||||
runBackendStepOutput.addValue("downloadFileName", downloadFileBaseName + ".xlsx");
|
||||
runBackendStepOutput.addValue("serverFilePath", tmpFile.getCanonicalPath());
|
||||
}
|
||||
}
|
||||
@ -82,4 +86,26 @@ public class ExecuteReportStep implements BackendStep
|
||||
throw (new QException("Error running report", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String getDownloadFileBaseName(RunBackendStepInput runBackendStepInput, QReportMetaData report)
|
||||
{
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmm").withZone(ZoneId.systemDefault());
|
||||
String datePart = formatter.format(Instant.now());
|
||||
|
||||
String downloadFileBaseName = runBackendStepInput.getValueString("downloadFileBaseName");
|
||||
if(!StringUtils.hasContent(downloadFileBaseName))
|
||||
{
|
||||
downloadFileBaseName = report.getLabel();
|
||||
}
|
||||
|
||||
downloadFileBaseName = downloadFileBaseName.replaceAll("/", "-");
|
||||
|
||||
return (downloadFileBaseName + " - " + datePart);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.reports;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
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.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
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.reporting.QReportMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Version of PrepareReportStep for a report that runs off a single record.
|
||||
*******************************************************************************/
|
||||
public class PrepareReportForRecordStep extends PrepareReportStep
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
super.run(runBackendStepInput, runBackendStepOutput);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// look for the recordId having been posted to the process - error if not found //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
Serializable recordId = null;
|
||||
if("recordIds".equals(runBackendStepInput.getValueString("recordsParam")))
|
||||
{
|
||||
String recordIdsString = runBackendStepInput.getValueString("recordIds");
|
||||
String[] recordIdsArray = recordIdsString.split(",");
|
||||
if(recordIdsArray.length != 1)
|
||||
{
|
||||
throw (new QUserFacingException("Exactly 1 record must be selected as input to this report."));
|
||||
}
|
||||
|
||||
recordId = recordIdsArray[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QUserFacingException("No record was selected as input to this report."));
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// look for the recordI input field on the process - put the input recordId in that field. //
|
||||
// then remove that input field from the process's inputFieldList //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@SuppressWarnings("unchecked")
|
||||
ArrayList<QFieldMetaData> inputFieldList = (ArrayList<QFieldMetaData>) runBackendStepOutput.getValue("inputFieldList");
|
||||
if(CollectionUtils.nullSafeHasContents(inputFieldList))
|
||||
{
|
||||
Iterator<QFieldMetaData> inputFieldListIterator = inputFieldList.iterator();
|
||||
while(inputFieldListIterator.hasNext())
|
||||
{
|
||||
QFieldMetaData fieldMetaData = inputFieldListIterator.next();
|
||||
if(fieldMetaData.getName().equals(RunReportForRecordProcess.FIELD_RECORD_ID))
|
||||
{
|
||||
runBackendStepOutput.addValue(RunReportForRecordProcess.FIELD_RECORD_ID, recordId);
|
||||
inputFieldListIterator.remove();
|
||||
runBackendStepOutput.addValue("inputFieldList", inputFieldList);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GetInput getInput = new GetInput(runBackendStepInput.getInstance());
|
||||
getInput.setSession(runBackendStepInput.getSession());
|
||||
getInput.setTableName(runBackendStepInput.getTableName());
|
||||
getInput.setPrimaryKey(recordId);
|
||||
getInput.setShouldGenerateDisplayValues(true);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
QRecord record = getOutput.getRecord();
|
||||
if(record == null)
|
||||
{
|
||||
throw (new QUserFacingException("The selected record for the report was not found."));
|
||||
}
|
||||
|
||||
String reportName = runBackendStepInput.getValueString("reportName");
|
||||
QReportMetaData report = runBackendStepInput.getInstance().getReport(reportName);
|
||||
// runBackendStepOutput.addValue("downloadFileBaseName", runBackendStepInput.getTable().getLabel() + " " + record.getRecordLabel());
|
||||
runBackendStepOutput.addValue("downloadFileBaseName", report.getLabel() + " - " + record.getRecordLabel());
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there are no more input fields, then remove the INPUT step from the process. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
inputFieldList = (ArrayList<QFieldMetaData>) runBackendStepOutput.getValue("inputFieldList");
|
||||
if(!CollectionUtils.nullSafeHasContents(inputFieldList))
|
||||
{
|
||||
removeInputStepFromProcess(runBackendStepOutput);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -43,6 +43,10 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
*******************************************************************************/
|
||||
public class PrepareReportStep implements BackendStep
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
@ -68,12 +72,22 @@ public class PrepareReportStep implements BackendStep
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////
|
||||
// no input? re-route the process to skip the input screen //
|
||||
//////////////////////////////////////////////////////////////
|
||||
List<String> stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList());
|
||||
stepList.removeIf(s -> s.equals(BasicRunReportProcess.STEP_NAME_INPUT));
|
||||
runBackendStepOutput.getProcessState().setStepList(stepList);
|
||||
removeInputStepFromProcess(runBackendStepOutput);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected void removeInputStepFromProcess(RunBackendStepOutput runBackendStepOutput)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////
|
||||
// no input? re-route the process to skip the input screen //
|
||||
//////////////////////////////////////////////////////////////
|
||||
List<String> stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList());
|
||||
stepList.removeIf(s -> s.equals(BasicRunReportProcess.STEP_NAME_INPUT));
|
||||
runBackendStepOutput.getProcessState().setStepList(stepList);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,167 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.reports;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.AbstractProcessMetaDataBuilder;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Definition for Basic process to run a report.
|
||||
*******************************************************************************/
|
||||
public class RunReportForRecordProcess
|
||||
{
|
||||
public static final String PROCESS_NAME = "reports.forRecord";
|
||||
|
||||
public static final String STEP_NAME_PREPARE = "prepare";
|
||||
public static final String STEP_NAME_INPUT = "input";
|
||||
public static final String STEP_NAME_EXECUTE = "execute";
|
||||
public static final String STEP_NAME_ACCESS = "accessReport";
|
||||
|
||||
public static final String FIELD_REPORT_NAME = "reportName";
|
||||
public static final String FIELD_RECORD_ID = "recordId";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Builder processMetaDataBuilder()
|
||||
{
|
||||
return (new Builder(defineProcessMetaData()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static QProcessMetaData defineProcessMetaData()
|
||||
{
|
||||
QStepMetaData prepareStep = new QBackendStepMetaData()
|
||||
.withName(STEP_NAME_PREPARE)
|
||||
.withCode(new QCodeReference(PrepareReportForRecordStep.class))
|
||||
.withInputData(new QFunctionInputMetaData()
|
||||
.withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING))
|
||||
.withField(new QFieldMetaData(FIELD_RECORD_ID, QFieldType.STRING)));
|
||||
|
||||
QStepMetaData inputStep = new QFrontendStepMetaData()
|
||||
.withName(STEP_NAME_INPUT)
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
|
||||
|
||||
QStepMetaData executeStep = new QBackendStepMetaData()
|
||||
.withName(STEP_NAME_EXECUTE)
|
||||
.withCode(new QCodeReference(ExecuteReportStep.class))
|
||||
.withInputData(new QFunctionInputMetaData()
|
||||
.withField(new QFieldMetaData(FIELD_REPORT_NAME, QFieldType.STRING)));
|
||||
|
||||
QStepMetaData accessStep = new QFrontendStepMetaData()
|
||||
.withName(STEP_NAME_ACCESS)
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.DOWNLOAD_FORM));
|
||||
// .withViewField(new QFieldMetaData("outputFile", QFieldType.STRING))
|
||||
// .withViewField(new QFieldMetaData("message", QFieldType.STRING));
|
||||
|
||||
return new QProcessMetaData()
|
||||
.withName(PROCESS_NAME)
|
||||
.addStep(prepareStep)
|
||||
.addStep(inputStep)
|
||||
.addStep(executeStep)
|
||||
.addStep(accessStep);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class Builder extends AbstractProcessMetaDataBuilder
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder(QProcessMetaData processMetaData)
|
||||
{
|
||||
super(processMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withProcessName(String name)
|
||||
{
|
||||
processMetaData.setName(name);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tableName
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withTableName(String tableName)
|
||||
{
|
||||
processMetaData.setTableName(tableName);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for icon
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withIcon(QIcon icon)
|
||||
{
|
||||
processMetaData.setIcon(icon);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for reportName
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withReportName(String reportName)
|
||||
{
|
||||
setInputFieldDefaultValue(RunReportForRecordProcess.FIELD_REPORT_NAME, reportName);
|
||||
return (this);
|
||||
}
|
||||
}
|
||||
}
|
@ -54,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
@ -143,9 +144,8 @@ class QInstanceValidatorTest
|
||||
qInstance.setTables(null);
|
||||
qInstance.setProcesses(null);
|
||||
},
|
||||
"At least 1 table must be defined",
|
||||
"Unrecognized table shape for possibleValueSource shape",
|
||||
"Unrecognized processName for queue");
|
||||
true,
|
||||
"At least 1 table must be defined");
|
||||
}
|
||||
|
||||
|
||||
@ -162,9 +162,8 @@ class QInstanceValidatorTest
|
||||
qInstance.setTables(new HashMap<>());
|
||||
qInstance.setProcesses(new HashMap<>());
|
||||
},
|
||||
"At least 1 table must be defined",
|
||||
"Unrecognized table shape for possibleValueSource shape",
|
||||
"Unrecognized processName for queue");
|
||||
true,
|
||||
"At least 1 table must be defined");
|
||||
}
|
||||
|
||||
|
||||
@ -570,6 +569,40 @@ class QInstanceValidatorTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFieldSectionDuplicateName()
|
||||
{
|
||||
QTableMetaData table = new QTableMetaData().withName("test")
|
||||
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
|
||||
.withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id")))
|
||||
.withSection(new QFieldSection("section1", "Section 2", new QIcon("person"), Tier.T2, List.of("name")))
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("name", QFieldType.INTEGER));
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "more than 1 section named");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFieldSectionDuplicateLabel()
|
||||
{
|
||||
QTableMetaData table = new QTableMetaData().withName("test")
|
||||
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
|
||||
.withSection(new QFieldSection("section1", "Section 1", new QIcon("person"), Tier.T1, List.of("id")))
|
||||
.withSection(new QFieldSection("section2", "Section 1", new QIcon("person"), Tier.T2, List.of("name")))
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("name", QFieldType.INTEGER));
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.addTable(table), "more than 1 section labeled");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -1258,6 +1291,138 @@ class QInstanceValidatorTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testReportName()
|
||||
{
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName(null),
|
||||
"Inconsistent naming for report");
|
||||
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName(""),
|
||||
"Inconsistent naming for report");
|
||||
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withName("wrongName"),
|
||||
"Inconsistent naming for report");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testReportNoDataSources()
|
||||
{
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withDataSources(null),
|
||||
"At least 1 data source",
|
||||
"unrecognized dataSourceName");
|
||||
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).withDataSources(new ArrayList<>()),
|
||||
"At least 1 data source",
|
||||
"unrecognized dataSourceName");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testReportDataSourceNames()
|
||||
{
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setName(null),
|
||||
"Missing name for a dataSource",
|
||||
"unrecognized dataSourceName");
|
||||
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setName(""),
|
||||
"Missing name for a dataSource",
|
||||
"unrecognized dataSourceName");
|
||||
|
||||
assertValidationFailureReasons((qInstance) ->
|
||||
{
|
||||
List<QReportDataSource> dataSources = new ArrayList<>(qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources());
|
||||
dataSources.add(dataSources.get(0));
|
||||
qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).setDataSources(dataSources);
|
||||
},
|
||||
"More than one dataSource with name");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testReportDataSourceTables()
|
||||
{
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable("notATable"),
|
||||
"is not a table in this instance");
|
||||
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable(null),
|
||||
"does not have a sourceTable");
|
||||
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).setSourceTable(""),
|
||||
"does not have a sourceTable");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testReportDataSourceTablesFilter()
|
||||
{
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setFieldName(null),
|
||||
"Missing fieldName for a criteria");
|
||||
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setFieldName("notAField"),
|
||||
"is not a field in this table");
|
||||
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().getCriteria().get(0).setOperator(null),
|
||||
"Missing operator for a criteria");
|
||||
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().withOrderBy(new QFilterOrderBy(null)),
|
||||
"Missing fieldName for an orderBy");
|
||||
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).getQueryFilter().withOrderBy(new QFilterOrderBy("notAField")),
|
||||
"is not a field in this table");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testReportDataSourceStaticDataSupplier()
|
||||
{
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).withStaticDataSupplier(new QCodeReference()),
|
||||
"has both a sourceTable and a staticDataSupplier");
|
||||
|
||||
assertValidationFailureReasons((qInstance) ->
|
||||
{
|
||||
QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0);
|
||||
dataSource.setSourceTable(null);
|
||||
dataSource.setStaticDataSupplier(new QCodeReference(null, QCodeType.JAVA, null));
|
||||
},
|
||||
"missing a code reference name");
|
||||
|
||||
assertValidationFailureReasons((qInstance) ->
|
||||
{
|
||||
QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0);
|
||||
dataSource.setSourceTable(null);
|
||||
dataSource.setStaticDataSupplier(new QCodeReference(ArrayList.class, null));
|
||||
},
|
||||
"is not of the expected type");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.reports;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for BasicRunReportProcess
|
||||
*******************************************************************************/
|
||||
class RunReportForRecordProcessTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testRunReport() throws QException
|
||||
{
|
||||
QInstance instance = TestUtils.defineInstance();
|
||||
TestUtils.insertDefaultShapes(instance);
|
||||
|
||||
RunProcessInput runProcessInput = new RunProcessInput(instance);
|
||||
runProcessInput.setSession(TestUtils.getMockSession());
|
||||
runProcessInput.setProcessName(TestUtils.PROCESS_NAME_RUN_SHAPES_PERSON_REPORT);
|
||||
runProcessInput.addValue(BasicRunReportProcess.FIELD_REPORT_NAME, TestUtils.REPORT_NAME_SHAPES_PERSON);
|
||||
runProcessInput.addValue("recordsParam", "recordIds");
|
||||
runProcessInput.addValue("recordIds", "1");
|
||||
|
||||
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
|
||||
// runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
// assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo(BasicRunReportProcess.STEP_NAME_ACCESS);
|
||||
// assertThat(runProcessOutput.getValues()).containsKeys("downloadFileName", "serverFilePath");
|
||||
}
|
||||
|
||||
}
|
@ -76,6 +76,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaDa
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
|
||||
@ -91,6 +96,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.basepull.Basepul
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.reports.RunReportForRecordProcess;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@ -118,10 +124,12 @@ public class TestUtils
|
||||
public static final String PROCESS_NAME_INCREASE_BIRTHDATE = "increaseBirthdate";
|
||||
public static final String PROCESS_NAME_ADD_TO_PEOPLES_AGE = "addToPeoplesAge";
|
||||
public static final String PROCESS_NAME_BASEPULL = "basepullTest";
|
||||
public static final String PROCESS_NAME_RUN_SHAPES_PERSON_REPORT = "runShapesPersonReport";
|
||||
public static final String TABLE_NAME_PERSON_FILE = "personFile";
|
||||
public static final String TABLE_NAME_PERSON_MEMORY = "personMemory";
|
||||
public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly";
|
||||
public static final String TABLE_NAME_BASEPULL = "basepullTest";
|
||||
public static final String REPORT_NAME_SHAPES_PERSON = "shapesPersonReport";
|
||||
|
||||
public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type
|
||||
public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type
|
||||
@ -167,6 +175,9 @@ public class TestUtils
|
||||
qInstance.addProcess(defineProcessIncreasePersonBirthdate());
|
||||
qInstance.addProcess(defineProcessBasepull());
|
||||
|
||||
qInstance.addReport(defineShapesPersonsReport());
|
||||
qInstance.addProcess(defineShapesPersonReportProcess());
|
||||
|
||||
qInstance.addAutomationProvider(definePollingAutomationProvider());
|
||||
|
||||
qInstance.addQueueProvider(defineSqsProvider());
|
||||
@ -951,4 +962,52 @@ public class TestUtils
|
||||
.withProcessName(PROCESS_NAME_INCREASE_BIRTHDATE));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QReportMetaData defineShapesPersonsReport()
|
||||
{
|
||||
return new QReportMetaData()
|
||||
.withName(REPORT_NAME_SHAPES_PERSON)
|
||||
.withProcessName(PROCESS_NAME_RUN_SHAPES_PERSON_REPORT)
|
||||
.withInputFields(List.of(
|
||||
new QFieldMetaData(RunReportForRecordProcess.FIELD_RECORD_ID, QFieldType.INTEGER).withIsRequired(true)
|
||||
))
|
||||
.withDataSources(List.of(
|
||||
new QReportDataSource()
|
||||
.withName("persons")
|
||||
.withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||
.withQueryFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("favoriteShapeId", QCriteriaOperator.EQUALS, List.of("${input." + RunReportForRecordProcess.FIELD_RECORD_ID + "}")))
|
||||
)
|
||||
))
|
||||
.withViews(List.of(
|
||||
new QReportView()
|
||||
.withName("person")
|
||||
.withDataSourceName("persons")
|
||||
.withType(ReportType.TABLE)
|
||||
.withColumns(List.of(
|
||||
new QReportField().withName("id"),
|
||||
new QReportField().withName("firstName"),
|
||||
new QReportField().withName("lastName")
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QProcessMetaData defineShapesPersonReportProcess()
|
||||
{
|
||||
return RunReportForRecordProcess.processMetaDataBuilder()
|
||||
.withProcessName(PROCESS_NAME_RUN_SHAPES_PERSON_REPORT)
|
||||
.withReportName(REPORT_NAME_SHAPES_PERSON)
|
||||
.withTableName(TestUtils.TABLE_NAME_SHAPE)
|
||||
.getProcessMetaData();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,8 +24,10 @@ package com.kingsrook.qqq.backend.module.api.model.metadata;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.module.api.APIBackendModule;
|
||||
import com.kingsrook.qqq.backend.module.api.model.AuthorizationType;
|
||||
|
||||
@ -379,4 +381,14 @@ public class APIBackendMetaData extends QBackendMetaData
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void performValidation(QInstanceValidator qInstanceValidator)
|
||||
{
|
||||
qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), "Missing baseUrl for API backend: " + getName());
|
||||
}
|
||||
}
|
||||
|
@ -32,9 +32,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
@ -43,7 +42,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@DisabledOnOs(OS.LINUX)
|
||||
@Disabled // OnOs(OS.LINUX)
|
||||
public class EasyPostApiTest
|
||||
{
|
||||
|
||||
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.module.api.model.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for APIBackendMetaData
|
||||
*******************************************************************************/
|
||||
class APIBackendMetaDataTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test()
|
||||
{
|
||||
APIBackendMetaData apiBackendMetaData = new APIBackendMetaData()
|
||||
.withName("test");
|
||||
QInstanceValidator qInstanceValidator = new QInstanceValidator();
|
||||
apiBackendMetaData.performValidation(qInstanceValidator);
|
||||
assertEquals(1, qInstanceValidator.getErrors().size());
|
||||
assertThat(qInstanceValidator.getErrors()).anyMatch(e -> e.contains("Missing baseUrl"));
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user