Merge pull request #51309 from nirvn/validity_table

[attributes table] Add constraint-based conditional styling and failing constraints feature filter
This commit is contained in:
Mathieu Pellerin 2023-01-17 09:24:42 +07:00 committed by GitHub
commit 8e12541dfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 306 additions and 46 deletions

View File

@ -369,6 +369,7 @@
<file>themes/default/mActionOpenTable.svg</file>
<file>themes/default/mActionOpenTableEdited.svg</file>
<file>themes/default/mActionOpenTableSelected.svg</file>
<file>themes/default/mActionOpenTableInvalid.svg</file>
<file>themes/default/mActionOpenTableVisible.svg</file>
<file>themes/default/mActionAddTable.svg</file>
<file>themes/default/mActionOffsetPointSymbols.svg</file>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><g stroke="#6e97c4"><path d="m2.5 7.5h19v14h-19z" fill="#e6e6e6"/><path d="m2.5 2.5h19v5h-19z" fill="#bad9ec"/><path d="m5 5h4" fill="none" stroke-width="2"/><path d="m11 5h8" fill="none" stroke-width="2"/></g><g transform="translate(0,-8)" fill="none" stroke="#6e97c4"><path d="m5 18.7h4"/><path d="m11 18.7h8" stroke="#6e97c4"/><path d="m5 21.7h4"/><path d="m11 21.7h8"/><path d="m5 24.7h4" stroke="#6e97c4"/><path d="m11 24.7h8"/></g><text transform="scale(1 .99998)" x="7.3536935" y="22.897999" fill="#fe9800" font-family="'Wide Latin'" font-size="21.505px" stroke-width=".53763" style="line-height:1.25" xml:space="preserve"><tspan x="7.3536935" y="22.897999" stroke-width=".53763"></tspan></text></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -8,8 +8,8 @@
typedef QList<QgsConditionalStyle> QgsConditionalStyles;
typedef QList<QgsConditionalStyle> QgsConditionalStyles;
class QgsConditionalLayerStyles : QObject
{
@ -43,12 +43,24 @@ Each row will check be checked against each rule.
.. seealso:: :py:func:`rowStyles`
.. versionadded:: 2.12
%End
QgsConditionalStyle constraintFailureStyles( QgsFieldConstraints::ConstraintStrength strength );
%Docstring
Returns a style associated to a constraint failure.
:param strength: the type of constraint
.. versionadded:: 3.30
%End
void setFieldStyles( const QString &fieldName, const QList<QgsConditionalStyle> &styles );
%Docstring
Set the conditional ``styles`` for a field, with the specified ``fieldName``.
The field value is inserted into a 'value' variable to conduct
expression checks.
.. seealso:: :py:func:`fieldStyles`
%End

View File

@ -168,6 +168,16 @@ be unique within regard to ``existingValues``.
The optional seed value can be used as a basis for generated values.
.. versionadded:: 3.6
%End
static bool attributeHasConstraints( const QgsVectorLayer *layer, int attributeIndex );
%Docstring
Returns ``True`` if a feature attribute has active constraints.
:param layer: the vector layer from which field constraints will be checked for
:param attributeIndex: the attribute index
.. versionadded:: 3.30
%End
static bool validateAttribute( const QgsVectorLayer *layer, const QgsFeature &feature, int attributeIndex, QStringList &errors /Out/,

View File

@ -24,7 +24,8 @@ class QgsAttributeTableFilterModel: QSortFilterProxyModel, QgsFeatureModel
ShowSelected,
ShowVisible,
ShowFilteredList,
ShowEdited
ShowEdited,
ShowInvalid,
};
enum ColumnType

View File

@ -247,6 +247,22 @@ Any extra columns need to be implemented by proxy models in front of this model.
%Docstring
Empty extra columns to announce from this model.
Any extra columns need to be implemented by proxy models in front of this model.
%End
bool showValidityState() const;
%Docstring
Returns whether the attribute table will add a visual feedback to cells when an attribute
constraint is not met.
.. versionadded:: 3.30
%End
void setShowValidityState( bool show );
%Docstring
Sets whether the attribute table will add a visual feedback to cells when an attribute constraint
is not met.
.. versionadded:: 3.30
%End
public slots:

View File

@ -23,7 +23,35 @@
QgsConditionalLayerStyles::QgsConditionalLayerStyles( QObject *parent )
: QObject( parent )
{}
{
}
QgsConditionalStyle QgsConditionalLayerStyles::constraintFailureStyles( QgsFieldConstraints::ConstraintStrength strength )
{
switch ( strength )
{
case QgsFieldConstraints::ConstraintStrengthHard:
{
QgsConditionalStyle hardConstraintFailureStyle;
hardConstraintFailureStyle.setBackgroundColor( QColor( 255, 152, 0 ) );
hardConstraintFailureStyle.setTextColor( QColor( 0, 0, 0 ) );
return hardConstraintFailureStyle;
}
case QgsFieldConstraints::ConstraintStrengthSoft:
{
QgsConditionalStyle softConstraintFailureStyle;
softConstraintFailureStyle.setBackgroundColor( QColor( 255, 191, 12 ) );
softConstraintFailureStyle.setTextColor( QColor( 0, 0, 0 ) );
return softConstraintFailureStyle;
}
case QgsFieldConstraints::ConstraintStrengthNotSet:
return QgsConditionalStyle();
}
return QgsConditionalStyle();
}
QgsConditionalStyles QgsConditionalLayerStyles::rowStyles() const
{
@ -56,13 +84,13 @@ QList<QgsConditionalStyle> QgsConditionalLayerStyles::fieldStyles( const QString
bool QgsConditionalLayerStyles::writeXml( QDomNode &node, QDomDocument &doc, const QgsReadWriteContext &context ) const
{
QDomElement stylesel = doc.createElement( QStringLiteral( "conditionalstyles" ) );
QDomElement rowel = doc.createElement( QStringLiteral( "rowstyles" ) );
const auto constMRowStyles = mRowStyles;
for ( const QgsConditionalStyle &style : constMRowStyles )
{
style.writeXml( rowel, doc, context );
}
stylesel.appendChild( rowel );
QDomElement fieldsel = doc.createElement( QStringLiteral( "fieldstyles" ) );
@ -79,7 +107,6 @@ bool QgsConditionalLayerStyles::writeXml( QDomNode &node, QDomDocument &doc, con
}
fieldsel.appendChild( fieldel );
}
stylesel.appendChild( fieldsel );
node.appendChild( stylesel );
@ -100,9 +127,11 @@ bool QgsConditionalLayerStyles::rulesNeedGeometry() const
bool QgsConditionalLayerStyles::readXml( const QDomNode &node, const QgsReadWriteContext &context )
{
const QDomElement condel = node.firstChildElement( QStringLiteral( "conditionalstyles" ) );
mRowStyles.clear();
mFieldStyles.clear();
const QDomElement condel = node.firstChildElement( QStringLiteral( "conditionalstyles" ) );
const QDomElement rowstylesel = condel.firstChildElement( QStringLiteral( "rowstyles" ) );
QDomNodeList nodelist = rowstylesel.toElement().elementsByTagName( QStringLiteral( "style" ) );
for ( int i = 0; i < nodelist.count(); i++ )

View File

@ -16,6 +16,8 @@
#define QGSCONDITIONALSTYLE_H
#include "qgis_core.h"
#include "qgsfield.h"
#include <QObject>
#include <QFont>
#include <QColor>
@ -32,7 +34,6 @@ class QgsSymbol;
typedef QList<QgsConditionalStyle> QgsConditionalStyles;
/**
* \ingroup core
* \brief The QgsConditionalLayerStyles class holds conditional style information
@ -65,9 +66,19 @@ class CORE_EXPORT QgsConditionalLayerStyles : public QObject
*/
void setRowStyles( const QgsConditionalStyles &styles );
/**
* Returns a style associated to a constraint failure.
* \param strength the type of constraint
* \since QGIS 3.30
*/
QgsConditionalStyle constraintFailureStyles( QgsFieldConstraints::ConstraintStrength strength );
/**
* Set the conditional \a styles for a field, with the specified \a fieldName.
*
* The field value is inserted into a 'value' variable to conduct
* expression checks.
*
* \see fieldStyles()
*/
void setFieldStyles( const QString &fieldName, const QList<QgsConditionalStyle> &styles );

View File

@ -367,6 +367,20 @@ QVariant QgsVectorLayerUtils::createUniqueValueFromCache( const QgsVectorLayer *
}
bool QgsVectorLayerUtils::attributeHasConstraints( const QgsVectorLayer *layer, int attributeIndex )
{
if ( !layer )
return false;
if ( attributeIndex < 0 || attributeIndex >= layer->fields().count() )
return false;
const QgsFieldConstraints constraints = layer->fields().at( attributeIndex ).constraints();
return ( constraints.constraints() & QgsFieldConstraints::ConstraintNotNull ||
constraints.constraints() & QgsFieldConstraints::ConstraintUnique ||
constraints.constraints() & QgsFieldConstraints::ConstraintExpression );
}
bool QgsVectorLayerUtils::validateAttribute( const QgsVectorLayer *layer, const QgsFeature &feature, int attributeIndex, QStringList &errors,
QgsFieldConstraints::ConstraintStrength strength, QgsFieldConstraints::ConstraintOrigin origin )
{

View File

@ -183,6 +183,14 @@ class CORE_EXPORT QgsVectorLayerUtils
*/
static QVariant createUniqueValueFromCache( const QgsVectorLayer *layer, int fieldIndex, const QSet<QVariant> &existingValues, const QVariant &seed = QVariant() );
/**
* Returns TRUE if a feature attribute has active constraints.
* \param layer the vector layer from which field constraints will be checked for
* \param attributeIndex the attribute index
* \since QGIS 3.30
*/
static bool attributeHasConstraints( const QgsVectorLayer *layer, int attributeIndex );
/**
* Tests a feature attribute value to check whether it passes all constraints which are present on the corresponding field.
* Returns TRUE if the attribute value is valid for the field. Any constraint failures will be reported in the errors argument.

View File

@ -313,7 +313,11 @@ bool QgsAttributeTableFilterModel::selectedOnTop()
void QgsAttributeTableFilterModel::setFilteredFeatures( const QgsFeatureIds &ids )
{
mFilteredFeatures = ids;
setFilterMode( ShowFilteredList );
if ( mFilterMode != ShowFilteredList &&
mFilterMode != ShowInvalid )
{
setFilterMode( ShowFilteredList );
}
invalidateFilter();
}
@ -337,6 +341,11 @@ void QgsAttributeTableFilterModel::setFilterMode( FilterMode filterMode )
connectFilterModeConnections( filterMode );
mFilterMode = filterMode;
invalidate();
if ( mFilterMode == QgsAttributeTableFilterModel::ShowInvalid )
{
filterFeatures();
}
}
}
@ -356,6 +365,7 @@ void QgsAttributeTableFilterModel::disconnectFilterModeConnections()
case ShowSelected:
break;
case ShowFilteredList:
case ShowInvalid:
disconnect( layer(), &QgsVectorLayer::featureAdded, this, &QgsAttributeTableFilterModel::startTimedFilterFeatures );
disconnect( layer(), &QgsVectorLayer::attributeValueChanged, this, &QgsAttributeTableFilterModel::onAttributeValueChanged );
disconnect( layer(), &QgsVectorLayer::geometryChanged, this, &QgsAttributeTableFilterModel::onGeometryChanged );
@ -380,6 +390,7 @@ void QgsAttributeTableFilterModel::connectFilterModeConnections( QgsAttributeTab
case ShowSelected:
break;
case ShowFilteredList:
case ShowInvalid:
connect( layer(), &QgsVectorLayer::featureAdded, this, &QgsAttributeTableFilterModel::startTimedFilterFeatures );
connect( layer(), &QgsVectorLayer::attributeValueChanged, this, &QgsAttributeTableFilterModel::onAttributeValueChanged );
connect( layer(), &QgsVectorLayer::geometryChanged, this, &QgsAttributeTableFilterModel::onGeometryChanged );
@ -396,6 +407,7 @@ bool QgsAttributeTableFilterModel::filterAcceptsRow( int sourceRow, const QModel
return true;
case ShowFilteredList:
case ShowInvalid:
return mFilteredFeatures.contains( masterModel()->rowToId( sourceRow ) );
case ShowSelected:
@ -449,7 +461,7 @@ void QgsAttributeTableFilterModel::onAttributeValueChanged( QgsFeatureId fid, in
Q_UNUSED( fid );
Q_UNUSED( value );
if ( mFilterExpression.referencedAttributeIndexes( layer()->fields() ).contains( idx ) )
if ( mFilterMode == QgsAttributeTableFilterModel::ShowInvalid || mFilterExpression.referencedAttributeIndexes( layer()->fields() ).contains( idx ) )
{
startTimedFilterFeatures();
}
@ -457,7 +469,7 @@ void QgsAttributeTableFilterModel::onAttributeValueChanged( QgsFeatureId fid, in
void QgsAttributeTableFilterModel::onGeometryChanged()
{
if ( mFilterExpression.needsGeometry() )
if ( mFilterMode == QgsAttributeTableFilterModel::ShowInvalid || mFilterExpression.needsGeometry() )
{
startTimedFilterFeatures();
}

View File

@ -48,7 +48,8 @@ class GUI_EXPORT QgsAttributeTableFilterModel: public QSortFilterProxyModel, pub
ShowSelected, //!< Show only selected features
ShowVisible, //!< Show only visible features (depends on the map canvas)
ShowFilteredList, //!< Show only features whose ids are on the filter list. {\see setFilteredFeatures}
ShowEdited //!< Show only features which have unsaved changes
ShowEdited, //!< Show only features which have unsaved changes
ShowInvalid, //!< Show only features not respecting constraints (since QGIS 3.30)
};
Q_ENUM( FilterMode )

View File

@ -511,6 +511,7 @@ void QgsAttributeTableModel::fieldConditionalStyleChanged( const QString &fieldN
if ( fieldName.isNull() )
{
mRowStylesMap.clear();
mConstraintStylesMap.clear();
emit dataChanged( index( 0, 0 ), index( rowCount() - 1, columnCount() - 1 ) );
return;
}
@ -749,11 +750,38 @@ QVariant QgsAttributeTableModel::data( const QModelIndex &index, int role ) cons
styles = QgsConditionalStyle::matchingConditionalStyles( mLayer->conditionalStyles()->rowStyles(), QVariant(), mExpressionContext );
mRowStylesMap.insert( mFeat.id(), styles );
}
const QgsConditionalStyle rowstyle = QgsConditionalStyle::compressStyles( styles );
QgsConditionalStyle constraintstyle;
if ( mShowValidityState && QgsVectorLayerUtils::attributeHasConstraints( mLayer, fieldId ) )
{
if ( mConstraintStylesMap.contains( mFeat.id() ) &&
mConstraintStylesMap[mFeat.id()].contains( fieldId ) )
{
constraintstyle = mConstraintStylesMap[mFeat.id()][fieldId];
}
else
{
QStringList errors;
if ( !QgsVectorLayerUtils::validateAttribute( mLayer, mFeat, fieldId, errors, QgsFieldConstraints::ConstraintStrengthHard ) )
{
constraintstyle = mLayer->conditionalStyles()->constraintFailureStyles( QgsFieldConstraints::ConstraintStrengthHard );
}
else
{
if ( !QgsVectorLayerUtils::validateAttribute( mLayer, mFeat, fieldId, errors, QgsFieldConstraints::ConstraintStrengthSoft ) )
{
constraintstyle = mLayer->conditionalStyles()->constraintFailureStyles( QgsFieldConstraints::ConstraintStrengthSoft );
}
}
mConstraintStylesMap[mFeat.id()].insert( fieldId, constraintstyle );
}
}
styles = mLayer->conditionalStyles()->fieldStyles( field.name() );
styles = QgsConditionalStyle::matchingConditionalStyles( styles, val, mExpressionContext );
styles.insert( 0, rowstyle );
styles.insert( 0, constraintstyle );
const QgsConditionalStyle style = QgsConditionalStyle::compressStyles( styles );
if ( style.isValid() )
@ -796,6 +824,7 @@ bool QgsAttributeTableModel::setData( const QModelIndex &index, const QVariant &
return false;
mRowStylesMap.remove( mFeat.id() );
mConstraintStylesMap.remove( mFeat.id() );
if ( !mLayer->isModified() )
return false;

View File

@ -256,6 +256,20 @@ class GUI_EXPORT QgsAttributeTableModel: public QAbstractTableModel
*/
void setExtraColumns( int extraColumns );
/**
* Returns whether the attribute table will add a visual feedback to cells when an attribute
* constraint is not met.
* \since QGIS 3.30
*/
bool showValidityState() const { return mShowValidityState; }
/**
* Sets whether the attribute table will add a visual feedback to cells when an attribute constraint
* is not met.
* \since QGIS 3.30
*/
void setShowValidityState( bool show ) { mShowValidityState = show; }
public slots:
/**
@ -350,6 +364,7 @@ class GUI_EXPORT QgsAttributeTableModel: public QAbstractTableModel
QHash<QgsFeatureId, int> mIdRowMap;
QHash<int, QgsFeatureId> mRowIdMap;
mutable QHash<QgsFeatureId, QList<QgsConditionalStyle> > mRowStylesMap;
mutable QHash<QgsFeatureId, QHash<int, QgsConditionalStyle> > mConstraintStylesMap;
mutable QgsExpressionContext mExpressionContext;
@ -412,6 +427,8 @@ class GUI_EXPORT QgsAttributeTableModel: public QAbstractTableModel
//! TRUE if triggered by afterRollback()
bool mIsCleaningUpAfterRollback = false;
bool mShowValidityState = false;
friend class TestQgsAttributeTable;
};

View File

@ -326,6 +326,10 @@ void QgsDualView::setFilterMode( QgsAttributeTableFilterModel::FilterMode filter
case QgsAttributeTableFilterModel::ShowSelected:
disconnect( masterModel()->layer(), &QgsVectorLayer::selectionChanged, this, &QgsDualView::updateSelectedFeatures );
break;
case QgsAttributeTableFilterModel::ShowInvalid:
mMasterModel->setShowValidityState( false );
break;
}
QgsFeatureRequest r = mMasterModel->request();
@ -363,6 +367,16 @@ void QgsDualView::setFilterMode( QgsAttributeTableFilterModel::FilterMode filter
connect( masterModel()->layer(), &QgsVectorLayer::layerModified, this, &QgsDualView::updateEditedAddedFeatures );
break;
case QgsAttributeTableFilterModel::ShowInvalid:
{
mMasterModel->setShowValidityState( true );
const QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) );
filterFeatures( QStringLiteral( "is_feature_valid() = false" ), context );
connect( mFilterModel, &QgsAttributeTableFilterModel::featuresFiltered, this, &QgsDualView::filterChanged );
connect( mFilterModel, &QgsAttributeTableFilterModel::filterError, this, &QgsDualView::filterError );
break;
}
case QgsAttributeTableFilterModel::ShowAll:
case QgsAttributeTableFilterModel::ShowFilteredList:
{
@ -390,6 +404,7 @@ void QgsDualView::setFilterMode( QgsAttributeTableFilterModel::FilterMode filter
case QgsAttributeTableFilterModel::ShowAll:
case QgsAttributeTableFilterModel::ShowEdited:
case QgsAttributeTableFilterModel::ShowFilteredList:
case QgsAttributeTableFilterModel::ShowInvalid:
case QgsAttributeTableFilterModel::ShowSelected:
setBrowsingAutoPanScaleAllowed( true );
break;

View File

@ -55,6 +55,7 @@ QgsFeatureFilterWidget::QgsFeatureFilterWidget( QWidget *parent )
mActionShowAllFilter->setIcon( QgsApplication::getThemeIcon( "/mActionOpenTable.svg" ) );
mActionAdvancedFilter->setIcon( QgsApplication::getThemeIcon( "/mActionFilter2.svg" ) );
mActionSelectedFilter->setIcon( QgsApplication::getThemeIcon( "/mActionOpenTableSelected.svg" ) );
mActionInvalidFilter->setIcon( QgsApplication::getThemeIcon( "/mActionOpenTableInvalid.svg" ) );
mActionVisibleFilter->setIcon( QgsApplication::getThemeIcon( "/mActionOpenTableVisible.svg" ) );
mActionEditedFilter->setIcon( QgsApplication::getThemeIcon( "/mActionOpenTableEdited.svg" ) );
@ -70,6 +71,7 @@ QgsFeatureFilterWidget::QgsFeatureFilterWidget( QWidget *parent )
connect( mActionAdvancedFilter, &QAction::triggered, this, &QgsFeatureFilterWidget::filterExpressionBuilder );
connect( mActionShowAllFilter, &QAction::triggered, this, &QgsFeatureFilterWidget::filterShowAll );
connect( mActionSelectedFilter, &QAction::triggered, this, &QgsFeatureFilterWidget::filterSelected );
connect( mActionInvalidFilter, &QAction::triggered, this, &QgsFeatureFilterWidget::filterInvalid );
connect( mActionVisibleFilter, &QAction::triggered, this, &QgsFeatureFilterWidget::filterVisible );
connect( mActionEditedFilter, &QAction::triggered, this, &QgsFeatureFilterWidget::filterEdited );
connect( mFilterQuery, &QLineEdit::returnPressed, this, &QgsFeatureFilterWidget::filterQueryAccepted );
@ -138,6 +140,18 @@ void QgsFeatureFilterWidget::filterVisible()
mMainView->setFilterMode( QgsAttributeTableFilterModel::ShowVisible );
}
void QgsFeatureFilterWidget::filterInvalid()
{
mFilterButton->setDefaultAction( mActionInvalidFilter );
mFilterButton->setPopupMode( QToolButton::InstantPopup );
mFilterQuery->setVisible( false );
mApplyFilterButton->setVisible( false );
mStoreFilterExpressionButton->setVisible( false );
const QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) );
mMainView->filterFeatures( QStringLiteral( "is_feature_valid() = false" ), context );
mMainView->setFilterMode( QgsAttributeTableFilterModel::ShowInvalid );
}
void QgsFeatureFilterWidget::filterEdited()
{
mFilterButton->setDefaultAction( mActionEditedFilter );
@ -192,6 +206,7 @@ void QgsFeatureFilterWidget::columnBoxInit()
{
mFilterButton->addAction( mActionVisibleFilter );
}
mFilterButton->addAction( mActionInvalidFilter );
mFilterButton->addAction( mActionEditedFilter );
mFilterButton->addAction( mActionFilterColumnsMenu );
mFilterButton->addAction( mActionAdvancedFilter );

View File

@ -69,6 +69,7 @@ class GUI_EXPORT QgsFeatureFilterWidget : public QWidget, private Ui::QgsFeature
public slots:
void filterShowAll();
void filterSelected();
void filterInvalid();
void filterVisible();
void filterEdited();

View File

@ -34,8 +34,8 @@ QgsFieldConditionalFormatWidget::QgsFieldConditionalFormatWidget( QWidget *paren
setupUi( this );
setPanelTitle( tr( "Conditional Styles" ) );
connect( mFieldCombo, &QgsFieldComboBox::fieldChanged, this, &QgsFieldConditionalFormatWidget::fieldChanged );
connect( fieldRadio, &QAbstractButton::clicked, this, &QgsFieldConditionalFormatWidget::reloadStyles );
connect( rowRadio, &QAbstractButton::clicked, this, &QgsFieldConditionalFormatWidget::reloadStyles );
connect( fieldRadio, &QAbstractButton::clicked, this, &QgsFieldConditionalFormatWidget::typeChanged );
connect( rowRadio, &QAbstractButton::clicked, this, &QgsFieldConditionalFormatWidget::typeChanged );
connect( mNewButton, &QAbstractButton::clicked, this, &QgsFieldConditionalFormatWidget::addNewRule );
connect( listView, &QAbstractItemView::clicked, this, &QgsFieldConditionalFormatWidget::ruleClicked );
mModel = new QStandardItemModel( listView );
@ -103,10 +103,11 @@ void QgsFieldConditionalFormatWidget::editStyle( int editIndex, const QgsConditi
fieldName = mFieldCombo->currentField();
mLayer->conditionalStyles()->setFieldStyles( fieldName, styles );
}
if ( rowRadio->isChecked() )
else if ( rowRadio->isChecked() )
{
mLayer->conditionalStyles()->setRowStyles( styles );
}
reloadStyles();
emit rulesUpdated( fieldName );
} );
@ -142,10 +143,11 @@ QList<QgsConditionalStyle> QgsFieldConditionalFormatWidget::getStyles()
{
styles = mLayer->conditionalStyles()->fieldStyles( mFieldCombo->currentField() );
}
if ( rowRadio->isChecked() )
else if ( rowRadio->isChecked() )
{
styles = mLayer->conditionalStyles()->rowStyles();
}
return styles;
}
@ -187,6 +189,11 @@ QList<QgsConditionalStyle> QgsFieldConditionalFormatWidget::defaultPresets()
return styles;
}
void QgsFieldConditionalFormatWidget::typeChanged()
{
reloadStyles();
}
void QgsFieldConditionalFormatWidget::reloadStyles()
{
mModel->clear();
@ -224,7 +231,7 @@ void QgsFieldConditionalFormatWidget::deleteCurrentRule()
fieldName = mFieldCombo->currentField();
mLayer->conditionalStyles()->setFieldStyles( fieldName, styles );
}
if ( rowRadio->isChecked() )
else if ( rowRadio->isChecked() )
{
mLayer->conditionalStyles()->setRowStyles( styles );
}

View File

@ -110,6 +110,7 @@ class GUI_EXPORT QgsFieldConditionalFormatWidget : public QgsPanelWidget, privat
private slots:
void typeChanged();
void ruleClicked( const QModelIndex &index );
void reloadStyles();
void addNewRule();

View File

@ -198,7 +198,16 @@
<normaloff>:/images/themes/default/mActionOpenTableVisible.svg</normaloff>:/images/themes/default/mActionOpenTableVisible.svg</iconset>
</property>
<property name="text">
<string>Show Features Visible On Map</string>
<string>Show Features Visible on Map</string>
</property>
</action>
<action name="mActionInvalidFilter">
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionOpenTableInvalid.svg</normaloff>:/images/themes/default/mActionOpenTableInvalid.svg</iconset>
</property>
<property name="text">
<string>Show Features with Failing Constraints</string>
</property>
</action>
<action name="mActionEditedFilter">

View File

@ -21,18 +21,15 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_4" columnstretch="0,0,0,2">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<property name="topMargin">
<number>0</number>
</property>
<item row="0" column="3" rowspan="2">
<item row="2" column="0">
<widget class="QPushButton" name="mNewButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
@ -45,32 +42,36 @@
</property>
</widget>
</item>
<item row="0" column="2">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QgsFieldComboBox" name="mFieldCombo"/>
<widget class="QRadioButton" name="fieldRadio">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Field</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QgsFieldComboBox" name="mFieldCombo">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QRadioButton" name="fieldRadio">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Field</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="rowRadio">
<property name="sizePolicy">

View File

@ -67,6 +67,7 @@ class TestQgsAttributeTable : public QObject
void testSortNumbers();
void testStartMultiEditNoChanges();
void testMultiEditMakeUncommittedChanges();
void testInvalidView();
private:
QgisApp *mQgisApp = nullptr;
@ -793,5 +794,43 @@ void TestQgsAttributeTable::testOpenWithFilterExpression()
QCOMPARE( dlg->mMainView->filteredFeatures(), QgsFeatureIds() << 1 << 3 );
}
void TestQgsAttributeTable::testInvalidView()
{
std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:4326&field=pk:int&field=col1:date" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
QVERIFY( tempLayer->isValid() );
QgsPolylineXY line;
line << QgsPointXY( 0, 0 ) << QgsPointXY( 1, 1 );
QgsGeometry geometry = QgsGeometry::fromPolylineXY( line ) ;
QgsFeature f1( tempLayer->dataProvider()->fields(), 1 );
f1.setGeometry( geometry );
f1.setAttributes( QgsAttributes() << 1 << QDate( 2020, 1, 1 ) );
QgsFeature f2( tempLayer->dataProvider()->fields(), 2 );
f2.setGeometry( geometry );
f2.setAttributes( QgsAttributes() << 2 << QDate( 2020, 3, 1 ) );
QgsFeature f3( tempLayer->dataProvider()->fields(), 3 );
line.clear();
line << QgsPointXY( -3, -3 ) << QgsPointXY( -2, -2 );
geometry = QgsGeometry::fromPolylineXY( line );
f3.setGeometry( geometry );
f3.setAttributes( QgsAttributes() << 3 << QDate( 2020, 1, 1 ) );
QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) );
const QString filterExpression = QStringLiteral( "col1 >= to_date('2020-02-03')" );
tempLayer->setConstraintExpression( 1, QStringLiteral( "col1 >= to_date('2020-02-03')" ) );
tempLayer->setFieldConstraint( 1, QgsFieldConstraints::ConstraintExpression, QgsFieldConstraints::ConstraintStrengthHard );
std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get(),
QgsAttributeTableFilterModel::ShowAll,
nullptr,
Qt::Window,
nullptr,
filterExpression ) );
dlg->mFeatureFilterWidget->filterInvalid();
// feature id 2 is filtered out due not matching the provided filter expression
QCOMPARE( dlg->mMainView->filteredFeatures(), QgsFeatureIds() << 1 << 3 );
}
QGSTEST_MAIN( TestQgsAttributeTable )
#include "testqgsattributetable.moc"

View File

@ -264,7 +264,9 @@ class TestQgsVectorLayerUtils(unittest.TestCase):
layer = createLayerWithOnePoint()
# field expression check
self.assertFalse(QgsVectorLayerUtils.attributeHasConstraints(layer, 1))
layer.setConstraintExpression(1, 'fldint>5')
self.assertTrue(QgsVectorLayerUtils.attributeHasConstraints(layer, 1))
f = QgsFeature(2)
f.setAttributes(["test123", 6])
@ -297,7 +299,10 @@ class TestQgsVectorLayerUtils(unittest.TestCase):
self.assertTrue(res)
self.assertEqual(len(errors), 0)
self.assertFalse(QgsVectorLayerUtils.attributeHasConstraints(layer, 1))
layer.setFieldConstraint(1, QgsFieldConstraints.ConstraintNotNull)
self.assertTrue(QgsVectorLayerUtils.attributeHasConstraints(layer, 1))
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f, 1)
self.assertFalse(res)
self.assertEqual(len(errors), 1)
@ -315,7 +320,11 @@ class TestQgsVectorLayerUtils(unittest.TestCase):
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f, 1)
self.assertTrue(res)
self.assertEqual(len(errors), 0)
self.assertFalse(QgsVectorLayerUtils.attributeHasConstraints(layer, 1))
layer.setFieldConstraint(1, QgsFieldConstraints.ConstraintUnique)
self.assertTrue(QgsVectorLayerUtils.attributeHasConstraints(layer, 1))
res, errors = QgsVectorLayerUtils.validateAttribute(layer, f, 1)
self.assertFalse(res)
self.assertEqual(len(errors), 1)