Skip to content

Commit b5864cd

Browse files
nyalldawsonm-kuhn
authored andcommittedNov 16, 2016
[FEATURE] Improve handling of defaults (inc provider default clauses,
literal defaults, and qgis expression defaults) and automatically handle unique value constraints on layers Add a new method QgsVectorLayerUtils::createFeature which returns a new feature which includes all relevant defaults. Any fields with unique value constraints will be guaranteed to have a value which is unique for the field. Currently only in use by the split feature tool. Sponsored by Canton of Zug and the QGEP project
1 parent 95271c8 commit b5864cd

File tree

9 files changed

+230
-41
lines changed

9 files changed

+230
-41
lines changed
 

‎python/core/qgsvectorlayerutils.sip

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,16 @@ class QgsVectorLayerUtils
3636
QgsFieldConstraints::ConstraintStrength strength = QgsFieldConstraints::ConstraintStrengthNotSet,
3737
QgsFieldConstraints::ConstraintOrigin origin = QgsFieldConstraints::ConstraintOriginNotSet );
3838

39+
/**
40+
* Creates a new feature ready for insertion into a layer. Default values and constraints
41+
* (eg unique constraints) will automatically be handled. An optional attribute map can be
42+
* passed for the new feature to copy as many attribute values as possible from the map,
43+
* assuming that they respect the layer's constraints. Note that the created feature is not
44+
* automatically inserted into the layer.
45+
*/
46+
static QgsFeature createFeature( QgsVectorLayer* layer,
47+
const QgsGeometry& geometry = QgsGeometry(),
48+
const QgsAttributeMap& attributes = QgsAttributeMap(),
49+
QgsExpressionContext* context = nullptr );
50+
3951
};

‎src/core/qgsvectorlayereditutils.cpp

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "qgsgeometryfactory.h"
2525
#include "qgis.h"
2626
#include "qgswkbtypes.h"
27+
#include "qgsvectorlayerutils.h"
2728

2829
#include <limits>
2930

@@ -371,28 +372,8 @@ int QgsVectorLayerEditUtils::splitFeatures( const QList<QgsPoint>& splitLine, bo
371372
//insert new features
372373
for ( int i = 0; i < newGeometries.size(); ++i )
373374
{
374-
QgsFeature newFeature;
375-
newFeature.setGeometry( newGeometries.at( i ) );
376-
377-
//use default value where possible for primary key (e.g. autoincrement),
378-
//and use the value from the original (split) feature if not primary key
379-
QgsAttributes newAttributes = feat.attributes();
380-
Q_FOREACH ( int pkIdx, L->dataProvider()->pkAttributeIndexes() )
381-
{
382-
const QVariant defaultValue = L->dataProvider()->defaultValueClause( pkIdx );
383-
if ( !defaultValue.isNull() )
384-
{
385-
newAttributes[ pkIdx ] = defaultValue;
386-
}
387-
else //try with NULL
388-
{
389-
newAttributes[ pkIdx ] = QVariant();
390-
}
391-
}
392-
393-
newFeature.setAttributes( newAttributes );
394-
395-
newFeatures.append( newFeature );
375+
QgsFeature f = QgsVectorLayerUtils::createFeature( L, newGeometries.at( i ), feat.attributes().toMap() );
376+
L->editBuffer()->addFeature( f );
396377
}
397378

398379
if ( topologicalEditing )
@@ -418,10 +399,6 @@ int QgsVectorLayerEditUtils::splitFeatures( const QList<QgsPoint>& splitLine, bo
418399
returnCode = 4;
419400
}
420401

421-
422-
//now add the new features to this vectorlayer
423-
L->editBuffer()->addFeatures( newFeatures );
424-
425402
return returnCode;
426403
}
427404

‎src/core/qgsvectorlayereditutils.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "qgsfeature.h"
2020

2121
#include "qgsvectorlayer.h"
22+
#include "qgsgeometry.h"
2223

2324
class QgsGeometryCache;
2425
class QgsCurve;

