Skip to content

Commit

Permalink
Merge pull request #31167 from signedav/default-values-update
Browse files Browse the repository at this point in the history
Update widgets with default values on attribute form
  • Loading branch information
m-kuhn committed Aug 16, 2019
2 parents bdf18ee + 556551b commit 3245376
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 9 deletions.
3 changes: 2 additions & 1 deletion python/core/auto_generated/qgis.sip.in
Expand Up @@ -194,7 +194,8 @@ Invalid < NULL < Values

bool qgsVariantEqual( const QVariant &lhs, const QVariant &rhs );
%Docstring
Compares two QVariant values and returns whether they are equal, NULL values are treated as equal.
Compares two QVariant values and returns whether they are equal, two NULL values are
always treated as equal and 0 is not treated as equal with NULL

:param lhs: first value
:param rhs: second value
Expand Down
2 changes: 1 addition & 1 deletion src/core/qgis.cpp
Expand Up @@ -297,5 +297,5 @@ uint qHash( const QVariant &variant )

bool qgsVariantEqual( const QVariant &lhs, const QVariant &rhs )
{
return lhs.isNull() == rhs.isNull() && lhs == rhs;
return ( lhs.isNull() == rhs.isNull() && lhs == rhs ) || ( lhs.isNull() && rhs.isNull() && lhs.isValid() && rhs.isValid() );
}
3 changes: 2 additions & 1 deletion src/core/qgis.h
Expand Up @@ -503,7 +503,8 @@ CORE_EXPORT qlonglong qgsPermissiveToLongLong( QString string, bool &ok );
CORE_EXPORT bool qgsVariantLessThan( const QVariant &lhs, const QVariant &rhs );

/**
* Compares two QVariant values and returns whether they are equal, NULL values are treated as equal.
* Compares two QVariant values and returns whether they are equal, two NULL values are
* always treated as equal and 0 is not treated as equal with NULL
*
* \param lhs first value
* \param rhs second value
Expand Down
91 changes: 85 additions & 6 deletions src/gui/qgsattributeform.cpp
Expand Up @@ -41,6 +41,7 @@
#include "qgshtmlwidgetwrapper.h"
#include "qgsapplication.h"
#include "qgsexpressioncontextutils.h"
#include "qgsfeaturerequest.h"

#include <QDir>
#include <QTextStream>
Expand Down Expand Up @@ -318,12 +319,7 @@ bool QgsAttributeForm::saveEdits()
QVariant dstVar = dst.at( eww->fieldIdx() );
QVariant srcVar = eww->value();

// need to check dstVar.isNull() != srcVar.isNull()
// otherwise if dstVar=NULL and scrVar=0, then dstVar = srcVar
// be careful- sometimes two null qvariants will be reported as not equal!! (e.g., different types)
bool changed = ( dstVar != srcVar && !dstVar.isNull() && !srcVar.isNull() )
|| ( dstVar.isNull() != srcVar.isNull() );
if ( changed && srcVar.isValid() && fieldIsEditable( eww->fieldIdx() ) )
if ( !qgsVariantEqual( dstVar, srcVar ) && srcVar.isValid() && fieldIsEditable( eww->fieldIdx() ) )
{
dst[eww->fieldIdx()] = srcVar;

Expand Down Expand Up @@ -418,6 +414,57 @@ bool QgsAttributeForm::saveEdits()
return success;
}

bool QgsAttributeForm::updateDefaultValues( const int originIdx )
{
if ( !mDefaultValueDependencies.contains( originIdx ) )
return false;

// create updated Feature
QgsFeature updatedFeature = QgsFeature( mFeature );
if ( mFeature.isValid() || mMode == QgsAttributeEditorContext::AddFeatureMode )
{
QgsAttributes dst = mFeature.attributes();
for ( QgsWidgetWrapper *ww : qgis::as_const( mWidgets ) )
{
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
if ( eww )
{
QVariant dstVar = dst.at( eww->fieldIdx() );
QVariant srcVar = eww->value();

if ( !qgsVariantEqual( dstVar, srcVar ) && srcVar.isValid() && fieldIsEditable( eww->fieldIdx() ) )
{
dst[eww->fieldIdx()] = srcVar;
}
}
}
updatedFeature.setAttributes( dst );

// go through depending fields and update the fields with defaultexpression
QList<QgsWidgetWrapper *> relevantWidgets = mDefaultValueDependencies.values( originIdx );
for ( QgsWidgetWrapper *ww : qgis::as_const( relevantWidgets ) )
{
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
if ( eww )
{
//do not update when when mMode is not AddFeatureMode and it's not applyOnUpdate
if ( mMode != QgsAttributeEditorContext::AddFeatureMode && !eww->field().defaultValueDefinition().applyOnUpdate() )
{
continue;
}

//do not update when this widget is already updating (avoid recursions)
if ( mAlreadyUpdatedFields.contains( eww->fieldIdx() ) )
continue;

QString value = mLayer->defaultValue( eww->fieldIdx(), updatedFeature ).toString();
eww->setValue( value );
}
}
}
return true;
}

void QgsAttributeForm::resetMultiEdit( bool promptToSave )
{
if ( promptToSave )
Expand Down Expand Up @@ -772,6 +819,11 @@ void QgsAttributeForm::onAttributeChanged( const QVariant &value )

updateConstraints( eww );

//append field index here, so it's not updated recursive
mAlreadyUpdatedFields.append( eww->fieldIdx() );
updateDefaultValues( eww->fieldIdx() );
mAlreadyUpdatedFields.removeAll( eww->fieldIdx() );

if ( !signalEmitted )
{
Q_NOWARN_DEPRECATED_PUSH
Expand Down Expand Up @@ -1417,6 +1469,33 @@ void QgsAttributeForm::init()
}
}

//create defaultValueDependencies
for ( QgsWidgetWrapper *ww : qgis::as_const( mWidgets ) )
{
QgsEditorWidgetWrapper *eww = qobject_cast<QgsEditorWidgetWrapper *>( ww );
if ( eww )
{
QgsExpression exp( eww->field().defaultValueDefinition().expression() );
const QSet<QString> referencedColumns = exp.referencedColumns();
for ( const QString &referencedColumn : referencedColumns )
{
if ( referencedColumn == QgsFeatureRequest::ALL_ATTRIBUTES )
{
const QList<int> allAttributeIds( mLayer->fields().allAttributesList() );

for ( const int id : allAttributeIds )
{
mDefaultValueDependencies.insertMulti( id, eww );
}
}
else
{
mDefaultValueDependencies.insertMulti( mLayer->fields().lookupField( referencedColumn ), eww );
}
}
}
}

if ( !mButtonBox )
{
mButtonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
Expand Down
15 changes: 15 additions & 0 deletions src/gui/qgsattributeform.h
Expand Up @@ -352,6 +352,12 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
//! Save single feature or add feature edits
bool saveEdits();

//! fill up dependency map for default values
void createDefaultValueDependencies();

//! update the default values in the fields after a referenced field changed
bool updateDefaultValues( const int originIdx );

int messageTimeout();
void clearMultiEditMessages();
void pushSelectedFeaturesMessage();
Expand Down Expand Up @@ -444,6 +450,15 @@ class GUI_EXPORT QgsAttributeForm : public QWidget

QMap<QWidget *, QSvgWidget *> mIconMap;

/**
* Dependency map for default values. Attribute index -> widget wrapper.
* Attribute indexes will be added multiple times if more than one widget depends on them.
*/
QMap<int, QgsWidgetWrapper *> mDefaultValueDependencies;

