Skip to content

Commit

Permalink
[expressions] New is_feature_valid()/is_attribute_valid() functions (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
nirvn committed Dec 30, 2022
1 parent 8bb9a01 commit 7e2e622
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 0 deletions.
30 changes: 30 additions & 0 deletions resources/function_help/json/is_attribute_valid
@@ -0,0 +1,30 @@
{
"name": "is_attribute_valid",
"type": "function",
"groups": ["Record and Attributes"],
"description": "Returns TRUE if a specific feature attribute meets all constraints.",
"arguments": [{
"arg": "attribute",
"description": "an attribute name"
},{
"arg": "feature",
"description": "A feature. If not set, the feature attached to the expression context will be used.",
"optional": true
},{
"arg": "layer",
"description": "A vector layer. If not set, the layer attached to the expression context will be used.",
"optional": true
},{
"arg": "strength",
"description": "Set to 'hard' or 'soft' to narrow down to a specific constraint type. If not set, the function will return FALSE if either a hard or a soft constraint fails.",
"optional": true
}],
"examples": [{
"expression": "is_attribute_valid('HECTARES')",
"returns": "TRUE"
}, {
"expression": "is_attribute_valid('HOUSES',get_feature('my_layer', 'FID', 10), 'my_layer')",
"returns": "FALSE"
}],
"tags": ["constraints", "hard", "soft"]
}
27 changes: 27 additions & 0 deletions resources/function_help/json/is_feature_valid
@@ -0,0 +1,27 @@
{
"name": "is_feature_valid",
"type": "function",
"groups": ["Record and Attributes"],
"description": "Returns TRUE if a feature meets all field constraints.",
"arguments": [{
"arg": "feature",
"description": "A feature. If not set, the feature attached to the expression context will be used.",
"optional": true
},{
"arg": "layer",
"description": "A vector layer. If not set, the layer attached to the expression context will be used.",
"optional": true
},{
"arg": "strength",
"description": "Set to 'hard' or 'soft' to narrow down to a specific constraint type. If not set, the function will return FALSE if either a hard or a soft constraint fails.",
"optional": true
}],
"examples": [{
"expression": "is_feature_valid(strength:='hard')",
"returns": "TRUE"
}, {
"expression": "is_feature_valid(get_feature('my_layer', 'FID', 10), 'my_layer')",
"returns": "FALSE"
}],
"tags": ["constraints", "hard", "soft"]
}
157 changes: 157 additions & 0 deletions src/core/expression/qgsexpressionfunction.cpp
Expand Up @@ -48,6 +48,7 @@
#include "qgsmessagelog.h"
#include "qgsrasterlayer.h"
#include "qgsvectorlayer.h"
#include "qgsvectorlayerutils.h"
#include "qgsrasterbandstats.h"
#include "qgscolorramp.h"
#include "qgsfieldformatterregistry.h"
Expand Down Expand Up @@ -1890,6 +1891,145 @@ static QVariant fcnMapToHtmlDefinitionList( const QVariantList &values, const Qg
return table.arg( rows );
}

static QVariant fcnValidateFeature( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
QVariant layer;
if ( values.size() < 1 || QgsVariantUtils::isNull( values.at( 0 ) ) )
{
layer = context->variable( QStringLiteral( "layer" ) );
}
else
{
//first node is layer id or name
QgsExpressionNode *node = QgsExpressionUtils::getNode( values.at( 0 ), parent );
ENSURE_NO_EVAL_ERROR
layer = node->eval( parent, context );
ENSURE_NO_EVAL_ERROR
}

QgsFeature feature;
if ( values.size() < 2 || QgsVariantUtils::isNull( values.at( 1 ) ) )
{
feature = context->feature();
}
else
{
feature = QgsExpressionUtils::getFeature( values.at( 1 ), parent );
}

QgsFieldConstraints::ConstraintStrength constraintStrength = QgsFieldConstraints::ConstraintStrengthNotSet;
const QString strength = QgsExpressionUtils::getStringValue( values.at( 2 ), parent ).toLower();
if ( strength == QStringLiteral( "hard" ) )
{
constraintStrength = QgsFieldConstraints::ConstraintStrengthHard;
}
else if ( strength == QStringLiteral( "soft" ) )
{
constraintStrength = QgsFieldConstraints::ConstraintStrengthSoft;
}

bool foundLayer = false;
const QVariant res = QgsExpressionUtils::runMapLayerFunctionThreadSafe( layer, context, parent, [parent, feature, constraintStrength]( QgsMapLayer * mapLayer ) -> QVariant
{
QgsVectorLayer *layer = qobject_cast< QgsVectorLayer * >( mapLayer );
if ( !layer )
{
parent->setEvalErrorString( QObject::tr( "No layer provided to conduct constraints checks" ) );
return QVariant();
}

const QgsFields fields = layer->fields();
bool valid = true;
for ( int i = 0; i < fields.size(); i++ )
{
QStringList errors;
valid = QgsVectorLayerUtils::validateAttribute( layer, feature, i, errors, constraintStrength );
if ( !valid )
{
break;
}
}

return valid;
}, foundLayer );

if ( !foundLayer )
{
parent->setEvalErrorString( QObject::tr( "No layer provided to conduct constraints checks" ) );
return QVariant();
}

return res;
}

static QVariant fcnValidateAttribute( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
QVariant layer;
if ( values.size() < 2 || QgsVariantUtils::isNull( values.at( 1 ) ) )
{
layer = context->variable( QStringLiteral( "layer" ) );
}
else
{
//first node is layer id or name
QgsExpressionNode *node = QgsExpressionUtils::getNode( values.at( 1 ), parent );
ENSURE_NO_EVAL_ERROR
layer = node->eval( parent, context );
ENSURE_NO_EVAL_ERROR
}

QgsFeature feature;
if ( values.size() < 3 || QgsVariantUtils::isNull( values.at( 2 ) ) )
{
feature = context->feature();
}
else
{
feature = QgsExpressionUtils::getFeature( values.at( 2 ), parent );
}

QgsFieldConstraints::ConstraintStrength constraintStrength = QgsFieldConstraints::ConstraintStrengthNotSet;
const QString strength = QgsExpressionUtils::getStringValue( values.at( 3 ), parent ).toLower();
if ( strength == QStringLiteral( "hard" ) )
{
constraintStrength = QgsFieldConstraints::ConstraintStrengthHard;
}
else if ( strength == QStringLiteral( "soft" ) )
{
constraintStrength = QgsFieldConstraints::ConstraintStrengthSoft;
}

const QString attributeName = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );

bool foundLayer = false;
const QVariant res = QgsExpressionUtils::runMapLayerFunctionThreadSafe( layer, context, parent, [parent, feature, attributeName, constraintStrength]( QgsMapLayer * mapLayer ) -> QVariant
{
QgsVectorLayer *layer = qobject_cast< QgsVectorLayer * >( mapLayer );
if ( !layer )
{
return QVariant();
}

const int fieldIndex = layer->fields().indexFromName( attributeName );
if ( fieldIndex == -1 )
{
parent->setEvalErrorString( QObject::tr( "The attribute name did not match any field for the given feature" ) );
return QVariant();
}

QStringList errors;
bool valid = QgsVectorLayerUtils::validateAttribute( layer, feature, fieldIndex, errors, constraintStrength );
return valid;
}, foundLayer );

if ( !foundLayer )
{
parent->setEvalErrorString( QObject::tr( "No layer provided to conduct constraints checks" ) );
return QVariant();
}

return res;
}