‎src/core/qgsvectorlayerutils.cpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,95 @@ bool QgsVectorLayerUtils::validateAttribute( const QgsVectorLayer* layer, const
215215
return valid;
216216
}
217217

218+
QgsFeature QgsVectorLayerUtils::createFeature( QgsVectorLayer* layer, const QgsGeometry& geometry,
219+
const QgsAttributeMap& attributes, QgsExpressionContext* context )
220+
{
221+
if ( !layer )
222+
{
223+
return QgsFeature();
224+
}
225+
226+
QgsExpressionContext* evalContext = context;
227+
QScopedPointer< QgsExpressionContext > tempContext;
228+
if ( !evalContext )
229+
{
230+
// no context passed, so we create a default one
231+
tempContext.reset( new QgsExpressionContext() );
232+
tempContext->appendScope( QgsExpressionContextUtils::globalScope() );
233+
tempContext->appendScope( QgsExpressionContextUtils::projectScope() );
234+
tempContext->appendScope( QgsExpressionContextUtils::layerScope( layer ) );
235+
evalContext = tempContext.data();
236+
}
237+
238+
QgsFields fields = layer->fields();
239+
240+
QgsFeature newFeature( fields );
241+
newFeature.setValid( true );
242+
newFeature.setGeometry( geometry );
243+
244+
// initialise attributes
245+
newFeature.initAttributes( fields.count() );
246+
for ( int idx = 0; idx < fields.count(); ++idx )
247+
{
248+
QVariant v;
249+
bool checkUnique = true;
250+
251+
// in order of priority:
252+
253+
// 1. client side default expression
254+
if ( !layer->defaultValueExpression( idx ).isEmpty() )
255+
{
256+
// client side default expression set - takes precedence over all. Why? Well, this is the only default
257+
// which QGIS users have control over, so we assume that they're deliberately overriding any
258+
// provider defaults for some good reason and we should respect that
259+
v = layer->defaultValue( idx, newFeature, evalContext );
260+
}
261+
262+
// 2. provider side default value clause
263+
// note - not an else if deliberately. Users may return null from a default value expression to fallback to provider defaults
264+
if ( !v.isValid() && fields.fieldOrigin( idx ) == QgsFields::OriginProvider )
265+
{
266+
int providerIndex = fields.fieldOriginIndex( idx );
267+
QString providerDefault = layer->dataProvider()->defaultValueClause( providerIndex );
268+
if ( !providerDefault.isEmpty() )
269+
{
270+
v = providerDefault;
271+
checkUnique = false;
272+
}
273+
}
274+
275+
// 3. passed attribute value
276+
// note - deliberately not using else if!
277+
if ( !v.isValid() && attributes.contains( idx ) )
278+
{
279+
v = attributes.value( idx );
280+
}
281+
282+
// 4. provider side default literal
283+
// note - deliberately not using else if!
284+
if ( !v.isValid() && fields.fieldOrigin( idx ) == QgsFields::OriginProvider )
285+
{
286+
int providerIndex = fields.fieldOriginIndex( idx );
287+
v = layer->dataProvider()->defaultValue( providerIndex );
288+
}
289+
290+
// last of all... check that unique constraints are respected
291+
// we can't handle not null or expression constraints here, since there's no way to pick a sensible
292+
// value if the constraint is violated
293+
if ( checkUnique && fields.at( idx ).constraints().constraints() & QgsFieldConstraints::ConstraintUnique )
294+
{
295+
if ( QgsVectorLayerUtils::valueExists( layer, idx, v ) )
296+
{
297+
// unique constraint violated
298+
QVariant uniqueValue = QgsVectorLayerUtils::createUniqueValue( layer, idx, v );
299+
if ( uniqueValue.isValid() )
300+
v = uniqueValue;
301+
}
302+
}
303+
304+
newFeature.setAttribute( idx, v );
305+
}
306+
307+
return newFeature;
308+
}
309+

‎src/core/qgsvectorlayerutils.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#define QGSVECTORLAYERUTILS_H
1818

1919
#include "qgsvectorlayer.h"
20+
#include "qgsgeometry.h"
2021

