Skip to content

Commit

Permalink
Move responsibility for testing for attribute against constraints
Browse files Browse the repository at this point in the history
to QgsVectorLayerUtils::validateAttribute()

Also clean up some strings shown to the user when a constraint
check fails
  • Loading branch information
nyalldawson committed Nov 2, 2016
1 parent 2500d75 commit c98d380
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 58 deletions.
6 changes: 6 additions & 0 deletions python/core/qgsvectorlayerutils.sip
Expand Up @@ -19,4 +19,10 @@ class QgsVectorLayerUtils
*/
static bool valueExists( const QgsVectorLayer* layer, int fieldIndex, const QVariant& value, const QgsFeatureIds& ignoreIds = QgsFeatureIds() );

/**
* Tests an attribute value to check whether it passes all constraints which are present on the corresponding field.
* Returns true if the attribute value is valid for the field. Any constraint failures will be reported in the errors argument.
*/
static bool validateAttribute( const QgsVectorLayer* layer, const QgsFeature& feature, int attributeIndex, QStringList& errors /Out/ );

};
61 changes: 61 additions & 0 deletions src/core/qgsvectorlayerutils.cpp
Expand Up @@ -49,3 +49,64 @@ bool QgsVectorLayerUtils::valueExists( const QgsVectorLayer* layer, int fieldInd

return false;
}

bool QgsVectorLayerUtils::validateAttribute( const QgsVectorLayer* layer, const QgsFeature& feature, int attributeIndex, QStringList& errors )
{
if ( !layer )
return false;

if ( attributeIndex < 0 || attributeIndex >= layer->fields().count() )
return false;

QgsField field = layer->fields().at( attributeIndex );
QVariant value = feature.attribute( attributeIndex );
bool valid = true;
errors.clear();

if ( field.constraints() & QgsField::ConstraintExpression && !field.constraintExpression().isEmpty() )
{
QgsExpressionContext context = layer->createExpressionContext();
context.setFeature( feature );

QgsExpression expr( field.constraintExpression() );

valid = expr.evaluate( &context ).toBool();

if ( expr.hasParserError() )
{
errors << QObject::tr( "parser error: %1" ).arg( expr.parserErrorString() );
}
else if ( expr.hasEvalError() )
{
errors << QObject::tr( "evaluation error: %1" ).arg( expr.evalErrorString() );
}
else if ( !valid )
{
errors << QObject::tr( "%1 check failed" ).arg( field.constraintDescription() );
}
}

if ( field.constraints() & QgsField::ConstraintNotNull )
{
valid = valid && !value.isNull();

if ( value.isNull() )
{
errors << QObject::tr( "value is NULL" );
}
}

if ( field.constraints() & QgsField::ConstraintUnique )
{
bool alreadyExists = QgsVectorLayerUtils::valueExists( layer, attributeIndex, value, QgsFeatureIds() << feature.id() );
valid = valid && !alreadyExists;

if ( alreadyExists )
{
errors << QObject::tr( "value is not unique" );
}
}

return valid;
}

6 changes: 6 additions & 0 deletions src/core/qgsvectorlayerutils.h
Expand Up @@ -36,6 +36,12 @@ class CORE_EXPORT QgsVectorLayerUtils
*/
static bool valueExists( const QgsVectorLayer* layer, int fieldIndex, const QVariant& value, const QgsFeatureIds& ignoreIds = QgsFeatureIds() );

/**
* Tests an attribute value to check whether it passes all constraints which are present on the corresponding field.
* Returns true if the attribute value is valid for the field. Any constraint failures will be reported in the errors argument.
*/
static bool validateAttribute( const QgsVectorLayer* layer, const QgsFeature& feature, int attributeIndex, QStringList& errors );

};

#endif // QGSVECTORLAYERUTILS_H
65 changes: 10 additions & 55 deletions src/gui/editorwidgets/core/qgseditorwidgetwrapper.cpp
Expand Up @@ -113,61 +113,25 @@ void QgsEditorWidgetWrapper::updateConstraint( const QgsFeature &ft )

QString expression = field.constraintExpression();
QStringList expressions, descriptions;
QVariant value = ft.attribute( mFieldIdx );

mConstraintFailureReason.clear();

QStringList errors;

if ( ! expression.isEmpty() )
{
expressions << expression;
descriptions << field.constraintDescription();

QgsExpressionContext context = layer()->createExpressionContext();
context.setFeature( ft );

QgsExpression expr( expression );

mValidConstraint = expr.evaluate( &context ).toBool();

if ( expr.hasParserError() )
{
errors << tr( "Parser error: %1" ).arg( expr.parserErrorString() );
}
else if ( expr.hasEvalError() )
{
errors << tr( "Evaluation error: %1" ).arg( expr.evalErrorString() );
}
else if ( ! mValidConstraint )
{
errors << tr( "%1 check failed" ).arg( field.constraintDescription() );
}

toEmit = true;
}
else
mValidConstraint = true;

if ( field.constraints() & QgsField::ConstraintNotNull )
{
descriptions << QStringLiteral( "NotNull" );
descriptions << QStringLiteral( "Not NULL" );
if ( !expression.isEmpty() )
{
expressions << field.name() + " IS NOT NULL";
}
else
{
expressions << QStringLiteral( "NotNull" );
expressions << QStringLiteral( "IS NOT NULL" );
}

mValidConstraint = mValidConstraint && !value.isNull();

if ( value.isNull() )
{
errors << tr( "Value is NULL" );
}

toEmit = true;
}

