Skip to content

Commit

Permalink
Allow constraints to be added at a QgsVectorLayer level
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Nov 2, 2016
1 parent 4efad04 commit bd9f672
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 0 deletions.
18 changes: 18 additions & 0 deletions python/core/qgsvectorlayer.sip
Expand Up @@ -1257,6 +1257,24 @@ class QgsVectorLayer : QgsMapLayer
*/
QString defaultValueExpression( int index ) const;

/**
* Returns any constraints which are present for a specified
* field index. These constraints may be inherited from the layer's data provider
* or may be set manually on the vector layer from within QGIS.
* @note added in QGIS 3.0
* @see setFieldConstraints()
*/
QgsField::Constraints fieldConstraints( int fieldIndex ) const;

/**
* Sets the constraints for a specified field index. Any constraints inherited from the layer's
* data provider will be kept intact and cannot be cleared. Ie, calling this method only allows for new
* constraints to be added on top of the existing provider constraints.
* @note added in QGIS 3.0
* @see fieldConstraints()
*/
void setFieldConstraints( int index, QgsField::Constraints constraints );

/** Calculates a list of unique values contained within an attribute in the layer. Note that
* in some circumstances when unsaved changes are present for the layer then the returned list
* may contain outdated values (for instance when the attribute value in a saved feature has
Expand Down
58 changes: 58 additions & 0 deletions src/core/qgsvectorlayer.cpp
Expand Up @@ -1440,6 +1440,26 @@ bool QgsVectorLayer::readXml( const QDomNode& layer_node )
mDefaultExpressionMap.insert( field, expression );
}
}

// constraints
mFieldConstraints.clear();
QDomNode constraintsNode = layer_node.namedItem( "constraints" );
if ( !constraintsNode.isNull() )
{
QDomNodeList constraintNodeList = constraintsNode.toElement().elementsByTagName( "constraint" );
for ( int i = 0; i < constraintNodeList.size(); ++i )
{
QDomElement constraintElem = constraintNodeList.at( i ).toElement();

QString field = constraintElem.attribute( "field", QString() );
int constraints = constraintElem.attribute( "constraints", QString( "0" ) ).toInt();
if ( field.isEmpty() || constraints == 0 )
continue;

mFieldConstraints.insert( field, static_cast< QgsField::Constraints >( constraints ) );
}
}

updateFields();

QDomNode depsNode = layer_node.namedItem( QStringLiteral( "dataDependencies" ) );
Expand Down Expand Up @@ -1646,6 +1666,17 @@ bool QgsVectorLayer::writeXml( QDomNode & layer_node,
}
layer_node.appendChild( defaultsElem );

// constraints
QDomElement constraintsElem = document.createElement( "constraints" );
Q_FOREACH ( const QgsField& field, mFields )
{
QDomElement constraintElem = document.createElement( "constraint" );
constraintElem.setAttribute( "field", field.name() );
constraintElem.setAttribute( "constraints", field.constraints() );
constraintsElem.appendChild( constraintElem );
}
layer_node.appendChild( constraintsElem );

// change dependencies
QDomElement dataDependenciesElement = document.createElement( QStringLiteral( "dataDependencies" ) );
Q_FOREACH ( const QgsMapLayerDependency& dep, dependencies() )
Expand Down Expand Up @@ -2901,6 +2932,17 @@ void QgsVectorLayer::updateFields()

mFields[ index ].setDefaultValueExpression( defaultIt.value() );
}
QMap< QString, QgsField::Constraints >::const_iterator constraintIt = mFieldConstraints.constBegin();
for ( ; constraintIt != mFieldConstraints.constEnd(); ++constraintIt )
{
int index = mFields.lookupField( constraintIt.key() );
if ( index < 0 )
continue;

// always keep provider constraints intact
mFields[ index ].setConstraints( mFields.at( index ).constraints() | constraintIt.value() );
}

if ( oldFields != mFields )
{
emit updatedFields();
Expand Down Expand Up @@ -4205,3 +4247,19 @@ QgsField::Constraints QgsVectorLayer::fieldConstraints( int fieldIndex ) const

return constraints;
}

void QgsVectorLayer::setFieldConstraints( int index, QgsField::Constraints constraints )
{
if ( index < 0 || index >= mFields.count() )
return;

if ( constraints == 0 )
{
mFieldConstraints.remove( mFields.at( index ).name() );
}
else
{
mFieldConstraints.insert( mFields.at( index ).name(), constraints );
}
updateFields();
}
13 changes: 13 additions & 0 deletions src/core/qgsvectorlayer.h
Expand Up @@ -1401,9 +1401,19 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
* field index. These constraints may be inherited from the layer's data provider
* or may be set manually on the vector layer from within QGIS.
* @note added in QGIS 3.0
* @see setFieldConstraints()
*/
QgsField::Constraints fieldConstraints( int fieldIndex ) const;

/**
* Sets the constraints for a specified field index. Any constraints inherited from the layer's
* data provider will be kept intact and cannot be cleared. Ie, calling this method only allows for new
* constraints to be added on top of the existing provider constraints.
* @note added in QGIS 3.0
* @see fieldConstraints()
*/
void setFieldConstraints( int index, QgsField::Constraints constraints );