//! List of updated fields to avoid recursion on the setting of defaultValues
QList<int> mAlreadyUpdatedFields;

friend class TestQgsDualView;
friend class TestQgsAttributeForm;
};
Expand Down
5 changes: 5 additions & 0 deletions tests/src/core/testqgis.cpp
Expand Up @@ -393,6 +393,11 @@ void TestQgis::testQgsVariantEqual()
// NULL identities
QVERIFY( qgsVariantEqual( QVariant( QVariant::Int ), QVariant( QVariant::Int ) ) );
QVERIFY( qgsVariantEqual( QVariant( QVariant::Double ), QVariant( QVariant::Double ) ) );
QVERIFY( qgsVariantEqual( QVariant( QVariant::Int ), QVariant( QVariant::Double ) ) );
QVERIFY( qgsVariantEqual( QVariant( QVariant::Int ), QVariant( QVariant::String ) ) );

// NULL should not be equal to invalid
QVERIFY( !qgsVariantEqual( QVariant(), QVariant( QVariant::Int ) ) );
}

void TestQgis::testQgsEnumValueToKey()
Expand Down
136 changes: 136 additions & 0 deletions tests/src/gui/testqgsattributeform.cpp
Expand Up @@ -50,6 +50,8 @@ class TestQgsAttributeForm : public QObject
void testEditableJoin();
void testUpsertOnEdit();
void testAttributeFormInterface();
void testDefaultValueUpdate();
void testDefaultValueUpdateRecursion();

private:
QLabel *constraintsLabel( QgsAttributeForm *form, QgsEditorWidgetWrapper *ww )
Expand Down Expand Up @@ -906,6 +908,140 @@ void TestQgsAttributeForm::testAttributeFormInterface()
}


