Skip to content

Commit

Permalink
[FEATURE] Add N:M relation editing possibilities
Browse files Browse the repository at this point in the history
This adds the possibility to manage data on a normalized relational
database in N:M relations. On the relation editor in a form, the tools
to add, delete, link and unlink work (also) on the linking table if a
relation is visualized as N:M relation.

Configuration is done through the fields tab where on the relation a
second relation can be chosen (if there is a suitable relation in terms
of a second relation on the linking table).

Limitations
===========

QGIS is not a database management system.

It is based on assumptions about the underlying database system. In
particular it expects

 * A `ON DELETE CASCADE` or similar measure on the second relation
 * Does not take care of setting the primary key when adding features.
   Either users need to be instructed to set them manually or - if it's a
   database derived value - the layers need to be in transaction mode
   (currently only activatable through the API)
  • Loading branch information
m-kuhn committed Dec 8, 2015
1 parent 4160097 commit be01b7e
Show file tree
Hide file tree
Showing 19 changed files with 942 additions and 88 deletions.
1 change: 1 addition & 0 deletions ci/travis/linux/before_script.sh
@@ -1,3 +1,4 @@
printf "[qgis_test]\nhost=localhost\ndbname=qgis_test\nuser=postgres" > ~/.pg_service.conf
psql -c 'CREATE DATABASE qgis_test;' -U postgres
psql -f $TRAVIS_BUILD_DIR/tests/testdata/provider/testdata.sql -U postgres -d qgis_test
psql -f $TRAVIS_BUILD_DIR/tests/testdata/provider/reltests.sql -U postgres -d qgis_test
11 changes: 9 additions & 2 deletions python/gui/editorwidgets/core/qgswidgetwrapper.sip
Expand Up @@ -14,6 +14,12 @@
***************************************************************************/


// This is required for the ConvertToSubClassCode to work properly
// so RTTI for casting is available in the whole module.
%ModuleCode
#include "qgsrelationwidgetwrapper.h"
%End

/**
* Manages an editor widget
* Widget and wrapper share the same parent
Expand All @@ -32,9 +38,10 @@ class QgsWidgetWrapper : QObject
%End

%ConvertToSubClassCode
QgsEditorWidgetWrapper* eww = qobject_cast<QgsEditorWidgetWrapper*>( sipCpp );
if ( eww )
if ( qobject_cast<QgsEditorWidgetWrapper*>( sipCpp ) )
sipType = sipType_QgsEditorWidgetWrapper;
else if ( qobject_cast<QgsRelationWidgetWrapper*>( sipCpp ) )
sipType = sipType_QgsRelationWidgetWrapper;
else
sipType = 0;
%End
Expand Down
33 changes: 33 additions & 0 deletions python/gui/editorwidgets/qgsrelationwidgetwrapper.sip
@@ -0,0 +1,33 @@
/***************************************************************************
qgsrelationwidgetwrapper.h
--------------------------------------
Date : 14.5.2014
Copyright : (C) 2014 Matthias Kuhn
Email : matthias at opengis dot ch
***************************************************************************
* *
* 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. *
* *
***************************************************************************/


class QgsRelationWidgetWrapper : QgsWidgetWrapper
{
%TypeHeaderCode
#include <qgsrelationwidgetwrapper.h>
%End

public:
explicit QgsRelationWidgetWrapper( QgsVectorLayer* vl, const QgsRelation& relation, QWidget* editor = 0, QWidget* parent /TransferThis/ = 0 );

protected:
QWidget* createWidget( QWidget* parent );
void initWidget( QWidget* editor );
bool valid() const;

public slots:
void setFeature( const QgsFeature& feature );
};
3 changes: 3 additions & 0 deletions python/gui/gui.sip
Expand Up @@ -71,6 +71,7 @@
%Include qgsfieldvalidator.sip
%Include qgsfiledropedit.sip
%Include qgsfilterlineedit.sip
%Include qgsfeatureselectiondlg.sip
%Include qgsformannotationitem.sip
%Include qgsgenericprojectionselector.sip
%Include qgsgeometryrubberband.sip
Expand Down Expand Up @@ -126,6 +127,7 @@
%Include qgsrasterlayersaveasdialog.sip
%Include qgsrasterpyramidsoptionswidget.sip
%Include qgsrubberband.sip
%Include qgsrelationeditorwidget.sip
%Include qgsscalecombobox.sip
%Include qgsscalerangewidget.sip
%Include qgsscalevisibilitydialog.sip
Expand Down Expand Up @@ -231,6 +233,7 @@
%Include editorwidgets/qgsdoublespinbox.sip
%Include editorwidgets/qgsrelationreferencewidget.sip
%Include editorwidgets/qgsrelationreferencewidgetwrapper.sip
%Include editorwidgets/qgsrelationwidgetwrapper.sip
%Include editorwidgets/qgsspinbox.sip
%Include editorwidgets/qgsdatetimeedit.sip