/** Calculates a list of unique values contained within an attribute in the layer. Note that
* in some circumstances when unsaved changes are present for the layer then the returned list
* may contain outdated values (for instance when the attribute value in a saved feature has
Expand Down Expand Up @@ -1941,6 +1951,9 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
//! Map which stores default value expressions for fields
QgsStringMap mDefaultExpressionMap;

//! Map which stores constraints for fields
QMap< QString, QgsField::Constraints > mFieldConstraints;

//! Holds the configuration for the edit form
QgsEditFormConfig mEditFormConfig;

Expand Down
21 changes: 21 additions & 0 deletions tests/src/python/test_provider_postgres.py
Expand Up @@ -480,6 +480,27 @@ def testUniqueConstraint(self):
self.assertTrue(fields.at(2).constraints() & QgsField.ConstraintUnique)
self.assertFalse(fields.at(3).constraints() & QgsField.ConstraintUnique)

def testConstraintOverwrite(self):
""" test that Postgres provider constraints can't be overwritten by vector layer method """
vl = QgsVectorLayer('%s table="qgis_test"."constraints" sql=' % (self.dbconn), "constraints", "postgres")
self.assertTrue(vl.isValid())

self.assertTrue(vl.dataProvider().fieldConstraints(0) & QgsField.ConstraintNotNull)
self.assertTrue(vl.fields().at(0).constraints() & QgsField.ConstraintNotNull)

# add a constraint at the layer level
vl.setFieldConstraints(0, QgsField.ConstraintUnique)

# should be no change at provider level
self.assertTrue(vl.dataProvider().fieldConstraints(0) & QgsField.ConstraintNotNull)

# but layer should still keep provider constraints...
self.assertTrue(vl.fields().at(0).constraints() & QgsField.ConstraintNotNull)
self.assertTrue(vl.fieldConstraints(0) & QgsField.ConstraintNotNull)
# ...in addition to layer level constraint
self.assertTrue(vl.fields().at(0).constraints() & QgsField.ConstraintUnique)
self.assertTrue(vl.fieldConstraints(0) & QgsField.ConstraintUnique)

# See http://hub.qgis.org/issues/15188
def testNumericPrecision(self):
uri = 'point?field=f1:int'
Expand Down
57 changes: 57 additions & 0 deletions tests/src/python/test_qgsvectorlayer.py
Expand Up @@ -1773,6 +1773,63 @@ def testEvaluatingDefaultExpressions(self):
layer.setDefaultValueExpression(1, 'not a valid expression')
self.assertFalse(layer.defaultValue(1))

def testGetSetConstraints(self):
""" test getting and setting field constraints """
layer = createLayerWithOnePoint()

self.assertFalse(layer.fieldConstraints(0))
self.assertFalse(layer.fieldConstraints(1))
self.assertFalse(layer.fieldConstraints(2))

layer.setFieldConstraints(0, QgsField.ConstraintNotNull)
self.assertEqual(layer.fieldConstraints(0), QgsField.ConstraintNotNull)
self.assertFalse(layer.fieldConstraints(1))
self.assertFalse(layer.fieldConstraints(2))
self.assertEqual(layer.fields().at(0).constraints(), QgsField.ConstraintNotNull)

layer.setFieldConstraints(1, QgsField.ConstraintNotNull | QgsField.ConstraintUnique)
self.assertEqual(layer.fieldConstraints(0), QgsField.ConstraintNotNull)
self.assertEqual(layer.fieldConstraints(1), QgsField.ConstraintNotNull | QgsField.ConstraintUnique)
self.assertFalse(layer.fieldConstraints(2))
self.assertEqual(layer.fields().at(0).constraints(), QgsField.ConstraintNotNull)
self.assertEqual(layer.fields().at(1).constraints(), QgsField.ConstraintNotNull | QgsField.ConstraintUnique)

layer.setFieldConstraints(1, QgsField.Constraints())
self.assertEqual(layer.fieldConstraints(0), QgsField.ConstraintNotNull)
self.assertFalse(layer.fieldConstraints(1))
self.assertFalse(layer.fieldConstraints(2))
self.assertEqual(layer.fields().at(0).constraints(), QgsField.ConstraintNotNull)
self.assertFalse(layer.fields().at(1).constraints())

def testSaveRestoreConstraints(self):
""" test saving and restoring constraints from xml"""
layer = createLayerWithOnePoint()

# no constraints
doc = QDomDocument("testdoc")
elem = doc.createElement("maplayer")
self.assertTrue(layer.writeXml(elem, doc))

layer2 = createLayerWithOnePoint()
self.assertTrue(layer2.readXml(elem))
self.assertFalse(layer2.fieldConstraints(0))
self.assertFalse(layer2.fieldConstraints(1))

# set some constraints
layer.setFieldConstraints(0, QgsField.ConstraintNotNull)
layer.setFieldConstraints(1, QgsField.ConstraintNotNull | QgsField.ConstraintUnique)

doc = QDomDocument("testdoc")
elem = doc.createElement("maplayer")
self.assertTrue(layer.writeXml(elem, doc))

layer3 = createLayerWithOnePoint()
self.assertTrue(layer3.readXml(elem))
self.assertEqual(layer3.fieldConstraints(0), QgsField.ConstraintNotNull)
self.assertEqual(layer3.fieldConstraints(1), QgsField.ConstraintNotNull | QgsField.ConstraintUnique)
self.assertEqual(layer3.fields().at(0).constraints(), QgsField.ConstraintNotNull)
self.assertEqual(layer3.fields().at(1).constraints(), QgsField.ConstraintNotNull | QgsField.ConstraintUnique)

def testGetFeatureLimitWithEdits(self):
""" test getting features with a limit, when edits are present """
layer = createLayerWithOnePoint()
Expand Down

0 comments on commit bd9f672

Please sign in to comment.