void TestQgsAttributeForm::testDefaultValueUpdate()
{
// make a temporary layer to check through
QString def = QStringLiteral( "Point?field=col0:integer&field=col1:integer&field=col2:integer&field=col3:integer" );
QgsVectorLayer *layer = new QgsVectorLayer( def, QStringLiteral( "test" ), QStringLiteral( "memory" ) );

//set defaultValueDefinitions
//col0 - no default value
//col1 - "col0"+1
//col2 - "col0"+"col1"
//col3 - "col2"

// set constraints for each field
layer->setDefaultValueDefinition( 1, QgsDefaultValue( QStringLiteral( "\"col0\"+1" ) ) );
layer->setDefaultValueDefinition( 2, QgsDefaultValue( QStringLiteral( "\"col0\"+\"col1\"" ) ) );
layer->setDefaultValueDefinition( 3, QgsDefaultValue( QStringLiteral( "\"col2\"" ) ) );

// build a form for this feature
QgsFeature ft( layer->dataProvider()->fields(), 1 );
ft.setAttribute( QStringLiteral( "col0" ), 0 );
QgsAttributeForm form( layer );
form.setMode( QgsAttributeEditorContext::AddFeatureMode );
form.setFeature( ft );

// get wrappers for each widget
QgsEditorWidgetWrapper *ww0, *ww1, *ww2, *ww3;
ww0 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
ww1 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
ww2 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[2] );
ww3 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[3] );

//set value in col0:
ww0->setValue( 5 );

//we expect
//col0 - 5
//col1 - 6
//col2 - 11
//col3 - 11

QCOMPARE( ww0->value().toInt(), 5 );
QCOMPARE( ww1->value().toInt(), 6 );
QCOMPARE( ww2->value().toInt(), 11 );
QCOMPARE( ww3->value().toInt(), 11 );

//set value in col1:
ww1->setValue( 10 );

//we expect
//col0 - 5
//col1 - 10
//col2 - 15
//col3 - 15

QCOMPARE( ww0->value().toInt(), 5 );
QCOMPARE( ww1->value().toInt(), 10 );
QCOMPARE( ww2->value().toInt(), 15 );
QCOMPARE( ww3->value().toInt(), 15 );
}

void TestQgsAttributeForm::testDefaultValueUpdateRecursion()
{
// make a temporary layer to check through
QString def = QStringLiteral( "Point?field=col0:integer&field=col1:integer&field=col2:integer&field=col3:integer" );
QgsVectorLayer *layer = new QgsVectorLayer( def, QStringLiteral( "test" ), QStringLiteral( "memory" ) );

//let's make a recursion
//col0 - COALESCE( 0, "col3"+1)
//col1 - COALESCE( 0, "col0"+1)
//col2 - COALESCE( 0, "col1"+1)
//col3 - COALESCE( 0, "col2"+1)

// set constraints for each field
layer->setDefaultValueDefinition( 0, QgsDefaultValue( QStringLiteral( "\"col3\"+1" ) ) );
layer->setDefaultValueDefinition( 1, QgsDefaultValue( QStringLiteral( "\"col0\"+1" ) ) );
layer->setDefaultValueDefinition( 2, QgsDefaultValue( QStringLiteral( "\"col1\"+1" ) ) );
layer->setDefaultValueDefinition( 3, QgsDefaultValue( QStringLiteral( "\"col2\"+1" ) ) );

// build a form for this feature
QgsFeature ft( layer->dataProvider()->fields(), 1 );
ft.setAttribute( QStringLiteral( "col0" ), 0 );
QgsAttributeForm form( layer );
form.setMode( QgsAttributeEditorContext::AddFeatureMode );
form.setFeature( ft );

// get wrappers for each widget
QgsEditorWidgetWrapper *ww0, *ww1, *ww2, *ww3;
ww0 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[0] );
ww1 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
ww2 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[2] );
ww3 = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[3] );

//set value in col0:
ww0->setValue( 20 );

//we expect
//col0 - 20
//col1 - 21
//col2 - 22
//col3 - 23

QCOMPARE( ww0->value().toInt(), 20 );
QCOMPARE( ww1->value().toInt(), 21 );
QCOMPARE( ww2->value().toInt(), 22 );
QCOMPARE( ww3->value().toInt(), 23 );

//set value in col2:
ww2->setValue( 30 );

//we expect
//col0 - 32
//col1 - 33
//col2 - 30
//col3 - 31

QCOMPARE( ww0->value().toInt(), 32 );
QCOMPARE( ww1->value().toInt(), 33 );
QCOMPARE( ww2->value().toInt(), 30 );
QCOMPARE( ww3->value().toInt(), 31 );

//set value in col0 again:
ww0->setValue( 40 );

//we expect
//col0 - 40
//col1 - 41
//col2 - 42
//col3 - 43

QCOMPARE( ww0->value().toInt(), 40 );
QCOMPARE( ww1->value().toInt(), 41 );
QCOMPARE( ww2->value().toInt(), 42 );
QCOMPARE( ww3->value().toInt(), 43 );
}

QGSTEST_MAIN( TestQgsAttributeForm )
#include "testqgsattributeform.moc"

0 comments on commit 3245376

Please sign in to comment.