2122
/** \ingroup core
2223
* \class QgsVectorLayerUtils
@@ -53,6 +54,17 @@ class CORE_EXPORT QgsVectorLayerUtils
5354
QgsFieldConstraints::ConstraintStrength strength = QgsFieldConstraints::ConstraintStrengthNotSet,
5455
QgsFieldConstraints::ConstraintOrigin origin = QgsFieldConstraints::ConstraintOriginNotSet );
5556

57+
/**
58+
* Creates a new feature ready for insertion into a layer. Default values and constraints
59+
* (eg unique constraints) will automatically be handled. An optional attribute map can be
60+
* passed for the new feature to copy as many attribute values as possible from the map,
61+
* assuming that they respect the layer's constraints. Note that the created feature is not
62+
* automatically inserted into the layer.
63+
*/
64+
static QgsFeature createFeature( QgsVectorLayer* layer,
65+
const QgsGeometry& geometry = QgsGeometry(),
66+
const QgsAttributeMap& attributes = QgsAttributeMap(),
67+
QgsExpressionContext* context = nullptr );
5668

5769
};
5870

‎src/providers/spatialite/qgsspatialiteprovider.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,9 @@ void QgsSpatiaLiteProvider::loadFieldsAbstractInterface( gaiaVectorLayerPtr lyr
762762
}
763763
}
764764

765+
// check for constraints
766+
fetchConstraints();
767+
765768
// for views try to get the primary key from the meta table
766769
if ( mViewBased && mPrimaryKey.isEmpty() )
767770
{
@@ -862,6 +865,15 @@ void QgsSpatiaLiteProvider::fetchConstraints()
862865
}
863866
sqlite3_free_table( results );
864867

868+
Q_FOREACH ( int fieldIdx, mPrimaryKeyAttrs )
869+
{
870+
//primary keys are unique, not null
871+
QgsFieldConstraints constraints = mAttributeFields.at( fieldIdx ).constraints();
872+
constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider );
873+
constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider );
874+
mAttributeFields[ fieldIdx ].setConstraints( constraints );
875+
}
876+
865877
return;
866878

867879
error:

‎tests/src/python/test_provider_postgres.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,21 @@ def testVectorLayerUtilsUniqueWithProviderDefault(self):
529529
self.assertTrue(vl.addFeatures([f]))
530530
self.assertFalse(QgsVectorLayerUtils.valueExists(vl, 0, default_clause))
531531

532+
def testVectorLayerUtilsCreateFeatureWithProviderDefault(self):
533+
vl = QgsVectorLayer('%s table="qgis_test"."someData" sql=' % (self.dbconn), "someData", "postgres")
534+
default_clause = 'nextval(\'qgis_test."someData_pk_seq"\'::regclass)'
535+
self.assertEqual(vl.dataProvider().defaultValueClause(0), default_clause)
536+
537+
# check that provider default clause takes precendence over passed attribute values
538+
# this also checks that the inbuilt unique constraint handling is bypassed in the case of a provider default clause
539+
f = QgsVectorLayerUtils.createFeature(vl, attributes={1: 5, 3: 'map'})
540+
self.assertEqual(f.attributes(), [default_clause, 5, "'qgis'::text", "'qgis'::text", None, None])
541+
542+
# test take vector layer default value expression overrides postgres provider default clause
543+
vl.setDefaultValueExpression(3, "'mappy'")
544+
f = QgsVectorLayerUtils.createFeature(vl, attributes={1: 5, 3: 'map'})
545+
self.assertEqual(f.attributes(), [default_clause, 5, "'qgis'::text", 'mappy', None, None])
546+
532547
# See http://hub.qgis.org/issues/15188
533548
def testNumericPrecision(self):
534549
uri = 'point?field=f1:int'

‎tests/src/python/test_provider_spatialite.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import shutil
2020
import tempfile
2121

22-
from qgis.core import QgsVectorLayer, QgsPoint, QgsFeature, QgsGeometry, QgsProject, QgsMapLayerRegistry, QgsField, QgsFieldConstraints
22+
from qgis.core import QgsVectorLayer, QgsPoint, QgsFeature, QgsGeometry, QgsProject, QgsMapLayerRegistry, QgsField, QgsFieldConstraints, QgsVectorLayerUtils
2323