Expand All @@ -180,29 +144,20 @@ void QgsEditorWidgetWrapper::updateConstraint( const QgsFeature &ft )
}
else
{
expression = QStringLiteral( "Unique" );
}

bool alreadyExists = QgsVectorLayerUtils::valueExists( layer(), mFieldIdx, value, QgsFeatureIds() << ft.id() );
mValidConstraint = mValidConstraint && !alreadyExists;

if ( alreadyExists )
{
errors << tr( "Value is not unique" );
expressions << QStringLiteral( "IS UNIQUE" );
}

toEmit = true;
}

QStringList errors;
mValidConstraint = QgsVectorLayerUtils::validateAttribute( layer(), ft, mFieldIdx, errors );
mConstraintFailureReason = errors.join( ", " );

if ( toEmit )
{
QString errStr = errors.isEmpty() ? tr( "Constraint checks passed" ) : errors.join( '\n' );
mConstraintFailureReason = errors.join( ", " );
QString description;
if ( descriptions.size() > 1 )
description = "( " + descriptions.join( " ) AND ( " ) + " )";
else if ( !descriptions.isEmpty() )
description = descriptions.at( 0 );
QString errStr = errors.isEmpty() ? tr( "Constraint checks passed" ) : mConstraintFailureReason;

QString description = descriptions.join( ", " );
QString expressionDesc;
if ( expressions.size() > 1 )
expressionDesc = "( " + expressions.join( " ) AND ( " ) + " )";
Expand Down
5 changes: 3 additions & 2 deletions src/gui/qgsattributeform.cpp
Expand Up @@ -901,8 +901,9 @@ void QgsAttributeForm::onConstraintStatusChanged( const QString& constraint,

if ( buddy )
{
QString tooltip = tr( "Description: " ) + description + "\n" +
tr( "Raw expression: " ) + constraint + "\n" + tr( "Constraint: " ) + err;
QString tooltip = QStringLiteral( "<b>" ) + tr( "Constraints: " ) + QStringLiteral( "</b>" ) + description +
QStringLiteral( "<br /><b>" ) + tr( "Raw expression: " ) + QStringLiteral( "</b>" ) + constraint +
QStringLiteral( "<br /><b>" ) + tr( "Result: " ) + QStringLiteral( "</b>" ) + err;
buddy->setToolTip( tooltip );

if ( !buddy->property( "originalText" ).isValid() )
Expand Down
70 changes: 69 additions & 1 deletion tests/src/python/test_qgsvectorlayerutils.py
Expand Up @@ -22,7 +22,8 @@
QgsVectorLayerUtils,
QgsField,
QgsFields,
QgsFeature
QgsFeature,
NULL
)
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
Expand Down Expand Up @@ -83,6 +84,73 @@ def test_value_exists(self):
self.assertFalse(QgsVectorLayerUtils.valueExists(layer, 1, 125, [99999, 3]))
self.assertFalse(QgsVectorLayerUtils.valueExists(layer, 1, 125, [2, 4, 5, 3]))

def test_validate_attribute(self):
""" test validating attributes against constraints """
layer = createLayerWithOnePoint()

# field expression check
layer.setConstraintExpression(1, 'fldint>5')

f = QgsFeature(2)
f.setAttributes(["test123", 6])
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f, 1)
self.assertTrue(res)
self.assertEqual(len(errors), 0)
f.setAttributes(["test123", 2])
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f, 1)
self.assertFalse(res)
self.assertEqual(len(errors), 1)
print(errors)

# bad field expression check
layer.setConstraintExpression(1, 'fldint>')
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f, 1)
self.assertFalse(res)
self.assertEqual(len(errors), 1)
print(errors)

layer.setConstraintExpression(1, None)

# not null constraint
f.setAttributes(["test123", NULL])
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f, 1)
self.assertTrue(res)
self.assertEqual(len(errors), 0)

layer.setFieldConstraints(1, QgsField.ConstraintNotNull)
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f, 1)
self.assertFalse(res)
self.assertEqual(len(errors), 1)
print(errors)

# unique constraint
f.setAttributes(["test123", 123])
layer.setFieldConstraints(1, QgsField.Constraints())
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f, 1)
self.assertTrue(res)
self.assertEqual(len(errors), 0)
layer.setFieldConstraints(1, QgsField.ConstraintUnique)
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f, 1)
self.assertFalse(res)
self.assertEqual(len(errors), 1)
print(errors)

# check - same id should be ignored when testing for uniqueness
f1 = QgsFeature(1)
f1.setAttributes(["test123", 123])
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f1, 1)
self.assertTrue(res)
self.assertEqual(len(errors), 0)

# test double constraint failure
layer.setConstraintExpression(1, 'fldint>5')
layer.setFieldConstraints(1, QgsField.ConstraintNotNull)
f.setAttributes(["test123", NULL])
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f, 1)
self.assertFalse(res)
self.assertEqual(len(errors), 2)
print(errors)


if __name__ == '__main__':
unittest.main()

0 comments on commit c98d380

Please sign in to comment.