Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[FEATURE] Client side default field values
Allows an expression to be set for a vector layer field which
is used to evaluate a default value for this field.

A new method,
QgsVectorLayer::defaultValue( int index,
                              const QgsFeature& feature = QgsFeature(),
                              QgsExpressionContext* context = nullptr )
has been added which evaluates the default value for a given field
using the optionally passed feature and expression context. This
allows default values to utilise properties of the feature
which exist at the time of calling, such as digitized geometries.
The expression context parameter allows variables to be used
in default value expressions, making it easier to eg insert
a user's name, current datetime, project path, etc

Default values are set using QgsVectorLayer::setDefaultValueExpression()
and retrieved using defaultValueExpression()

Sponsored by DB Fahrwegdienste GmbH
  • Loading branch information
nyalldawson committed Aug 30, 2016
1 parent 9c62eec commit 6ed2cf2
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 3 deletions.
36 changes: 36 additions & 0 deletions python/core/qgsvectorlayer.sip
Expand Up @@ -1317,6 +1317,42 @@ class QgsVectorLayer : QgsMapLayer
/** Caches joined attributes if required (and not already done) */
void createJoinCaches();

/** Returns the calculated default value for the specified field index. The default
* value may be taken from a client side default value expression (see setDefaultValueExpression())
* or taken from the underlying data provider.
* @param index field index
* @param feature optional feature to use for default value evaluation. If passed,
* then properties from the feature (such as geometry) can be used when calculating
* the default value.
* @param context optional expression context to evaluate expressions again. If not
* specified, a default context will be created
* @return calculated default value
* @note added in QGIS 3.0
* @see setDefaultValueExpression()
*/
QVariant defaultValue( int index, const QgsFeature& feature = QgsFeature(),
QgsExpressionContext* context = nullptr ) const;

/** Sets an expression to use when calculating the default value for a field.
* @param index field index
* @param expression expression to evaluate when calculating default values for field. Pass
* an empty expression to clear the default.
* @note added in QGIS 3.0
* @see defaultValue()
* @see defaultValueExpression()
*/
void setDefaultValueExpression( int index, const QString& expression );

/** Returns the expression used when calculating the default value for a field.
* @param index field index
* @returns expression evaluated when calculating default values for field, or an
* empty string if no default is set
* @note added in QGIS 3.0
* @see defaultValue()
* @see setDefaultValueExpression()
*/
QString defaultValueExpression( int index ) const;

/** 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
4 changes: 2 additions & 2 deletions src/app/qgsattributetypedialog.h
Expand Up @@ -94,14 +94,14 @@ class APP_EXPORT QgsAttributeTypeDialog: public QDialog, private Ui::QgsAttribut
*/
bool notNull() const;

/*
/**
* Setter for constraint expression description
* @param desc the expression description
* @note added in QGIS 2.16
**/
void setExpressionDescription( const QString &desc );