2424
from qgis.testing import start_app, unittest
2525
from utilities import unitTestDataPath
@@ -185,28 +185,20 @@ def test_SplitFeature(self):
185185
self.assertTrue(layer.isValid())
186186
self.assertTrue(layer.hasGeometryType())
187187
layer.startEditing()
188-
self.assertEqual(layer.splitFeatures([QgsPoint(0.5, -0.5), QgsPoint(0.5, 1.5)], 0), 0)
189-
self.assertEqual(layer.splitFeatures([QgsPoint(-0.5, 0.5), QgsPoint(1.5, 0.5)], 0), 0)
188+
self.assertEqual(layer.splitFeatures([QgsPoint(0.75, -0.5), QgsPoint(0.75, 1.5)], 0), 0)
189+
self.assertEqual(layer.splitFeatures([QgsPoint(-0.5, 0.25), QgsPoint(1.5, 0.25)], 0), 0)
190190
self.assertTrue(layer.commitChanges())
191191
self.assertEqual(layer.featureCount(), 4)
192192

193-
def xtest_SplitFeatureWithFailedCommit(self):
193+
def test_SplitFeatureWithMultiKey(self):
194194
"""Create spatialite database"""
195195
layer = QgsVectorLayer("dbname=%s table=test_pg_mk (geometry)" % self.dbname, "test_pg_mk", "spatialite")
196196
self.assertTrue(layer.isValid())
197197
self.assertTrue(layer.hasGeometryType())
198198
layer.startEditing()
199-
self.asserEqual(layer.splitFeatures([QgsPoint(0.5, -0.5), QgsPoint(0.5, 1.5)], 0), 0)
200-
self.asserEqual(layer.splitFeatures([QgsPoint(-0.5, 0.5), QgsPoint(1.5, 0.5)], 0), 0)
201-
self.assertFalse(layer.commitChanges())
202-
layer.rollBack()
203-
feat = next(layer.getFeatures())
204-
ref = [[(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]]
205-
res = feat.geometry().asPolygon()
206-
for ring1, ring2 in zip(ref, res):
207-
for p1, p2 in zip(ring1, ring2):
208-
for c1, c2 in zip(p1, p2):
209-
self.asserEqual(c1, c2)
199+
self.assertEqual(layer.splitFeatures([QgsPoint(0.5, -0.5), QgsPoint(0.5, 1.5)], 0), 0)
200+
self.assertEqual(layer.splitFeatures([QgsPoint(-0.5, 0.5), QgsPoint(1.5, 0.5)], 0), 0)
201+
self.assertTrue(layer.commitChanges())
210202

211203
def test_queries(self):
212204
"""Test loading of query-based layers"""
@@ -475,6 +467,21 @@ def testDefaultValues(self):
475467
self.assertEqual(l.dataProvider().defaultValue(3), 5.7)
476468
self.assertFalse(l.dataProvider().defaultValue(4))
477469

470+
def testVectorLayerUtilsCreateFeatureWithProviderDefaultLiteral(self):
471+
vl = QgsVectorLayer("dbname=%s table='test_defaults' key='id'" % self.dbname, "test_defaults", "spatialite")
472+
self.assertEqual(vl.dataProvider().defaultValue(2), 5)
473+
474+
f = QgsVectorLayerUtils.createFeature(vl)
475+
self.assertEqual(f.attributes(), [None, "qgis 'is good", 5, 5.7, None])
476+
477+
# check that provider passed attribute values take precedence over default literals
478+
f = QgsVectorLayerUtils.createFeature(vl, attributes={1: 'qgis is great', 0: 3})
479+
self.assertEqual(f.attributes(), [3, "qgis is great", 5, 5.7, None])
480+
481+
# test take vector layer default value expression overrides postgres provider default clause
482+
vl.setDefaultValueExpression(3, "4*3")
483+
f = QgsVectorLayerUtils.createFeature(vl, attributes={1: 'qgis is great', 0: 3})
484+
self.assertEqual(f.attributes(), [3, "qgis is great", 5, 12, None])
478485

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

‎tests/src/python/test_qgsvectorlayerutils.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
QgsFieldConstraints,
2525
QgsFields,
2626
QgsFeature,
27+
QgsGeometry,
28+
QgsPoint,
2729
NULL
2830
)
2931
from qgis.testing import start_app, unittest
@@ -211,5 +213,64 @@ def testCreateUniqueValue(self):
211213
self.assertEqual(QgsVectorLayerUtils.createUniqueValue(layer, 0, 'seed'), 'seed')
212214
self.assertEqual(QgsVectorLayerUtils.createUniqueValue(layer, 0, 'superpig'), 'superpig_1')
213215