Expand Down
50 changes: 50 additions & 0 deletions python/gui/qgsfeatureselectiondlg.sip
@@ -0,0 +1,50 @@
/***************************************************************************
qgsfeatureselectiondlg.sip
--------------------------------------
Date : 30.11.2015
Copyright : (C) 2015 Matthias Kuhn
Email : matthias at opengis dot ch
***************************************************************************
* *
* 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. *
* *
***************************************************************************/

// This is required for the ConvertToSubClassCode to work properly
// so RTTI for casting is available in the whole module.
%ModuleCode
#include "qgsfeatureselectiondlg.h"
%End

class QgsFeatureSelectionDlg : QDialog
{
%TypeHeaderCode
#include "qgsfeatureselectiondlg.h"
%End

%ConvertToSubClassCode
if ( qobject_cast<QgsFeatureSelectionDlg*>( sipCpp ) )
sipType = sipType_QgsFeatureSelectionDlg;
else
sipType = 0;
%End

public:
explicit QgsFeatureSelectionDlg( QgsVectorLayer* vl, QgsAttributeEditorContext &context, QWidget *parent /TransferThis/ = 0 );

/**
* Get the selected features
*
* @return The selected feature ids
*/
const QgsFeatureIds& selectedFeatures();

/**
* Set the selected features
* @param ids The feature ids to select
*/
void setSelectedFeatures( const QgsFeatureIds& ids );
};
68 changes: 68 additions & 0 deletions python/gui/qgsrelationeditorwidget.sip
@@ -0,0 +1,68 @@
/***************************************************************************
qgsrelationeditorwidget.sip
--------------------------------------
Date : 28.11.2015
Copyright : (C) 2015 Matthias Kuhn
Email : matthias at opengis dot ch
***************************************************************************
* *
* 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. *
* *
***************************************************************************/

%ModuleCode
#include "qgsrelationeditorwidget.h"
%End

class QgsRelationEditorWidget : QgsCollapsibleGroupBox
{
%TypeHeaderCode
#include <qgsrelationeditorwidget.h>
%End


%ConvertToSubClassCode
if ( qobject_cast<QgsRelationEditorWidget*>( sipCpp ) )
sipType = sipType_QgsRelationEditorWidget;
else
sipType = 0;
%End

public:
/**
* @param parent parent widget
*/
QgsRelationEditorWidget( QWidget* parent /TransferThis/= 0 );

//! Define the view mode for the dual view
void setViewMode( QgsDualView::ViewMode mode );

//! Get the view mode for the dual view
QgsDualView::ViewMode viewMode();

void setRelationFeature( const QgsRelation& relation, const QgsFeature& feature );

/**
* Set the relation(s) for this widget
* If only one relation is set, it will act as a simple 1:N relation widget
* If both relations are set, it will act as an N:M relation widget
* inserting and deleting entries on the intermediate table as required.
*
* @param relation Relation referencing the edited table
* @param nmrelation Optional reference from the referencing table to a 3rd N:M table
*/
void setRelations( const QgsRelation& relation, const QgsRelation& nmrelation );

void setFeature( const QgsFeature& feature );

void setEditorContext( const QgsAttributeEditorContext& context );

/**
* The feature selection manager is responsible for the selected features
* which are currently being edited.
*/
QgsIFeatureSelectionManager* featureSelectionManager();
};
60 changes: 60 additions & 0 deletions src/core/qgseditformconfig.cpp
Expand Up @@ -195,6 +195,32 @@ void QgsEditFormConfig::readXml( const QDomNode& node )
QgsAttributeEditorElement *attributeEditorWidget = attributeEditorElementFromDomElement( elem, this );
addTab( attributeEditorWidget );
}


//// TODO: MAKE THIS MORE GENERIC, SO INDIVIDUALL WIDGETS CAN NOT ONLY SAVE STRINGS
/// SEE QgsEditorWidgetFactory::writeConfig

QDomElement widgetsElem = node.namedItem( "widgets" ).toElement();

QDomNodeList widgetConfigsElems = widgetsElem.childNodes();

for ( int i = 0; i < widgetConfigsElems.size(); ++i )
{
QgsEditorWidgetConfig cfg;

QDomElement wdgElem = widgetConfigsElems.at( i ).toElement();

QDomElement cfgElem = wdgElem.namedItem( "config" ).toElement();

for ( int j = 0; j < cfgElem.attributes().size(); ++j )
{
QDomAttr attr = cfgElem.attributes().item( j ).toAttr();
cfg[attr.name()] = attr.value();
}

setWidgetConfig( wdgElem.attribute( "name" ), cfg );
}
//// END TODO
}

void QgsEditFormConfig::writeXml( QDomNode& node ) const
Expand Down Expand Up @@ -262,6 +288,40 @@ void QgsEditFormConfig::writeXml( QDomNode& node ) const

node.appendChild( tabsElem );
}

//// TODO: MAKE THIS MORE GENERIC, SO INDIVIDUALL WIDGETS CAN NOT ONLY SAVE STRINGS
/// SEE QgsEditorWidgetFactory::writeConfig

QDomElement widgetsElem = doc.createElement( "widgets" );

QMap<QString, QgsEditorWidgetConfig >::ConstIterator configIt( mWidgetConfigs.constBegin() );

