Skip to content
Permalink
Browse files

[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()
  • Loading branch information
nyalldawson committed Aug 30, 2016
1 parent 7dea970 commit 4d5bae22b8af6ca77fc128c6a196b38af7d8b572
@@ -1202,6 +1202,42 @@ class QgsVectorLayer : QgsMapLayer
// marked as const as these are just caches, and need to be created from const accessors
void createJoinCaches() const;

/** 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
@@ -78,14 +78,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
@@ -1436,6 +1436,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 ) );

@@ -1617,6 +1639,26 @@ bool QgsVectorLayer::writeXml( QDomNode & layer_node,
}
layer_node.appendChild( dependenciesElement );

//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
@@ -2799,6 +2841,69 @@ void QgsVectorLayer::createJoinCaches() const
}
}

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 ) const
{
uniqueValues.clear();
@@ -1331,6 +1331,42 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
// marked as const as these are just caches, and need to be created from const accessors
void createJoinCaches() const;

/** 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
@@ -1865,6 +1901,9 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
/** 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;

@@ -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,
QgsWkbTypes,
@@ -36,7 +37,11 @@
QgsCoordinateReferenceSystem,
QgsProject,
QgsUnitTypes,
QgsAggregateCalculator)
QgsAggregateCalculator,
QgsPointV2,
QgsExpressionContext,
QgsExpressionContextScope,
QgsExpressionContextUtils)
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
start_app()
@@ -1602,6 +1607,94 @@ def test_setRenderer(self):
self.assertTrue(self.rendererChanged)
self.assertEqual(layer.renderer(), 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

0 comments on commit 4d5bae2

Please sign in to comment.
You can’t perform that action at this time.