static QVariant fcnAttributes( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
Expand Down Expand Up @@ -8607,6 +8747,23 @@ const QList<QgsExpressionFunction *> &QgsExpression::Functions()
representAttributesFunc->setIsStatic( false );
functions << representAttributesFunc;

QgsStaticExpressionFunction *validateFeature = new QgsStaticExpressionFunction( QStringLiteral( "is_feature_valid" ),
QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "layer" ), true )
<< QgsExpressionFunction::Parameter( QStringLiteral( "feature" ), true )
<< QgsExpressionFunction::Parameter( QStringLiteral( "strength" ), true ),
fcnValidateFeature, QStringLiteral( "Record and Attributes" ), QString(), false, QSet<QString>() << QgsFeatureRequest::ALL_ATTRIBUTES );
validateFeature->setIsStatic( false );
functions << validateFeature;

QgsStaticExpressionFunction *validateAttribute = new QgsStaticExpressionFunction( QStringLiteral( "is_attribute_valid" ),
QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "attribute" ), false )
<< QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "layer" ), true )
<< QgsExpressionFunction::Parameter( QStringLiteral( "feature" ), true )
<< QgsExpressionFunction::Parameter( QStringLiteral( "strength" ), true ),
fcnValidateAttribute, QStringLiteral( "Record and Attributes" ), QString(), false, QSet<QString>() << QgsFeatureRequest::ALL_ATTRIBUTES );
validateAttribute->setIsStatic( false );
functions << validateAttribute;