/*
/**
* Getter for constraint expression description
* @return the expression description
* @note added in QGIS 2.16
Expand Down
105 changes: 105 additions & 0 deletions src/core/qgsvectorlayer.cpp
Expand Up @@ -1679,6 +1679,28 @@ bool QgsVectorLayer::readXml( const QDomNode& layer_node )

readStyleManager( layer_node );

// default expressions
mDefaultExpressionMap.clear();
QDomNode defaultsNode = layer_node.namedItem( "defaults" );
if ( !defaultsNode.isNull() )
{
QDomNodeList defaultNodeList = defaultsNode.toElement().elementsByTagName( "default" );
for ( int i = 0; i < defaultNodeList.size(); ++i )
{
QDomElement defaultElem = defaultNodeList.at( i ).toElement();

QString field = defaultElem.attribute( "field", QString() );
QString expression = defaultElem.attribute( "expression", QString() );
if ( field.isEmpty() || expression.isEmpty() )
continue;

int index = mUpdatedFields.fieldNameIndex( field );
if ( index < 0 )
continue;

mDefaultExpressionMap.insert( index, expression );
}
}

setLegend( QgsMapLayerLegend::defaultVectorLegend( this ) );

Expand Down Expand Up @@ -1880,6 +1902,26 @@ bool QgsVectorLayer::writeXml( QDomNode & layer_node,
// save expression fields
mExpressionFieldBuffer->writeXml( layer_node, document );

//default expressions
if ( !mDefaultExpressionMap.isEmpty() )
{
QDomElement defaultsElem = document.createElement( "defaults" );
QMap<int, QString>::const_iterator it = mDefaultExpressionMap.constBegin();
for ( ; it != mDefaultExpressionMap.constEnd(); ++it )
{
if ( it.key() >= mUpdatedFields.count() )
continue;

QString fieldName = mUpdatedFields.at( it.key() ).name();

QDomElement defaultElem = document.createElement( "default" );
defaultElem.setAttribute( "field", fieldName );
defaultElem.setAttribute( "expression", it.value() );
defaultsElem.appendChild( defaultElem );
}
layer_node.appendChild( defaultsElem );
}

writeStyleManager( layer_node, document );

// renderer specific settings
Expand Down Expand Up @@ -3133,6 +3175,69 @@ void QgsVectorLayer::createJoinCaches()
}
}

QVariant QgsVectorLayer::defaultValue( int index, const QgsFeature& feature, QgsExpressionContext* context ) const
{
QString expression = mDefaultExpressionMap.value( index, QString() );
if ( expression.isEmpty() )
return mDataProvider->defaultValue( index );

QgsExpressionContext* evalContext = context;
QScopedPointer< QgsExpressionContext > tempContext;
if ( !evalContext )
{
// no context passed, so we create a default one
tempContext.reset( new QgsExpressionContext() );
tempContext->appendScope( QgsExpressionContextUtils::globalScope() );
tempContext->appendScope( QgsExpressionContextUtils::projectScope() );
tempContext->appendScope( QgsExpressionContextUtils::layerScope( this ) );
evalContext = tempContext.data();
}

if ( feature.isValid() )
{
QgsExpressionContextScope* featScope = new QgsExpressionContextScope();
featScope->setFeature( feature );
featScope->setFields( *feature.fields() );
evalContext->appendScope( featScope );
}

QVariant val;
QgsExpression exp( expression );
exp.prepare( evalContext );
if ( exp.hasEvalError() )
{
QgsLogger::warning( "Error evaluating default value: " + exp.evalErrorString() );
}
else
{
val = exp.evaluate( evalContext );
}

if ( feature.isValid() )
{
delete evalContext->popScope();
}

return val;
}

void QgsVectorLayer::setDefaultValueExpression( int index, const QString& expression )
{
if ( expression.isEmpty() )
{
mDefaultExpressionMap.remove( index );
}
else
{
mDefaultExpressionMap.insert( index, expression );
}
}

QString QgsVectorLayer::defaultValueExpression( int index ) const
{
return mDefaultExpressionMap.value( index, QString() );
}

void QgsVectorLayer::uniqueValues( int index, QList<QVariant> &uniqueValues, int limit )
{
uniqueValues.clear();
Expand Down
39 changes: 39 additions & 0 deletions src/core/qgsvectorlayer.h
Expand Up @@ -1707,6 +1707,42 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer
/** Caches joined attributes if required (and not already done) */
void createJoinCaches();

/** Returns the calculated default value for the specified field index. The default
* value may be taken from a client side default value expression (see setDefaultValueExpression())
* or taken from the underlying data provider.
* @param index field index
* @param feature optional feature to use for default value evaluation. If passed,
* then properties from the feature (such as geometry) can be used when calculating
* the default value.
* @param context optional expression context to evaluate expressions again. If not
* specified, a default context will be created
* @return calculated default value
* @note added in QGIS 3.0
* @see setDefaultValueExpression()
*/
QVariant defaultValue( int index, const QgsFeature& feature = QgsFeature(),
QgsExpressionContext* context = nullptr ) const;

/** Sets an expression to use when calculating the default value for a field.
* @param index field index
* @param expression expression to evaluate when calculating default values for field. Pass
* an empty expression to clear the default.
* @note added in QGIS 3.0
* @see defaultValue()
* @see defaultValueExpression()
*/
void setDefaultValueExpression( int index, const QString& expression );

/** Returns the expression used when calculating the default value for a field.
* @param index field index
* @returns expression evaluated when calculating default values for field, or an
* empty string if no default is set
* @note added in QGIS 3.0
* @see defaultValue()
* @see setDefaultValueExpression()
*/
QString defaultValueExpression( int index ) const;

