Skip to content

Commit

Permalink
Merge pull request #4839 from pblottiere/joinconstraints
Browse files Browse the repository at this point in the history
[FEATURE] Constraints are resolved for joined fields
  • Loading branch information
Hugo Mercier committed Jul 25, 2017
2 parents c5371b6 + 588fe49 commit 1b9c5be
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 40 deletions.
12 changes: 12 additions & 0 deletions python/gui/editorwidgets/core/qgseditorwidgetwrapper.sip
Expand Up @@ -125,6 +125,18 @@ class QgsEditorWidgetWrapper : QgsWidgetWrapper
\param constraintOrigin optional origin for constraints to check. This can be used to limit the constraints tested
to only provider or layer based constraints.
.. versionadded:: 2.16
%End

void updateConstraint( const QgsVectorLayer *layer, int index, const QgsFeature &feature, QgsFieldConstraints::ConstraintOrigin constraintOrigin = QgsFieldConstraints::ConstraintOriginNotSet );
%Docstring
Update constraint on a feature coming from a specific layer.
\param layer The vector layer where the feature is defined
\param index The index of the field to check
\param feature The feature to use to evaluate the constraint
\param constraintOrigin Optional origin for constraints to check. This
can be used to limit the constraints tested to only provider or layer
based constraints.
.. versionadded:: 3.0
%End