QgsStaticExpressionFunction *maptipFunc = new QgsStaticExpressionFunction(
QStringLiteral( "maptip" ),
-1,
Expand Down
50 changes: 50 additions & 0 deletions tests/src/core/testqgsexpression.cpp
Expand Up @@ -180,6 +180,8 @@ class TestQgsExpression: public QObject
f4.setAttribute( QStringLiteral( "col2" ), "test4" );
f4.setAttribute( QStringLiteral( "datef" ), QDate( 2022, 9, 23 ) );
mMemoryLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 << f4 );
mMemoryLayer->setConstraintExpression( 0, QStringLiteral( "col1 > 10" ), QStringLiteral( "col0 is not null" ) );
mMemoryLayer->setFieldConstraint( 0, QgsFieldConstraints::ConstraintExpression, QgsFieldConstraints::ConstraintStrengthHard );
QgsProject::instance()->addMapLayer( mMemoryLayer );

// test layer for aggregates
Expand Down Expand Up @@ -2840,6 +2842,54 @@ class TestQgsExpression: public QObject
QTest::newRow( "num_selected with params" ) << "num_selected('test')" << ( QgsFeatureIds() << 4 << 2 ) << QgsFeature() << noLayer << QVariant( 2 );
}

void constraintsValidity()
{
QFETCH( QString, expression );
QFETCH( QgsFeature, feature );
QFETCH( QgsVectorLayer *, layer );
QFETCH( QVariant, result );

QgsExpressionContext context;
if ( layer )
context.appendScope( QgsExpressionContextUtils::layerScope( layer ) );

context.setFeature( feature );

QgsExpression exp( expression );
QCOMPARE( exp.parserErrorString(), QString() );
exp.prepare( &context );
QVariant res = exp.evaluate( &context );
QCOMPARE( res, result );
}

void constraintsValidity_data()
{
QTest::addColumn<QString>( "expression" );
QTest::addColumn<QgsFeature>( "feature" );
QTest::addColumn<QgsVectorLayer *>( "layer" );
QTest::addColumn<QVariant>( "result" );

QgsFeature firstFeature = mMemoryLayer->getFeature( 1 ); // hard constraint failure on col1
QgsFeature secondFeature = mMemoryLayer->getFeature( 2 ); // all constraints valid
QgsVectorLayer *noLayer = nullptr;

QTest::newRow( "is_feature_valid hard failure" ) << "is_feature_valid()" << firstFeature << mMemoryLayer << QVariant( false );
QTest::newRow( "is_feature_valid hard constraint failure" ) << "is_feature_valid(strength:='hard')" << firstFeature << mMemoryLayer << QVariant( false );
QTest::newRow( "is_feature_valid soft constraint valid" ) << "is_feature_valid(strength:='soft')" << firstFeature << mMemoryLayer << QVariant( true );
QTest::newRow( "is_feature_valid valid" ) << "is_feature_valid()" << secondFeature << mMemoryLayer << QVariant( true );
QTest::newRow( "is_feature_valid hard constraint valid" ) << "is_feature_valid(strength:='hard')" << secondFeature << mMemoryLayer << QVariant( true );
QTest::newRow( "is_feature_valid soft constraint valid" ) << "is_feature_valid(strength:='soft')" << secondFeature << mMemoryLayer << QVariant( true );
QTest::newRow( "is_attribute_valid failure" ) << "is_attribute_valid('col1')" << firstFeature << mMemoryLayer << QVariant( false );
QTest::newRow( "is_attribute_valid hard constraint failure" ) << "is_attribute_valid('col1',strength:='hard')" << firstFeature << mMemoryLayer << QVariant( false );
QTest::newRow( "is_attribute_valid soft constraint valid" ) << "is_attribute_valid('col1',strength:='soft')" << firstFeature << mMemoryLayer << QVariant( true );
QTest::newRow( "is_attribute_valid valid" ) << "is_attribute_valid('col1')" << secondFeature << mMemoryLayer << QVariant( true );
QTest::newRow( "is_attribute_valid hard constraint valid" ) << "is_attribute_valid('col1',strength:='hard')" << secondFeature << mMemoryLayer << QVariant( true );
QTest::newRow( "is_attribute_valid soft constraint valid" ) << "is_attribute_valid('col1',strength:='soft')" << secondFeature << mMemoryLayer << QVariant( true );
QTest::newRow( "is_feature_valid no layer" ) << "is_feature_valid()" << secondFeature << noLayer << QVariant();
QTest::newRow( "is_attribute_valid no layer" ) << "is_attribute_valid('col1')" << secondFeature << noLayer << QVariant();
QTest::newRow( "is_attribute_valid wrong attribute name" ) << "is_attribute_valid('WRONG_NAME')" << secondFeature << mMemoryLayer << QVariant();
}

void layerAggregates()
{
QgsExpressionContext context;
Expand Down

0 comments on commit 7e2e622

Please sign in to comment.