diff --git a/images/images.qrc b/images/images.qrc index d301528e2dc..9dc459e01c8 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -591,6 +591,7 @@ themes/default/propertyicons/system.svg themes/default/propertyicons/transparency.svg themes/default/propertyicons/spacer.svg + themes/default/propertyicons/relations.svg themes/default/rendererCategorizedSymbol.svg themes/default/rendererGraduatedSymbol.svg themes/default/rendererNullSymbol.svg diff --git a/images/themes/default/propertyicons/relations.svg b/images/themes/default/propertyicons/relations.svg new file mode 100644 index 00000000000..1651d1c041c --- /dev/null +++ b/images/themes/default/propertyicons/relations.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/core/auto_generated/qgsmaplayer.sip.in b/python/core/auto_generated/qgsmaplayer.sip.in index 79f8601c779..0e2d1bd727d 100644 --- a/python/core/auto_generated/qgsmaplayer.sip.in +++ b/python/core/auto_generated/qgsmaplayer.sip.in @@ -91,6 +91,7 @@ This is the base class for all map layer types (vector, raster). Rendering, CustomProperties, GeometryOptions, + Relations, AllStyleCategories }; typedef QFlags StyleCategories; diff --git a/python/core/auto_generated/qgsrelationmanager.sip.in b/python/core/auto_generated/qgsrelationmanager.sip.in index fb084ca6378..581b87d7c90 100644 --- a/python/core/auto_generated/qgsrelationmanager.sip.in +++ b/python/core/auto_generated/qgsrelationmanager.sip.in @@ -104,7 +104,7 @@ Gets all relations where the specified layer (and field) is the referencing part :return: A list of relations matching the given layer and fieldIdx. %End - QList referencedRelations( QgsVectorLayer *layer = 0 ) const; + QList referencedRelations( const QgsVectorLayer *layer = 0 ) const; %Docstring Gets all relations where this layer is the referenced part (i.e. the parent table with the primary key being referenced from another layer). diff --git a/python/core/auto_generated/qgsvectorlayer.sip.in b/python/core/auto_generated/qgsvectorlayer.sip.in index a9b6d1d4aaa..294daa263ef 100644 --- a/python/core/auto_generated/qgsvectorlayer.sip.in +++ b/python/core/auto_generated/qgsvectorlayer.sip.in @@ -2014,6 +2014,8 @@ Returns the layer's relations, where the foreign key is on this layer. :return: A list of relations %End + + QgsVectorLayerEditBuffer *editBuffer(); %Docstring Buffer with uncommitted editing operations. Only valid after editing has been turned on. diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 5301c30e7f9..5107e351ee0 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -75,9 +75,11 @@ #include "qgssettings.h" #include "qgsnetworkaccessmanager.h" +#include "qgsrelationmanager.h" #include "qgsapplication.h" #include "qgslayerstylingwidget.h" #include "qgstaskmanager.h" +#include "qgsweakrelation.h" #include "qgsziputils.h" #include "qgsbrowserguimodel.h" #include "qgsvectorlayerjoinbuffer.h" @@ -667,13 +669,24 @@ void QgisApp::onActiveLayerChanged( QgsMapLayer *layer ) void QgisApp::vectorLayerStyleLoaded( QgsMapLayer::StyleCategories categories ) { - if ( categories.testFlag( QgsMapLayer::StyleCategory::Forms ) ) + + QgsVectorLayer *vl = qobject_cast( sender() ); + + if ( vl && vl->isValid( ) ) { - QgsVectorLayer *vl = qobject_cast( sender() ); - if ( vl && vl->isValid( ) ) + + // Check broken dependencies in forms + if ( categories.testFlag( QgsMapLayer::StyleCategory::Forms ) ) { - checkVectorLayerDependencies( vl ); + resolveVectorLayerDependencies( vl ); } + + // Check broken relations and try to restore them + if ( categories.testFlag( QgsMapLayer::StyleCategory::Relations ) ) + { + resolveVectorLayerWeakRelations( vl ); + } + } } @@ -1995,25 +2008,85 @@ QgsMessageBar *QgisApp::visibleMessageBar() } } -QList QgisApp::findBrokenWidgetDependencies( QgsVectorLayer *vl ) +const QList QgisApp::findBrokenLayerDependencies( QgsVectorLayer *vl, QgsMapLayer::StyleCategories categories ) const { QList brokenDependencies; - // Check for missing layer widget dependencies - for ( int i = 0; i < vl->fields().count(); i++ ) + + if ( categories.testFlag( QgsMapLayer::StyleCategory::Forms ) ) { - const QgsEditorWidgetSetup setup = QgsGui::editorWidgetRegistry()->findBest( vl, vl->fields().field( i ).name() ); - QgsFieldFormatter *fieldFormatter = QgsApplication::fieldFormatterRegistry()->fieldFormatter( setup.type() ); - if ( fieldFormatter ) + for ( int i = 0; i < vl->fields().count(); i++ ) { - const auto constDependencies { fieldFormatter->layerDependencies( setup.config() ) }; - for ( const QgsVectorLayerRef &dependency : constDependencies ) + const QgsEditorWidgetSetup setup = QgsGui::editorWidgetRegistry()->findBest( vl, vl->fields().field( i ).name() ); + QgsFieldFormatter *fieldFormatter = QgsApplication::fieldFormatterRegistry()->fieldFormatter( setup.type() ); + if ( fieldFormatter ) { - const QgsVectorLayer *depVl { QgsVectorLayerRef( dependency ).resolveWeakly( - QgsProject::instance(), - QgsVectorLayerRef::MatchType::Name ) }; - if ( ! depVl || ! depVl->isValid() ) + const QList constDependencies { fieldFormatter->layerDependencies( setup.config() ) }; + for ( const QgsVectorLayerRef &dependency : constDependencies ) { - brokenDependencies.append( dependency ); + const QgsVectorLayer *depVl { QgsVectorLayerRef( dependency ).resolveWeakly( + QgsProject::instance(), + QgsVectorLayerRef::MatchType::Name ) }; + if ( ! depVl || ! depVl->isValid() ) + { + brokenDependencies.append( dependency ); + } + } + } + } + } + + if ( categories.testFlag( QgsMapLayer::StyleCategory::Relations ) ) + { + // Check for layer weak relations + const QList constWeakRelations { vl->weakRelations() }; + for ( const QgsWeakRelation &rel : constWeakRelations ) + { + QgsRelation relation { rel.resolvedRelation( QgsProject::instance(), QgsVectorLayerRef::MatchType::Name ) }; + QgsVectorLayerRef dependency; + bool found = false; + if ( ! relation.isValid() ) + { + // This is the big question: do we really + // want to automatically load the referencing layer(s) too? + // This could potentially lead to a cascaded load of a + // long list of layers. + // The code is in place but let's leave it disabled for now. + if ( relation.referencedLayer() == vl ) + { + // Do nothing because vl is the referenced layer +#if 0 + dependency = rel.referencingLayer(); + found = true; +#endif + } + else if ( relation.referencingLayer() == vl ) + { + dependency = rel.referencedLayer(); + found = true; + } + else + { + // Something wrong is going on here, maybe this relation + // does not really apply to this layer? + QgsMessageLog::logMessage( tr( "None of the layers in the relation stored in the style match the current layer, skipping relation id: %1." ).arg( relation.id() ) ); + } + + if ( found ) + { + // Make sure we don't add it twice + bool refFound = false; + for ( const QgsVectorLayerRef &otherRef : qgis::as_const( brokenDependencies ) ) + { + if ( dependency.layerId == otherRef.layerId || ( dependency.source == otherRef.source && dependency.provider == otherRef.provider ) ) + { + refFound = true; + break; + } + } + if ( ! refFound ) + { + brokenDependencies.append( dependency ); + } } } } @@ -2021,11 +2094,11 @@ QList QgisApp::findBrokenWidgetDependencies( QgsVectorLayer * return brokenDependencies; } -void QgisApp::checkVectorLayerDependencies( QgsVectorLayer *vl ) +void QgisApp::resolveVectorLayerDependencies( QgsVectorLayer *vl, QgsMapLayer::StyleCategories categories ) { if ( vl && vl->isValid() ) { - const auto constDependencies { findBrokenWidgetDependencies( vl ) }; + const auto constDependencies { findBrokenLayerDependencies( vl, categories ) }; for ( const QgsVectorLayerRef &dependency : constDependencies ) { // try to aggressively resolve the broken dependencies @@ -2115,6 +2188,31 @@ void QgisApp::checkVectorLayerDependencies( QgsVectorLayer *vl ) } } +void QgisApp::resolveVectorLayerWeakRelations( QgsVectorLayer *vectorLayer ) +{ + if ( vectorLayer && vectorLayer->isValid() ) + { + const QList constWeakRelations { vectorLayer->weakRelations( ) }; + for ( const QgsWeakRelation &rel : constWeakRelations ) + { + QgsRelation relation { rel.resolvedRelation( QgsProject::instance(), QgsVectorLayerRef::MatchType::Name ) }; + if ( relation.isValid() ) + { + // Avoid duplicates + const QList constRelations { QgsProject::instance()->relationManager()->relations().values() }; + for ( const QgsRelation &other : constRelations ) + { + if ( relation.hasEqualDefinition( other ) ) + { + continue; + } + } + QgsProject::instance()->relationManager()->addRelation( relation ); + } + } + } +} + void QgisApp::dataSourceManager( const QString &pageName ) { if ( ! mDataSourceManagerDialog ) @@ -6506,7 +6604,7 @@ bool QgisApp::addProject( const QString &projectFile ) { if ( vl->isValid() ) { - checkVectorLayerDependencies( vl ); + resolveVectorLayerDependencies( vl ); } } diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index bd13868a141..b6afb1bbd13 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -2025,15 +2025,31 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QgsMessageBar *visibleMessageBar(); /** - * Searches for layer widget dependencies - * \return a list of weak references to broken widget layer dependencies + * Searches for layer dependencies by querying the form widgets and the + * \a vectorLayer itself for broken relations. Style \a categories can be + * used to limit the search to one or more of the currently implemented search + * categories ("Forms" for the form widgets and "Relations" for layer weak relations). + * \return a list of weak references to broken layer dependencies */ - QList< QgsVectorLayerRef > findBrokenWidgetDependencies( QgsVectorLayer *vectorLayer ); + const QList< QgsVectorLayerRef > findBrokenLayerDependencies( QgsVectorLayer *vectorLayer, + QgsMapLayer::StyleCategories categories = QgsMapLayer::StyleCategory::AllStyleCategories ) const; /** - * Scans the \a vectorLayer for broken dependencies and warns the user + * Scans the \a vectorLayer for broken dependencies and automatically + * try to load the missing layers, users are notified about the operation + * result. Style \a categories can be + * used to exclude one of the currently implemented search categories + * ("Forms" for the form widgets and "Relations" for layer weak relations). */ - void checkVectorLayerDependencies( QgsVectorLayer *vectorLayer ); + void resolveVectorLayerDependencies( QgsVectorLayer *vectorLayer, + QgsMapLayer::StyleCategories categories = QgsMapLayer::AllStyleCategories ); + + /** + * Scans the \a vectorLayer for weak relations and automatically + * try to resolve and create the broken relations. + */ + void resolveVectorLayerWeakRelations( QgsVectorLayer *vectorLayer ); + QgisAppStyleSheet *mStyleSheetBuilder = nullptr; diff --git a/src/app/qgsmaplayerstylecategoriesmodel.cpp b/src/app/qgsmaplayerstylecategoriesmodel.cpp index 2f69319f15f..ad879398ae5 100644 --- a/src/app/qgsmaplayerstylecategoriesmodel.cpp +++ b/src/app/qgsmaplayerstylecategoriesmodel.cpp @@ -77,7 +77,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro switch ( category ) { - case QgsMapLayer::LayerConfiguration: + case QgsMapLayer::StyleCategory::LayerConfiguration: switch ( role ) { case Qt::DisplayRole: @@ -88,7 +88,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/system.svg" ) ); } break; - case QgsMapLayer::Symbology: + case QgsMapLayer::StyleCategory::Symbology: switch ( role ) { case Qt::DisplayRole: @@ -99,7 +99,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/symbology.svg" ) ); } break; - case QgsMapLayer::Symbology3D: + case QgsMapLayer::StyleCategory::Symbology3D: switch ( role ) { case Qt::DisplayRole: @@ -110,7 +110,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/3d.svg" ) ); } break; - case QgsMapLayer::Labeling: + case QgsMapLayer::StyleCategory::Labeling: switch ( role ) { case Qt::DisplayRole: @@ -121,7 +121,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/labels.svg" ) ); } break; - case QgsMapLayer::Fields: + case QgsMapLayer::StyleCategory::Fields: switch ( role ) { case Qt::DisplayRole: @@ -132,7 +132,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/mSourceFields.svg" ) ); } break; - case QgsMapLayer::Forms: + case QgsMapLayer::StyleCategory::Forms: switch ( role ) { case Qt::DisplayRole: @@ -143,7 +143,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/mActionFormView.svg" ) ); } break; - case QgsMapLayer::Actions: + case QgsMapLayer::StyleCategory::Actions: switch ( role ) { case Qt::DisplayRole: @@ -154,7 +154,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/action.svg" ) ); } break; - case QgsMapLayer::MapTips: + case QgsMapLayer::StyleCategory::MapTips: switch ( role ) { case Qt::DisplayRole: @@ -165,7 +165,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/display.svg" ) ); } break; - case QgsMapLayer::Diagrams: + case QgsMapLayer::StyleCategory::Diagrams: switch ( role ) { case Qt::DisplayRole: @@ -176,7 +176,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/diagram.svg" ) ); } break; - case QgsMapLayer::AttributeTable: + case QgsMapLayer::StyleCategory::AttributeTable: switch ( role ) { case Qt::DisplayRole: @@ -187,7 +187,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/mActionOpenTable.svg" ) ); } break; - case QgsMapLayer::Rendering: + case QgsMapLayer::StyleCategory::Rendering: switch ( role ) { case Qt::DisplayRole: @@ -198,7 +198,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/rendering.svg" ) ); } break; - case QgsMapLayer::CustomProperties: + case QgsMapLayer::StyleCategory::CustomProperties: switch ( role ) { case Qt::DisplayRole: @@ -209,7 +209,7 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/mActionOptions.svg" ) ); } break; - case QgsMapLayer::GeometryOptions: + case QgsMapLayer::StyleCategory::GeometryOptions: switch ( role ) { case Qt::DisplayRole: @@ -220,7 +220,18 @@ QVariant QgsMapLayerStyleCategoriesModel::data( const QModelIndex &index, int ro return QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/digitizing.svg" ) ); } break; - case QgsMapLayer::AllStyleCategories: + case QgsMapLayer::StyleCategory::Relations: + switch ( role ) + { + case Qt::DisplayRole: + return tr( "Relations" ); + case Qt::ToolTipRole: + return tr( "Relations with other layers" ); + case Qt::DecorationRole: + return QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/relations.svg" ) ); + } + break; + case QgsMapLayer::StyleCategory::AllStyleCategories: switch ( role ) { case Qt::DisplayRole: diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 66ec28b7708..059098f2738 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -339,6 +339,7 @@ SET(QGIS_CORE_SRCS qgsreadwritecontext.cpp qgsreadwritelocker.cpp qgsrelation.cpp + qgsweakrelation.cpp qgsrelationmanager.cpp qgsrenderchecker.cpp qgsrendercontext.cpp @@ -840,6 +841,7 @@ SET(QGIS_CORE_HDRS qgsreadwritecontext.h qgsreadwritelocker.h qgsrelation.h + qgsweakrelation.h qgsrelationmanager.h qgsrenderchecker.h qgsrendercontext.h diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp index 0fcc29a064f..8d34cdf9933 100644 --- a/src/core/qgsmaplayer.cpp +++ b/src/core/qgsmaplayer.cpp @@ -267,15 +267,6 @@ bool QgsMapLayer::readLayerXml( const QDomElement &layerElement, QgsReadWriteCon QgsReadWriteContextCategoryPopper p = context.enterCategory( tr( "Layer" ), mne.text() ); - // now let the children grab what they need from the Dom node. - layerError = !readXml( layerElement, context ); - - // overwrite CRS with what we read from project file before the raster/vector - // file reading functions changed it. They will if projections is specified in the file. - // FIXME: is this necessary? - QgsCoordinateReferenceSystem::setCustomCrsValidation( savedValidation ); - mCRS = savedCRS; - // the internal name is just the data source basename //QFileInfo dataSourceFileInfo( mDataSource ); //internalName = dataSourceFileInfo.baseName(); @@ -296,7 +287,6 @@ bool QgsMapLayer::readLayerXml( const QDomElement &layerElement, QgsReadWriteCon setRefreshOnNofifyMessage( layerElement.attribute( QStringLiteral( "refreshOnNotifyMessage" ), QString() ) ); setRefreshOnNotifyEnabled( layerElement.attribute( QStringLiteral( "refreshOnNotifyEnabled" ), QStringLiteral( "0" ) ).toInt() ); - // set name mnl = layerElement.namedItem( QStringLiteral( "layername" ) ); mne = mnl.toElement(); @@ -304,6 +294,15 @@ bool QgsMapLayer::readLayerXml( const QDomElement &layerElement, QgsReadWriteCon //name can be translated setName( context.projectTranslator()->translate( QStringLiteral( "project:layers:%1" ).arg( layerElement.namedItem( QStringLiteral( "id" ) ).toElement().text() ), mne.text() ) ); + // now let the children grab what they need from the Dom node. + layerError = !readXml( layerElement, context ); + + // overwrite CRS with what we read from project file before the raster/vector + // file reading functions changed it. They will if projections is specified in the file. + // FIXME: is this necessary? Yes, it is (autumn 2019) + QgsCoordinateReferenceSystem::setCustomCrsValidation( savedValidation ); + mCRS = savedCRS; + //short name QDomElement shortNameElem = layerElement.firstChildElement( QStringLiteral( "shortname" ) ); if ( !shortNameElem.isNull() ) diff --git a/src/core/qgsmaplayer.h b/src/core/qgsmaplayer.h index ec431c10600..d3e454c5a89 100644 --- a/src/core/qgsmaplayer.h +++ b/src/core/qgsmaplayer.h @@ -161,8 +161,9 @@ class CORE_EXPORT QgsMapLayer : public QObject Rendering = 1 << 10, //!< Rendering: scale visibility, simplify method, opacity CustomProperties = 1 << 11, //!< Custom properties (by plugins for instance) GeometryOptions = 1 << 12, //!< Geometry validation configuration + Relations = 1 << 13, //!< Relations AllStyleCategories = LayerConfiguration | Symbology | Symbology3D | Labeling | Fields | Forms | Actions | - MapTips | Diagrams | AttributeTable | Rendering | CustomProperties | GeometryOptions, + MapTips | Diagrams | AttributeTable | Rendering | CustomProperties | GeometryOptions | Relations, }; Q_ENUM( StyleCategory ) Q_DECLARE_FLAGS( StyleCategories, StyleCategory ) diff --git a/src/core/qgsrelationmanager.cpp b/src/core/qgsrelationmanager.cpp index 5469f103050..404a9560713 100644 --- a/src/core/qgsrelationmanager.cpp +++ b/src/core/qgsrelationmanager.cpp @@ -27,8 +27,11 @@ QgsRelationManager::QgsRelationManager( QgsProject *project ) { if ( mProject ) { + // TODO: QGIS 4 remove: relations are now stored with the layer style connect( project, &QgsProject::readProjectWithContext, this, &QgsRelationManager::readProject ); + // TODO: QGIS 4 remove: relations are now stored with the layer style connect( project, &QgsProject::writeProject, this, &QgsRelationManager::writeProject ); + connect( project, &QgsProject::layersRemoved, this, &QgsRelationManager::layersRemoved ); } } @@ -148,7 +151,7 @@ QList QgsRelationManager::referencingRelations( const QgsVectorLaye return relations; } -QList QgsRelationManager::referencedRelations( QgsVectorLayer *layer ) const +QList QgsRelationManager::referencedRelations( const QgsVectorLayer *layer ) const { if ( !layer ) { diff --git a/src/core/qgsrelationmanager.h b/src/core/qgsrelationmanager.h index 0f5f48565ff..53b76f5ff13 100644 --- a/src/core/qgsrelationmanager.h +++ b/src/core/qgsrelationmanager.h @@ -121,7 +121,7 @@ class CORE_EXPORT QgsRelationManager : public QObject * * \returns A list of relations where the specified layer is the referenced part. */ - QList referencedRelations( QgsVectorLayer *layer = nullptr ) const; + QList referencedRelations( const QgsVectorLayer *layer = nullptr ) const; /** * Discover all the relations available from the current layers. diff --git a/src/core/qgsvectorlayer.cpp b/src/core/qgsvectorlayer.cpp index baa946d7a8a..854d45fcd7b 100644 --- a/src/core/qgsvectorlayer.cpp +++ b/src/core/qgsvectorlayer.cpp @@ -67,6 +67,7 @@ #include "qgsproviderregistry.h" #include "qgsrectangle.h" #include "qgsrelationmanager.h" +#include "qgsweakrelation.h" #include "qgsrendercontext.h" #include "qgsvectordataprovider.h" #include "qgsvectorlayereditbuffer.h" @@ -2026,6 +2027,80 @@ bool QgsVectorLayer::readSymbology( const QDomNode &layerNode, QString &errorMes updateFields(); } + if ( categories.testFlag( Relations ) ) + { + + const QgsPathResolver resolver { QgsProject::instance()->pathResolver() }; + + // Restore referenced layers: relations where "this" is the child layer (the referencing part, that holds the FK) + QDomNodeList referencedLayersNodeList = layerNode.toElement().elementsByTagName( QStringLiteral( "referencedLayers" ) ); + if ( referencedLayersNodeList.size() > 0 ) + { + const QDomNodeList relationNodes { referencedLayersNodeList.at( 0 ).childNodes() }; + for ( int i = 0; i < relationNodes.length(); ++i ) + { + const QDomElement relationElement = relationNodes.at( i ).toElement(); + QList fieldPairs; + const QDomNodeList fieldPairNodes { relationElement.elementsByTagName( QStringLiteral( "fieldPair" ) ) }; + for ( int j = 0; j < fieldPairNodes.length(); ++j ) + { + const QDomElement fieldPairElement = fieldPairNodes.at( i ).toElement(); + fieldPairs.push_back( { fieldPairElement.attribute( QStringLiteral( "referencing" ) ), + fieldPairElement.attribute( QStringLiteral( "referenced" ) ) } ); + } + mWeakRelations.push_back( QgsWeakRelation { relationElement.attribute( QStringLiteral( "id" ) ), + relationElement.attribute( QStringLiteral( "name" ) ), + static_cast( relationElement.attribute( QStringLiteral( "strength" ) ).toInt() ), + // Referencing + id(), + name(), + resolver.writePath( publicSource() ), + providerType(), + // Referenced + relationElement.attribute( QStringLiteral( "layerId" ) ), + relationElement.attribute( QStringLiteral( "layerName" ) ), + relationElement.attribute( QStringLiteral( "dataSource" ) ), + relationElement.attribute( QStringLiteral( "providerKey" ) ), + fieldPairs + } ); + } + } + + // Restore referencing layers: relations where "this" is the parent layer (the referenced part where the FK points to) + QDomNodeList referencingLayersNodeList = layerNode.toElement().elementsByTagName( QStringLiteral( "referencingLayers" ) ); + if ( referencingLayersNodeList.size() > 0 ) + { + const QDomNodeList relationNodes { referencingLayersNodeList.at( 0 ).childNodes() }; + for ( int i = 0; i < relationNodes.length(); ++i ) + { + const QDomElement relationElement = relationNodes.at( i ).toElement(); + QList fieldPairs; + const QDomNodeList fieldPairNodes { relationElement.elementsByTagName( QStringLiteral( "fieldPair" ) ) }; + for ( int j = 0; j < fieldPairNodes.length(); ++j ) + { + const QDomElement fieldPairElement = fieldPairNodes.at( i ).toElement(); + fieldPairs.push_back( { fieldPairElement.attribute( QStringLiteral( "referencing" ) ), + fieldPairElement.attribute( QStringLiteral( "referenced" ) ) } ); + } + mWeakRelations.push_back( QgsWeakRelation { relationElement.attribute( QStringLiteral( "id" ) ), + relationElement.attribute( QStringLiteral( "name" ) ), + static_cast( relationElement.attribute( QStringLiteral( "strength" ) ).toInt() ), + // Referencing + relationElement.attribute( QStringLiteral( "layerId" ) ), + relationElement.attribute( QStringLiteral( "layerName" ) ), + relationElement.attribute( QStringLiteral( "dataSource" ) ), + relationElement.attribute( QStringLiteral( "providerKey" ) ), + // Referenced + id(), + name(), + resolver.writePath( publicSource() ), + providerType(), + fieldPairs + } ); + } + } + } + QDomElement layerElement = layerNode.toElement(); readCommonStyle( layerElement, context, categories ); @@ -2445,6 +2520,74 @@ bool QgsVectorLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QString if ( categories.testFlag( GeometryOptions ) ) mGeometryOptions->writeXml( node ); + + // Relation information for both referenced and referencing sides + if ( categories.testFlag( Relations ) ) + { + + const QgsPathResolver resolver { QgsProject::instance()->pathResolver() }; + + // Store referenced layers: relations where "this" is the child layer (the referencing part, that holds the FK) + QDomElement referencedLayersElement = doc.createElement( QStringLiteral( "referencedLayers" ) ); + node.appendChild( referencedLayersElement ); + + const auto constReferencingRelations { QgsProject::instance()->relationManager()->referencingRelations( this ) }; + for ( const auto &rel : constReferencingRelations ) + { + + QDomElement relationElement = doc.createElement( QStringLiteral( "relation" ) ); + referencedLayersElement.appendChild( relationElement ); + + relationElement.setAttribute( QStringLiteral( "id" ), rel.id() ); + relationElement.setAttribute( QStringLiteral( "name" ), rel.name() ); + relationElement.setAttribute( QStringLiteral( "strength" ), rel.strength() ); + relationElement.setAttribute( QStringLiteral( "layerId" ), rel.referencedLayer()->id() ); + relationElement.setAttribute( QStringLiteral( "layerName" ), rel.referencedLayer()->name() ); + relationElement.setAttribute( QStringLiteral( "dataSource" ), resolver.writePath( rel.referencedLayer()->publicSource() ) ); + relationElement.setAttribute( QStringLiteral( "providerKey" ), rel.referencedLayer()->providerType() ); + + const QList constFieldPairs { rel.fieldPairs() }; + for ( const QgsRelation::FieldPair &fp : constFieldPairs ) + { + QDomElement fieldPair = doc.createElement( QStringLiteral( "fieldPair" ) ); + relationElement.appendChild( fieldPair ); + fieldPair.setAttribute( QStringLiteral( "referenced" ), fp.referencedField() ); + fieldPair.setAttribute( QStringLiteral( "referencing" ), fp.referencingField() ); + } + + } + + // Store referencing layers: relations where "this" is the parent layer (the referenced part where the FK points to) + QDomElement referencingLayersElement = doc.createElement( QStringLiteral( "referencingLayers" ) ); + node.appendChild( referencingLayersElement ); + + const auto constReferencedRelations { QgsProject::instance()->relationManager()->referencedRelations( this ) }; + for ( const auto &rel : constReferencedRelations ) + { + + QDomElement relationElement = doc.createElement( QStringLiteral( "relation" ) ); + referencingLayersElement.appendChild( relationElement ); + + relationElement.setAttribute( QStringLiteral( "id" ), rel.id() ); + relationElement.setAttribute( QStringLiteral( "name" ), rel.name() ); + relationElement.setAttribute( QStringLiteral( "strength" ), rel.strength() ); + relationElement.setAttribute( QStringLiteral( "layerId" ), rel.referencingLayer()->id() ); + relationElement.setAttribute( QStringLiteral( "layerName" ), rel.referencingLayer()->name() ); + relationElement.setAttribute( QStringLiteral( "dataSource" ), resolver.writePath( rel.referencingLayer()->publicSource() ) ); + relationElement.setAttribute( QStringLiteral( "providerKey" ), rel.referencingLayer()->providerType() ); + + const QList constFieldPairs { rel.fieldPairs() }; + for ( const QgsRelation::FieldPair &fp : constFieldPairs ) + { + QDomElement fieldPair = doc.createElement( QStringLiteral( "fieldPair" ) ); + relationElement.appendChild( fieldPair ); + fieldPair.setAttribute( QStringLiteral( "referenced" ), fp.referencedField() ); + fieldPair.setAttribute( QStringLiteral( "referencing" ), fp.referencingField() ); + } + + } + } + if ( categories.testFlag( Fields ) ) { QDomElement fieldConfigurationElement = doc.createElement( QStringLiteral( "fieldConfiguration" ) ); @@ -4766,6 +4909,11 @@ QList QgsVectorLayer::referencingRelations( int idx ) const return QgsProject::instance()->relationManager()->referencingRelations( this, idx ); } +QList QgsVectorLayer::weakRelations() const +{ + return mWeakRelations; +} + int QgsVectorLayer::listStylesInDatabase( QStringList &ids, QStringList &names, QStringList &descriptions, QString &msgError ) { return QgsProviderRegistry::instance()->listStyles( mProviderKey, mDataSource, ids, names, descriptions, msgError ); diff --git a/src/core/qgsvectorlayer.h b/src/core/qgsvectorlayer.h index 28d4525ba78..b86ca83468b 100644 --- a/src/core/qgsvectorlayer.h +++ b/src/core/qgsvectorlayer.h @@ -62,6 +62,7 @@ class QgsMapToPixel; class QgsRectangle; class QgsRectangle; class QgsRelation; +class QgsWeakRelation; class QgsRelationManager; class QgsSingleSymbolRenderer; class QgsStoredExpressionManager; @@ -1872,6 +1873,15 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte */ QList referencingRelations( int idx ) const; + /** + * Returns the layer's weak relations as specified in the layer's style. + * \returns A list of weak relations + * \note not available in Python bindings + * \since QGIS 3.12 + */ + QList weakRelations( ) const SIP_SKIP; + + //! Buffer with uncommitted editing operations. Only valid after editing has been turned on. Q_INVOKABLE QgsVectorLayerEditBuffer *editBuffer() { return mEditBuffer; } @@ -2827,6 +2837,8 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte //! To avoid firing multiple time dataChanged signal on circular layer circular dependencies bool mDataChangedFired = false; + + QList mWeakRelations; }; diff --git a/src/core/qgsweakrelation.cpp b/src/core/qgsweakrelation.cpp new file mode 100644 index 00000000000..45782f0f9d5 --- /dev/null +++ b/src/core/qgsweakrelation.cpp @@ -0,0 +1,75 @@ +/*************************************************************************** + qgsweakrelation.cpp - QgsWeakRelation + + --------------------- + begin : 5.12.2019 + copyright : (C) 2019 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include "qgsweakrelation.h" + + +QgsWeakRelation::QgsWeakRelation( const QString &relationId, const QString &relationName, const QgsRelation::RelationStrength strength, + const QString &referencingLayerId, const QString &referencingLayerName, const QString &referencingLayerSource, const QString &referencingLayerProviderKey, + const QString &referencedLayerId, const QString &referencedLayerName, const QString &referencedLayerSource, const QString &referencedLayerProviderKey, + const QList &fieldPairs ) + : mReferencingLayer( referencingLayerId, referencingLayerName, referencingLayerSource, referencingLayerProviderKey ) + , mReferencedLayer( referencedLayerId, referencedLayerName, referencedLayerSource, referencedLayerProviderKey ) + , mRelationId( relationId ) + , mRelationName( relationName ) + , mStrength( strength ) + , mFieldPairs( fieldPairs ) +{ +} + +QgsRelation QgsWeakRelation::resolvedRelation( const QgsProject *project, QgsVectorLayerRef::MatchType matchType ) const +{ + QgsRelation relation; + relation.setId( mRelationId ); + relation.setName( mRelationName ); + relation.setStrength( mStrength ); + QgsVectorLayerRef referencedLayerRef { mReferencedLayer }; + QgsMapLayer *referencedLayer { referencedLayerRef.resolveWeakly( project, matchType ) }; + if ( referencedLayer ) + { + relation.setReferencedLayer( referencedLayer->id() ); + } + QgsVectorLayerRef referencingLayerRef { mReferencingLayer }; + QgsMapLayer *referencingLayer { referencingLayerRef.resolveWeakly( project, matchType ) }; + if ( referencingLayer ) + { + relation.setReferencingLayer( referencingLayer->id() ); + } + for ( const auto &fp : qgis::as_const( mFieldPairs ) ) + { + relation.addFieldPair( fp ); + } + return relation; +} + +QgsVectorLayerRef QgsWeakRelation::referencingLayer() const +{ + return mReferencingLayer; +} + +QgsVectorLayerRef QgsWeakRelation::referencedLayer() const +{ + return mReferencedLayer; +} + +QgsRelation::RelationStrength QgsWeakRelation::strength() const +{ + return mStrength; +} + +QList QgsWeakRelation::fieldPairs() const +{ + return mFieldPairs; +} diff --git a/src/core/qgsweakrelation.h b/src/core/qgsweakrelation.h new file mode 100644 index 00000000000..1f31e0706e0 --- /dev/null +++ b/src/core/qgsweakrelation.h @@ -0,0 +1,101 @@ +/*************************************************************************** + qgsweakrelation.h - QgsWeakRelation + + --------------------- + begin : 5.12.2019 + copyright : (C) 2019 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSWEAKRELATION_H +#define QGSWEAKRELATION_H + +#define SIP_NO_FILE + +#include "qgis_core.h" +#include "qgsrelation.h" +#include "qgsvectorlayerref.h" + +/** + * The QgsWeakRelation class represent a QgsRelation with possibly + * unresolved layers or unmatched fields. + * + * This class is used to store relation information attached to a + * layer style, a method to attempt relation resolution is also + * implemented and can be used to create a QgsRelation after the + * dependent layers are loaded and available. + * + * \note not available in Python bindings + * \ingroup core + * \since QGIS 3.12 + */ +class CORE_EXPORT QgsWeakRelation +{ + public: + + /** + * Creates a QgsWeakRelation + */ + QgsWeakRelation( const QString &relationId, + const QString &relationName, + const QgsRelation::RelationStrength strength, + const QString &referencingLayerId, + const QString &referencingLayerName, + const QString &referencingLayerSource, + const QString &referencingLayerProviderKey, + const QString &referencedLayerId, + const QString &referencedLayerName, + const QString &referencedLayerSource, + const QString &referencedLayerProviderKey, + const QList &fieldPairs + ); + + /** + * Resolves a weak relation in the given \a project returning a possibly invalid QgsRelation + * and without performing any kind of validity check. + * + * \note Client code should never assume that the returned relation is valid and the + * layer components are not NULL. + */ + QgsRelation resolvedRelation( const QgsProject *project, QgsVectorLayerRef::MatchType matchType = QgsVectorLayerRef::MatchType::All ) const; + + /** + * Returns a weak reference to the referencing layer + */ + QgsVectorLayerRef referencingLayer() const; + + /** + * Returns a weak reference to the referenced layer + */ + QgsVectorLayerRef referencedLayer() const; + + /** + * Returns the strength of the relation + */ + QgsRelation::RelationStrength strength() const; + + /** + * Returns the list of field pairs + */ + QList fieldPairs() const; + + private: + + QgsVectorLayerRef mReferencingLayer; + QgsVectorLayerRef mReferencedLayer; + QString mRelationId; + QString mRelationName; + QgsRelation::RelationStrength mStrength = QgsRelation::RelationStrength::Association; + QList mFieldPairs; + + friend class TestQgsWeakRelation; + +}; + +#endif // QGSWEAKRELATION_H diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index 4cb8c63638d..e052a510569 100644 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -241,6 +241,7 @@ SET(TESTS testqobjectuniqueptr.cpp testqgspostgresstringutils.cpp testqgsstoredexpressionmanager.cpp + testqgsweakrelation.cpp ) IF(WITH_QTWEBKIT) diff --git a/tests/src/core/testqgsweakrelation.cpp b/tests/src/core/testqgsweakrelation.cpp new file mode 100644 index 00000000000..703dbc67602 --- /dev/null +++ b/tests/src/core/testqgsweakrelation.cpp @@ -0,0 +1,106 @@ +/*************************************************************************** + testqgsweakrelation.cpp + ---------------- + Date : December 2019 + Copyright : (C) 2019 Alessandro Pasotti + Email : elpaso at itopen dot it + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstest.h" +#include "qgsapplication.h" +#include "qgsvectorlayer.h" +#include "qgsproject.h" +#include "qgsweakrelation.h" +#include "qgsrelation.h" + +class TestQgsWeakRelation: public QObject +{ + Q_OBJECT + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init();// will be called before each testfunction is executed. + void cleanup();// will be called after every testfunction. + + void testResolved(); // Test if relation can be resolved + + private: +}; + +void TestQgsWeakRelation::initTestCase() +{ + // Set up the QgsSettings environment + QCoreApplication::setOrganizationName( QStringLiteral( "QGIS" ) ); + QCoreApplication::setOrganizationDomain( QStringLiteral( "qgis.org" ) ); + QCoreApplication::setApplicationName( QStringLiteral( "QGIS-TEST" ) ); + QgsApplication::init(); + QgsApplication::initQgis(); +} + +void TestQgsWeakRelation::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsWeakRelation::init() +{ + QLocale::setDefault( QLocale::English ); +} + +void TestQgsWeakRelation::cleanup() +{ + QLocale::setDefault( QLocale::English ); +} + +void TestQgsWeakRelation::testResolved() +{ + QList fieldPairs {{ "fk_province", "pk" }}; + + QgsWeakRelation weakRel( QStringLiteral( "my_relation_id" ), + QStringLiteral( "my_relation_name" ), + QgsRelation::RelationStrength::Association, + QStringLiteral( "referencingLayerId" ), + QStringLiteral( "referencingLayerName" ), + QStringLiteral( "Point?crs=epsg:4326&field=pk:int&field=fk_province:int&field=fk_municipality:int" ), + QStringLiteral( "memory" ), + QStringLiteral( "referencedLayerId" ), + QStringLiteral( "referencedLayerName" ), + QStringLiteral( "Polygon?crs=epsg:4326&field=pk:int&field=province:int&field=municipality:string" ), + QStringLiteral( "memory" ), + fieldPairs + ); + QVERIFY( ! weakRel.resolvedRelation( QgsProject::instance(), QgsVectorLayerRef::MatchType::Name ).isValid() ); + + // create a vector layer + QgsVectorLayer referencedLayer( QStringLiteral( "Polygon?crs=epsg:4326&field=pk:int&field=province:int&field=municipality:string" ), QStringLiteral( "referencedLayerName" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( &referencedLayer, false, false ); + QVERIFY( ! weakRel.resolvedRelation( QgsProject::instance(), QgsVectorLayerRef::MatchType::Name ).isValid() ); + + QgsVectorLayer referencingLayer( QStringLiteral( "Point?crs=epsg:4326&field=pk:int&field=fk_province:int&field=fk_municipality:int" ), QStringLiteral( "referencingLayerName" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( &referencingLayer, false, false ); + QVERIFY( weakRel.resolvedRelation( QgsProject::instance(), QgsVectorLayerRef::MatchType::Name ).isValid() ); + + QVERIFY( weakRel.resolvedRelation( QgsProject::instance(), static_cast( QgsVectorLayerRef::MatchType::Name | QgsVectorLayerRef::MatchType::Provider ) ).isValid() ); + + // This fails because memory provider stores an UUID in the data source definition ... + QVERIFY( !weakRel.resolvedRelation( QgsProject::instance(), static_cast( QgsVectorLayerRef::MatchType::Name | QgsVectorLayerRef::MatchType::Source ) ).isValid() ); + + // ... let's fix it + weakRel.mReferencedLayer.source = referencedLayer.publicSource(); + weakRel.mReferencingLayer.source = referencingLayer.publicSource(); + QVERIFY( weakRel.resolvedRelation( QgsProject::instance(), static_cast( QgsVectorLayerRef::MatchType::Name | QgsVectorLayerRef::MatchType::Source ) ).isValid() ); + + // Just to be sure + QVERIFY( weakRel.resolvedRelation( QgsProject::instance() ).isValid() ); +} + +QGSTEST_MAIN( TestQgsWeakRelation ) +#include "testqgsweakrelation.moc"