216+
def testCreateFeature(self):
217+
""" test creating a feature respecting defaults and constraints """
218+
layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer&field=flddbl:double",
219+
"addfeat", "memory")
220+
# add a bunch of features
221+
f = QgsFeature()
222+
f.setAttributes(["test", 123, 1.0])
223+
f1 = QgsFeature(2)
224+
f1.setAttributes(["test_1", 124, 1.1])
225+
f2 = QgsFeature(3)
226+
f2.setAttributes(["test_2", 125, 2.4])
227+
f3 = QgsFeature(4)
228+
f3.setAttributes(["test_3", 126, 1.7])
229+
f4 = QgsFeature(5)
230+
f4.setAttributes(["superpig", 127, 0.8])
231+
self.assertTrue(layer.dataProvider().addFeatures([f, f1, f2, f3, f4]))
232+
233+
# no layer
234+
self.assertFalse(QgsVectorLayerUtils.createFeature(None).isValid())
235+
236+
# basic tests
237+
f = QgsVectorLayerUtils.createFeature(layer)
238+
self.assertTrue(f.isValid())
239+
self.assertEqual(f.fields(), layer.fields())
240+
self.assertFalse(f.hasGeometry())
241+
self.assertEqual(f.attributes(), [NULL, NULL, NULL])
242+
243+
# set geometry
244+
g = QgsGeometry.fromPoint(QgsPoint(100, 200))
245+
f = QgsVectorLayerUtils.createFeature(layer, g)
246+
self.assertTrue(f.hasGeometry())
247+
self.assertEqual(f.geometry().exportToWkt(), g.exportToWkt())
248+
249+
# using attribute map
250+
f = QgsVectorLayerUtils.createFeature(layer, attributes={0: 'a', 2: 6.0})
251+
self.assertEqual(f.attributes(), ['a', NULL, 6.0])
252+
253+
# layer with default value expression
254+
layer.setDefaultValueExpression(2, '3*4')
255+
f = QgsVectorLayerUtils.createFeature(layer)
256+
self.assertEqual(f.attributes(), [NULL, NULL, 12.0])
257+
# we expect the default value expression to take precedence over the attribute map
258+
f = QgsVectorLayerUtils.createFeature(layer, attributes={0: 'a', 2: 6.0})
259+
self.assertEqual(f.attributes(), ['a', NULL, 12.0])
260+
# layer with default value expression based on geometry
261+
layer.setDefaultValueExpression(2, '3*$x')
262+
f = QgsVectorLayerUtils.createFeature(layer, g)
263+
self.assertEqual(f.attributes(), [NULL, NULL, 300.0])
264+
layer.setDefaultValueExpression(2, None)
265+
266+
# test with violated unique constraints
267+
layer.setFieldConstraint(1, QgsFieldConstraints.ConstraintUnique)
268+
f = QgsVectorLayerUtils.createFeature(layer, attributes={0: 'test_1', 1: 123})
269+
self.assertEqual(f.attributes(), ['test_1', 128, NULL])
270+
layer.setFieldConstraint(0, QgsFieldConstraints.ConstraintUnique)
271+
f = QgsVectorLayerUtils.createFeature(layer, attributes={0: 'test_1', 1: 123})
272+
self.assertEqual(f.attributes(), ['test_4', 128, NULL])
273+
274+
214275
if __name__ == '__main__':
215276
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.