/** 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 @@ -2173,6 +2209,9 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer
/** Map that stores the aliases for attributes. Key is the attribute name and value the alias for that attribute*/
QMap< QString, QString > mAttributeAliasMap;

//! Map which stores default value expressions for fields
QMap< int, QString > mDefaultExpressionMap;

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

Expand Down
95 changes: 94 additions & 1 deletion tests/src/python/test_qgsvectorlayer.py
Expand Up @@ -18,6 +18,7 @@

from qgis.PyQt.QtCore import QVariant
from qgis.PyQt.QtGui import QPainter
from qgis.PyQt.QtXml import (QDomDocument, QDomElement)

from qgis.core import (QGis,
QgsVectorLayer,
Expand All @@ -35,7 +36,11 @@
QgsCoordinateReferenceSystem,
QgsProject,
QgsUnitTypes,
QgsAggregateCalculator)
QgsAggregateCalculator,
QgsPointV2,
QgsExpressionContext,
QgsExpressionContextScope,
QgsExpressionContextUtils)
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
start_app()
Expand Down Expand Up @@ -1455,6 +1460,94 @@ def test_setRendererV2(self):
self.assertTrue(self.rendererChanged)
self.assertEqual(layer.rendererV2(), r)

def testGetSetDefaults(self):
""" test getting and setting default expressions """
layer = createLayerWithOnePoint()

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

layer.setDefaultValueExpression(0, "'test'")
self.assertEqual(layer.defaultValueExpression(0), "'test'")
self.assertFalse(layer.defaultValueExpression(1))
self.assertFalse(layer.defaultValueExpression(2))

layer.setDefaultValueExpression(1, "2+2")
self.assertEqual(layer.defaultValueExpression(0), "'test'")
self.assertEqual(layer.defaultValueExpression(1), "2+2")
self.assertFalse(layer.defaultValueExpression(2))

def testSaveRestoreDefaults(self):
""" test saving and restoring default expressions from xml"""
layer = createLayerWithOnePoint()

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

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

# set some default expressions
layer.setDefaultValueExpression(0, "'test'")
layer.setDefaultValueExpression(1, "2+2")

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

layer3 = createLayerWithOnePoint()
self.assertTrue(layer3.readXml(elem))
self.assertEqual(layer3.defaultValueExpression(0), "'test'")
self.assertEqual(layer3.defaultValueExpression(1), "2+2")

def testEvaluatingDefaultExpressions(self):
""" tests calculation of default values"""
layer = createLayerWithOnePoint()
layer.setDefaultValueExpression(0, "'test'")
layer.setDefaultValueExpression(1, "2+2")
self.assertEqual(layer.defaultValue(0), 'test')
self.assertEqual(layer.defaultValue(1), 4)

# using feature
layer.setDefaultValueExpression(1, '$id * 2')
feature = QgsFeature(4)
feature.setValid(True)
feature.setFields(layer.fields())
# no feature:
self.assertFalse(layer.defaultValue(1))
# with feature:
self.assertEqual(layer.defaultValue(0, feature), 'test')
self.assertEqual(layer.defaultValue(1, feature), 8)

# using feature geometry
layer.setDefaultValueExpression(1, '$x * 2')
feature.setGeometry(QgsGeometry(QgsPointV2(6, 7)))
self.assertEqual(layer.defaultValue(1, feature), 12)

# using contexts
scope = QgsExpressionContextScope()
scope.setVariable('var1', 16)
context = QgsExpressionContext()
context.appendScope(scope)
layer.setDefaultValueExpression(1, '$id + @var1')
self.assertEqual(layer.defaultValue(1, feature, context), 20)

# if no scope passed, should use a default constructed one including layer variables
QgsExpressionContextUtils.setLayerVariable(layer, 'var2', 4)
QgsExpressionContextUtils.setProjectVariable('var3', 8)
layer.setDefaultValueExpression(1, 'to_int(@var2) + to_int(@var3) + $id')
self.assertEqual(layer.defaultValue(1, feature), 16)

# bad expression
layer.setDefaultValueExpression(1, 'not a valid expression')
self.assertFalse(layer.defaultValue(1))


# TODO:
# - fetch rect: feat with changed geometry: 1. in rect, 2. out of rect
# - more join tests
Expand Down

0 comments on commit 6ed2cf2

Please sign in to comment.