bool isValidConstraint() const;
Expand Down
7 changes: 4 additions & 3 deletions src/core/qgsvectorlayerjoinbuffer.cpp
Expand Up @@ -416,10 +416,11 @@ QgsFeature QgsVectorLayerJoinBuffer::joinedFeatureOf( const QgsVectorLayerJoinIn

if ( info->joinLayer() )
{
joinedFeature.setFields( info->joinLayer()->fields() );

QString joinFieldName = info->joinFieldName();
const QVariant targetValue = feature.attribute( info->targetFieldName() );
QString fieldRef = QgsExpression::quotedColumnRef( info->joinFieldName() );
QString quotedVal = QgsExpression::quotedValue( targetValue.toString() );
const QString filter = QStringLiteral( "%1 = %2" ).arg( fieldRef, quotedVal );
QString filter = QgsExpression::createFieldEqualityExpression( joinFieldName, targetValue );

QgsFeatureRequest request;
request.setFilterExpression( filter );
Expand Down
89 changes: 57 additions & 32 deletions src/gui/editorwidgets/core/qgseditorwidgetwrapper.cpp
Expand Up @@ -18,6 +18,8 @@
#include "qgsvectordataprovider.h"
#include "qgsfields.h"
#include "qgsvectorlayerutils.h"
#include "qgsvectorlayerjoinbuffer.h"
#include "qgsvectorlayerjoininfo.h"

#include <QTableView>

Expand Down Expand Up @@ -119,53 +121,76 @@ void QgsEditorWidgetWrapper::updateConstraintWidgetStatus( ConstraintResult cons

void QgsEditorWidgetWrapper::updateConstraint( const QgsFeature &ft, QgsFieldConstraints::ConstraintOrigin constraintOrigin )
{
updateConstraint( layer(), mFieldIdx, ft, constraintOrigin );
}

void QgsEditorWidgetWrapper::updateConstraint( const QgsVectorLayer *layer, int index, const QgsFeature &ft, QgsFieldConstraints::ConstraintOrigin constraintOrigin )
{
QStringList errors;
QStringList softErrors;
QStringList expressions;
QStringList descriptions;
bool toEmit( false );
QgsField field = layer()->fields().at( mFieldIdx );
bool hardConstraintsOk( true );
bool softConstraintsOk( true );

QgsField field = layer->fields().at( index );
QString expression = field.constraints().constraintExpression();
QStringList expressions, descriptions;

if ( ! expression.isEmpty() )
if ( ft.isValid() )
{
expressions << expression;
descriptions << field.constraints().constraintDescription();
toEmit = true;
}

if ( field.constraints().constraints() & QgsFieldConstraints::ConstraintNotNull )
{
descriptions << tr( "Not NULL" );
if ( !expression.isEmpty() )
if ( ! expression.isEmpty() )
{
expressions << field.name() + QStringLiteral( " IS NOT NULL" );
expressions << expression;
descriptions << field.constraints().constraintDescription();
toEmit = true;
}
else
{
expressions << QStringLiteral( "IS NOT NULL" );
}
toEmit = true;
}

if ( field.constraints().constraints() & QgsFieldConstraints::ConstraintUnique )
{
descriptions << tr( "Unique" );
if ( !expression.isEmpty() )
if ( field.constraints().constraints() & QgsFieldConstraints::ConstraintNotNull )
{
expressions << field.name() + QStringLiteral( " IS UNIQUE" );
descriptions << tr( "Not NULL" );
if ( !expression.isEmpty() )
{
expressions << field.name() + QStringLiteral( " IS NOT NULL" );
}
else
{
expressions << QStringLiteral( "IS NOT NULL" );
}
toEmit = true;
}
else

if ( field.constraints().constraints() & QgsFieldConstraints::ConstraintUnique )
{
expressions << QStringLiteral( "IS UNIQUE" );
descriptions << tr( "Unique" );
if ( !expression.isEmpty() )
{
expressions << field.name() + QStringLiteral( " IS UNIQUE" );
}
else
{
expressions << QStringLiteral( "IS UNIQUE" );
}
toEmit = true;
}
toEmit = true;

hardConstraintsOk = QgsVectorLayerUtils::validateAttribute( layer, ft, index, errors, QgsFieldConstraints::ConstraintStrengthHard, constraintOrigin );

softConstraintsOk = QgsVectorLayerUtils::validateAttribute( layer, ft, index, softErrors, QgsFieldConstraints::ConstraintStrengthSoft, constraintOrigin );
errors << softErrors;
}
else // invalid feature
{
if ( ! expression.isEmpty() )
{
hardConstraintsOk = true;
softConstraintsOk = false;

QStringList errors;
bool hardConstraintsOk = QgsVectorLayerUtils::validateAttribute( layer(), ft, mFieldIdx, errors, QgsFieldConstraints::ConstraintStrengthHard, constraintOrigin );
errors << "Invalid feature";

QStringList softErrors;
bool softConstraintsOk = QgsVectorLayerUtils::validateAttribute( layer(), ft, mFieldIdx, softErrors, QgsFieldConstraints::ConstraintStrengthSoft, constraintOrigin );
errors << softErrors;
toEmit = true;
}
}

mValidConstraint = hardConstraintsOk && softConstraintsOk;
mIsBlockingCommit = !hardConstraintsOk;
Expand Down
12 changes: 12 additions & 0 deletions src/gui/editorwidgets/core/qgseditorwidgetwrapper.h
Expand Up @@ -138,6 +138,18 @@ class GUI_EXPORT QgsEditorWidgetWrapper : public QgsWidgetWrapper
*/
void updateConstraint( const QgsFeature &featureContext, QgsFieldConstraints::ConstraintOrigin constraintOrigin = QgsFieldConstraints::ConstraintOriginNotSet );

/**
* Update constraint on a feature coming from a specific layer.
* \param layer The vector layer where the feature is defined
* \param index The index of the field to check
* \param feature The feature to use to evaluate the constraint
* \param constraintOrigin Optional origin for constraints to check. This
* can be used to limit the constraints tested to only provider or layer
* based constraints.
* \since QGIS 3.0
*/
void updateConstraint( const QgsVectorLayer *layer, int index, const QgsFeature &feature, QgsFieldConstraints::ConstraintOrigin constraintOrigin = QgsFieldConstraints::ConstraintOriginNotSet );

/**
* Get the current constraint status.
* \returns true if the constraint is valid or if there's no constraint,
Expand Down
38 changes: 33 additions & 5 deletions src/gui/qgsattributeform.cpp
Expand Up @@ -720,17 +720,15 @@ void QgsAttributeForm::updateConstraints( QgsEditorWidgetWrapper *eww )
// to test, but they are unlikely to have any control over provider-side constraints
// 2. the provider has already accepted the value, so presumably it doesn't violate the constraint
// and there's no point rechecking!
QgsFieldConstraints::ConstraintOrigin constraintOrigin = mLayer->isEditable() ? QgsFieldConstraints::ConstraintOriginNotSet
: QgsFieldConstraints::ConstraintOriginLayer;

// update eww constraint
eww->updateConstraint( ft, constraintOrigin );
updateConstraint( ft, eww );

// update eww dependencies constraint
QList<QgsEditorWidgetWrapper *> deps = constraintDependencies( eww );

Q_FOREACH ( QgsEditorWidgetWrapper *depsEww, deps )
depsEww->updateConstraint( ft, constraintOrigin );
updateConstraint( ft, depsEww );

// sync OK button status
synchronizeEnabledState();
Expand All @@ -745,6 +743,34 @@ void QgsAttributeForm::updateConstraints( QgsEditorWidgetWrapper *eww )
}
}

void QgsAttributeForm::updateConstraint( const QgsFeature &ft, QgsEditorWidgetWrapper *eww )
{
QgsFieldConstraints::ConstraintOrigin constraintOrigin = mLayer->isEditable() ? QgsFieldConstraints::ConstraintOriginNotSet : QgsFieldConstraints::ConstraintOriginLayer;

if ( eww->layer()->fields().fieldOrigin( eww->fieldIdx() ) == QgsFields::OriginJoin )
{
int srcFieldIdx;
const QgsVectorLayerJoinInfo *info = eww->layer()->joinBuffer()->joinForFieldIndex( eww->fieldIdx(), eww->layer()->fields(), srcFieldIdx );

if ( info && info->joinLayer() && info->isDynamicFormEnabled() )
{
if ( mJoinedFeatures.contains( info ) )
{
eww->updateConstraint( info->joinLayer(), srcFieldIdx, mJoinedFeatures[info], constraintOrigin );
return;
}
else // if we are here, it means there's not joined field for this feature
{
eww->updateConstraint( QgsFeature() );
return;
}
}
}

// default constraint update
eww->updateConstraint( ft, constraintOrigin );
}

bool QgsAttributeForm::currentFormFeature( QgsFeature &feature )
{
bool rc = true;
Expand All @@ -765,7 +791,7 @@ bool QgsAttributeForm::currentFormFeature( QgsFeature &feature )
QVariant srcVar = eww->value();
// need to check dstVar.isNull() != srcVar.isNull()
// otherwise if dstVar=NULL and scrVar=0, then dstVar = srcVar
if ( ( dstVar != srcVar || dstVar.isNull() != srcVar.isNull() ) && srcVar.isValid() && !mLayer->editFormConfig().readOnly( eww->fieldIdx() ) )
if ( ( dstVar != srcVar || dstVar.isNull() != srcVar.isNull() ) && srcVar.isValid() )
dst[eww->fieldIdx()] = srcVar;
}
else
Expand Down Expand Up @@ -1954,6 +1980,8 @@ void QgsAttributeForm::updateJoinedFields( const QgsEditorWidgetWrapper &eww )

QgsFeature joinFeature = mLayer->joinBuffer()->joinedFeatureOf( info, formFeature );

mJoinedFeatures[info] = joinFeature;

QStringList *subsetFields = info->joinFieldNamesSubset();
if ( subsetFields )
{
Expand Down
2 changes: 2 additions & 0 deletions src/gui/qgsattributeform.h
Expand Up @@ -318,6 +318,7 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
//! constraints management
void updateAllConstraints();
void updateConstraints( QgsEditorWidgetWrapper *w );
void updateConstraint( const QgsFeature &ft, QgsEditorWidgetWrapper *eww );
bool currentFormFeature( QgsFeature &feature );
bool currentFormValidConstraints( QStringList &invalidFields, QStringList &descriptions );
QList<QgsEditorWidgetWrapper *> constraintDependencies( QgsEditorWidgetWrapper *w );
Expand All @@ -340,6 +341,7 @@ class GUI_EXPORT QgsAttributeForm : public QWidget
QList<QgsAttributeFormInterface *> mInterfaces;
QMap< int, QgsAttributeFormEditorWidget * > mFormEditorWidgets;
QgsExpressionContext mExpressionContext;
QMap<const QgsVectorLayerJoinInfo *, QgsFeature> mJoinedFeatures;

struct ContainerInformation
{
Expand Down
71 changes: 71 additions & 0 deletions tests/src/gui/testqgsattributeform.cpp
Expand Up @@ -43,6 +43,7 @@ class TestQgsAttributeForm : public QObject
void testFieldMultiConstraints();
void testOKButtonStatus();
void testDynamicForm();
void testConstraintsOnJoinedFields();
};

void TestQgsAttributeForm::initTestCase()
Expand Down Expand Up @@ -440,5 +441,75 @@ void TestQgsAttributeForm::testDynamicForm()
delete layerC;
}

void TestQgsAttributeForm::testConstraintsOnJoinedFields()
{
QString validLabel = QStringLiteral( "col0<font color=\"green\">✔</font>" );
QString warningLabel = QStringLiteral( "col0<font color=\"orange\">✘</font>" );

// make temporary layers
QString defA = QStringLiteral( "Point?field=id_a:integer" );
QgsVectorLayer *layerA = new QgsVectorLayer( defA, QStringLiteral( "layerA" ), QStringLiteral( "memory" ) );

QString defB = QStringLiteral( "Point?field=id_b:integer&field=col0:integer" );
QgsVectorLayer *layerB = new QgsVectorLayer( defB, QStringLiteral( "layerB" ), QStringLiteral( "memory" ) );

// set constraints on joined layer
layerB->setConstraintExpression( 1, QStringLiteral( "col0 < 10" ) );
layerB->setFieldConstraint( 1, QgsFieldConstraints::ConstraintExpression, QgsFieldConstraints::ConstraintStrengthSoft );

// join configuration
QgsVectorLayerJoinInfo infoJoinAB;
infoJoinAB.setTargetFieldName( "id_a" );
infoJoinAB.setJoinLayer( layerB );
infoJoinAB.setJoinFieldName( "id_b" );
infoJoinAB.setDynamicFormEnabled( true );

layerA->addJoin( infoJoinAB );

// add features for main layer
QgsFeature ftA( layerA->fields() );
ftA.setAttribute( QStringLiteral( "id_a" ), 1 );
layerA->startEditing();
layerA->addFeature( ftA );
layerA->commitChanges();

// add features for joined layer
QgsFeature ft0B( layerB->fields() );
ft0B.setAttribute( QStringLiteral( "id_b" ), 30 );
ft0B.setAttribute( QStringLiteral( "col0" ), 9 );
layerB->startEditing();
layerB->addFeature( ft0B );
layerB->commitChanges();

QgsFeature ft1B( layerB->fields() );
ft1B.setAttribute( QStringLiteral( "id_b" ), 31 );
ft1B.setAttribute( QStringLiteral( "col0" ), 11 );
layerB->startEditing();
layerB->addFeature( ft1B );
layerB->commitChanges();

// build a form for this feature
QgsAttributeForm form( layerA );
form.setMode( QgsAttributeForm::AddFeatureMode );
form.setFeature( ftA );

// change layerA join id field
form.changeAttribute( "id_a", QVariant( 30 ) );

// compare
QgsEditorWidgetWrapper *ww = nullptr;
ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
QLabel *label = form.mBuddyMap.value( ww->widget() );
QCOMPARE( label->text(), "layerB_" + validLabel );

// change layerA join id field
form.changeAttribute( "id_a", QVariant( 31 ) );

// compare
ww = qobject_cast<QgsEditorWidgetWrapper *>( form.mWidgets[1] );
label = form.mBuddyMap.value( ww->widget() );
QCOMPARE( label->text(), "layerB_" + warningLabel );
}

QGSTEST_MAIN( TestQgsAttributeForm )
#include "testqgsattributeform.moc"

0 comments on commit 1b9c5be

Please sign in to comment.