while ( configIt != mWidgetConfigs.constEnd() )
{
if ( mFields.indexFromName( configIt.key() ) == -1 )
{
QDomElement widgetElem = doc.createElement( "widget" );
widgetElem.setAttribute( "name", configIt.key() );

QDomElement configElem = doc.createElement( "config" );
widgetElem.appendChild( configElem );

QgsEditorWidgetConfig::ConstIterator cfgIt( configIt.value().constBegin() );

while ( cfgIt != configIt.value().constEnd() )
{
configElem.setAttribute( cfgIt.key(), cfgIt.value().toString() );
++cfgIt;
}

widgetsElem.appendChild( widgetElem );
}
++configIt;
}

node.appendChild( widgetsElem );

//// END TODO
}

QgsAttributeEditorElement* QgsEditFormConfig::attributeEditorElementFromDomElement( QDomElement &elem, QObject* parent )
Expand Down
3 changes: 3 additions & 0 deletions src/gui/attributetable/qgsattributetablemodel.cpp
Expand Up @@ -370,6 +370,8 @@ void QgsAttributeTableModel::loadLayer()
{
++i;

QgsDebugMsg( QString( "Next feature %1" ).arg( i ) );

if ( t.elapsed() > 1000 )
{
bool cancel = false;
Expand Down Expand Up @@ -486,6 +488,7 @@ int QgsAttributeTableModel::fieldCol( int idx ) const

int QgsAttributeTableModel::rowCount( const QModelIndex &parent ) const
{
QgsDebugMsg( QString( "Row Count %1" ).arg( mRowIdMap.size() ) );
Q_UNUSED( parent );
return mRowIdMap.size();
}
Expand Down
7 changes: 6 additions & 1 deletion src/gui/editorwidgets/qgsrelationwidgetwrapper.cpp
Expand Up @@ -17,6 +17,7 @@

#include "qgsrelationeditorwidget.h"
#include "qgsattributeeditorcontext.h"
#include "qgsproject.h"

#include <QWidget>

Expand All @@ -35,7 +36,7 @@ QWidget* QgsRelationWidgetWrapper::createWidget( QWidget* parent )
void QgsRelationWidgetWrapper::setFeature( const QgsFeature& feature )
{
if ( mWidget && mRelation.isValid() )
mWidget->setRelationFeature( mRelation, feature );
mWidget->setFeature( feature );
}

void QgsRelationWidgetWrapper::initWidget( QWidget* editor )
Expand Down Expand Up @@ -71,6 +72,10 @@ void QgsRelationWidgetWrapper::initWidget( QWidget* editor )
}
while ( ctx );

QgsRelation nmrel = QgsProject::instance()->relationManager()->relation( config( "nm-rel" ).toString() );

w->setRelations( mRelation, nmrel );

mWidget = w;
}

Expand Down
2 changes: 1 addition & 1 deletion src/gui/qgsattributeform.cpp
Expand Up @@ -793,7 +793,7 @@ QWidget* QgsAttributeForm::createWidgetFromDef( const QgsAttributeEditorElement
}
QWidget* spacer = new QWidget();
spacer->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Preferred );
gbLayout->addWidget( spacer, index, 0 );
// gbLayout->addWidget( spacer, index, 0 );

labelText = QString::null;
labelOnTop = true;
Expand Down
2 changes: 1 addition & 1 deletion src/gui/qgscollapsiblegroupbox.cpp
Expand Up @@ -89,7 +89,7 @@ void QgsCollapsibleGroupBoxBasic::init()

void QgsCollapsibleGroupBoxBasic::showEvent( QShowEvent * event )
{
//QgsDebugMsg( "Entered" );
QgsDebugMsg( "Entered" );
// initialise widget on first show event only
if ( mShown )
{
Expand Down
5 changes: 5 additions & 0 deletions src/gui/qgsfeatureselectiondlg.cpp
Expand Up @@ -40,4 +40,9 @@ const QgsFeatureIds& QgsFeatureSelectionDlg::selectedFeatures()
return mFeatureSelection->selectedFeaturesIds();
}

void QgsFeatureSelectionDlg::setSelectedFeatures( const QgsFeatureIds& ids )
{
mFeatureSelection->setSelectedFeatures( ids );
}


11 changes: 11 additions & 0 deletions src/gui/qgsfeatureselectiondlg.h
Expand Up @@ -27,8 +27,19 @@ class GUI_EXPORT QgsFeatureSelectionDlg : public QDialog, private Ui::QgsFeature
public:
explicit QgsFeatureSelectionDlg( QgsVectorLayer* vl, QgsAttributeEditorContext &context, QWidget *parent = 0 );

/**
* Get the selected features
*
* @return The selected feature ids
*/
const QgsFeatureIds& selectedFeatures();

/**
* Set the selected features
* @param ids The feature ids to select
*/
void setSelectedFeatures( const QgsFeatureIds& ids );

private:
QgsGenericFeatureSelectionManager* mFeatureSelection;
QgsVectorLayer* mVectorLayer;
Expand Down

0 comments on commit be01b7e

Please sign in to comment.