Skip to content

Commit be01b7e

Browse files
committed
[FEATURE] Add N:M relation editing possibilities
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)
1 parent 4160097 commit be01b7e

19 files changed

+942
-88
lines changed

ci/travis/linux/before_script.sh

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
printf "[qgis_test]\nhost=localhost\ndbname=qgis_test\nuser=postgres" > ~/.pg_service.conf
22
psql -c 'CREATE DATABASE qgis_test;' -U postgres
33
psql -f $TRAVIS_BUILD_DIR/tests/testdata/provider/testdata.sql -U postgres -d qgis_test
4+
psql -f $TRAVIS_BUILD_DIR/tests/testdata/provider/reltests.sql -U postgres -d qgis_test

python/gui/editorwidgets/core/qgswidgetwrapper.sip

+9-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
***************************************************************************/
1515

1616

17+
// This is required for the ConvertToSubClassCode to work properly
18+
// so RTTI for casting is available in the whole module.
19+
%ModuleCode
20+
#include "qgsrelationwidgetwrapper.h"
21+
%End
22+
1723
/**
1824
* Manages an editor widget
1925
* Widget and wrapper share the same parent
@@ -32,9 +38,10 @@ class QgsWidgetWrapper : QObject
3238
%End
3339

3440
%ConvertToSubClassCode
35-
QgsEditorWidgetWrapper* eww = qobject_cast<QgsEditorWidgetWrapper*>( sipCpp );
36-
if ( eww )
41+
if ( qobject_cast<QgsEditorWidgetWrapper*>( sipCpp ) )
3742
sipType = sipType_QgsEditorWidgetWrapper;
43+
else if ( qobject_cast<QgsRelationWidgetWrapper*>( sipCpp ) )
44+
sipType = sipType_QgsRelationWidgetWrapper;
3845
else
3946
sipType = 0;
4047
%End
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/***************************************************************************
2+
qgsrelationwidgetwrapper.h
3+
--------------------------------------
4+
Date : 14.5.2014
5+
Copyright : (C) 2014 Matthias Kuhn
6+
Email : matthias at opengis dot ch
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
17+
class QgsRelationWidgetWrapper : QgsWidgetWrapper
18+
{
19+
%TypeHeaderCode
20+
#include <qgsrelationwidgetwrapper.h>
21+
%End
22+
23+
public:
24+
explicit QgsRelationWidgetWrapper( QgsVectorLayer* vl, const QgsRelation& relation, QWidget* editor = 0, QWidget* parent /TransferThis/ = 0 );
25+
26+
protected:
27+
QWidget* createWidget( QWidget* parent );
28+
void initWidget( QWidget* editor );
29+
bool valid() const;
30+
31+
public slots:
32+
void setFeature( const QgsFeature& feature );
33+
};

python/gui/gui.sip

+3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
%Include qgsfieldvalidator.sip
7272
%Include qgsfiledropedit.sip
7373
%Include qgsfilterlineedit.sip
74+
%Include qgsfeatureselectiondlg.sip
7475
%Include qgsformannotationitem.sip
7576
%Include qgsgenericprojectionselector.sip
7677
%Include qgsgeometryrubberband.sip
@@ -126,6 +127,7 @@
126127
%Include qgsrasterlayersaveasdialog.sip
127128
%Include qgsrasterpyramidsoptionswidget.sip
128129
%Include qgsrubberband.sip
130+
%Include qgsrelationeditorwidget.sip
129131
%Include qgsscalecombobox.sip
130132
%Include qgsscalerangewidget.sip
131133
%Include qgsscalevisibilitydialog.sip
@@ -231,6 +233,7 @@
231233
%Include editorwidgets/qgsdoublespinbox.sip
232234
%Include editorwidgets/qgsrelationreferencewidget.sip
233235
%Include editorwidgets/qgsrelationreferencewidgetwrapper.sip
236+
%Include editorwidgets/qgsrelationwidgetwrapper.sip
234237
%Include editorwidgets/qgsspinbox.sip
235238
%Include editorwidgets/qgsdatetimeedit.sip
236239

python/gui/qgsfeatureselectiondlg.sip

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/***************************************************************************
2+
qgsfeatureselectiondlg.sip
3+
--------------------------------------
4+
Date : 30.11.2015
5+
Copyright : (C) 2015 Matthias Kuhn
6+
Email : matthias at opengis dot ch
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
// This is required for the ConvertToSubClassCode to work properly
17+
// so RTTI for casting is available in the whole module.
18+
%ModuleCode
19+
#include "qgsfeatureselectiondlg.h"
20+
%End
21+
22+
class QgsFeatureSelectionDlg : QDialog
23+
{
24+
%TypeHeaderCode
25+
#include "qgsfeatureselectiondlg.h"
26+
%End
27+
28+
%ConvertToSubClassCode
29+
if ( qobject_cast<QgsFeatureSelectionDlg*>( sipCpp ) )
30+
sipType = sipType_QgsFeatureSelectionDlg;
31+
else
32+
sipType = 0;
33+
%End
34+
35+
public:
36+
explicit QgsFeatureSelectionDlg( QgsVectorLayer* vl, QgsAttributeEditorContext &context, QWidget *parent /TransferThis/ = 0 );
37+
38+
/**
39+
* Get the selected features
40+
*
41+
* @return The selected feature ids
42+
*/
43+
const QgsFeatureIds& selectedFeatures();
44+
45+
/**
46+
* Set the selected features
47+
* @param ids The feature ids to select
48+
*/
49+
void setSelectedFeatures( const QgsFeatureIds& ids );
50+
};
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/***************************************************************************
2+
qgsrelationeditorwidget.sip
3+
--------------------------------------
4+
Date : 28.11.2015
5+
Copyright : (C) 2015 Matthias Kuhn
6+
Email : matthias at opengis dot ch
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
%ModuleCode
17+
#include "qgsrelationeditorwidget.h"
18+
%End
19+
20+
class QgsRelationEditorWidget : QgsCollapsibleGroupBox
21+
{
22+
%TypeHeaderCode
23+
#include <qgsrelationeditorwidget.h>
24+
%End
25+
26+
27+
%ConvertToSubClassCode
28+
if ( qobject_cast<QgsRelationEditorWidget*>( sipCpp ) )
29+
sipType = sipType_QgsRelationEditorWidget;
30+
else
31+
sipType = 0;
32+
%End
33+
34+
public:
35+
/**
36+
* @param parent parent widget
37+
*/
38+
QgsRelationEditorWidget( QWidget* parent /TransferThis/= 0 );
39+
40+
//! Define the view mode for the dual view
41+
void setViewMode( QgsDualView::ViewMode mode );
42+
43+
//! Get the view mode for the dual view
44+
QgsDualView::ViewMode viewMode();
45+
46+
void setRelationFeature( const QgsRelation& relation, const QgsFeature& feature );
47+
48+
/**
49+
* Set the relation(s) for this widget
50+
* If only one relation is set, it will act as a simple 1:N relation widget
51+
* If both relations are set, it will act as an N:M relation widget
52+
* inserting and deleting entries on the intermediate table as required.
53+
*
54+
* @param relation Relation referencing the edited table
55+
* @param nmrelation Optional reference from the referencing table to a 3rd N:M table
56+
*/
57+
void setRelations( const QgsRelation& relation, const QgsRelation& nmrelation );
58+
59+
void setFeature( const QgsFeature& feature );
60+
61+
void setEditorContext( const QgsAttributeEditorContext& context );
62+
63+
/**
64+
* The feature selection manager is responsible for the selected features
65+
* which are currently being edited.
66+
*/
67+
QgsIFeatureSelectionManager* featureSelectionManager();
68+
};

src/core/qgseditformconfig.cpp

+60
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,32 @@ void QgsEditFormConfig::readXml( const QDomNode& node )
195195
QgsAttributeEditorElement *attributeEditorWidget = attributeEditorElementFromDomElement( elem, this );
196196
addTab( attributeEditorWidget );
197197
}
198+
199+
200+
//// TODO: MAKE THIS MORE GENERIC, SO INDIVIDUALL WIDGETS CAN NOT ONLY SAVE STRINGS
201+
/// SEE QgsEditorWidgetFactory::writeConfig
202+
203+
QDomElement widgetsElem = node.namedItem( "widgets" ).toElement();
204+
205+
QDomNodeList widgetConfigsElems = widgetsElem.childNodes();
206+
207+
for ( int i = 0; i < widgetConfigsElems.size(); ++i )
208+
{
209+
QgsEditorWidgetConfig cfg;
210+
211+
QDomElement wdgElem = widgetConfigsElems.at( i ).toElement();
212+
213+
QDomElement cfgElem = wdgElem.namedItem( "config" ).toElement();
214+
215+
for ( int j = 0; j < cfgElem.attributes().size(); ++j )
216+
{
217+
QDomAttr attr = cfgElem.attributes().item( j ).toAttr();
218+
cfg[attr.name()] = attr.value();
219+
}
220+
221+
setWidgetConfig( wdgElem.attribute( "name" ), cfg );
222+
}
223+
//// END TODO
198224
}
199225

200226
void QgsEditFormConfig::writeXml( QDomNode& node ) const
@@ -262,6 +288,40 @@ void QgsEditFormConfig::writeXml( QDomNode& node ) const
262288

263289
node.appendChild( tabsElem );
264290
}
291+
292+
//// TODO: MAKE THIS MORE GENERIC, SO INDIVIDUALL WIDGETS CAN NOT ONLY SAVE STRINGS
293+
/// SEE QgsEditorWidgetFactory::writeConfig
294+
295+
QDomElement widgetsElem = doc.createElement( "widgets" );
296+
297+
QMap<QString, QgsEditorWidgetConfig >::ConstIterator configIt( mWidgetConfigs.constBegin() );
298+
299+
while ( configIt != mWidgetConfigs.constEnd() )
300+
{
301+
if ( mFields.indexFromName( configIt.key() ) == -1 )
302+
{
303+
QDomElement widgetElem = doc.createElement( "widget" );
304+
widgetElem.setAttribute( "name", configIt.key() );
305+
306+
QDomElement configElem = doc.createElement( "config" );
307+
widgetElem.appendChild( configElem );
308+
309+
QgsEditorWidgetConfig::ConstIterator cfgIt( configIt.value().constBegin() );
310+
311+
while ( cfgIt != configIt.value().constEnd() )
312+
{
313+
configElem.setAttribute( cfgIt.key(), cfgIt.value().toString() );
314+
++cfgIt;
315+
}
316+
317+
widgetsElem.appendChild( widgetElem );
318+
}
319+
++configIt;
320+
}
321+
322+
node.appendChild( widgetsElem );
323+
324+
//// END TODO
265325
}
266326

267327
QgsAttributeEditorElement* QgsEditFormConfig::attributeEditorElementFromDomElement( QDomElement &elem, QObject* parent )

src/gui/attributetable/qgsattributetablemodel.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,8 @@ void QgsAttributeTableModel::loadLayer()
370370
{
371371
++i;
372372

373+
QgsDebugMsg( QString( "Next feature %1" ).arg( i ) );
374+
373375
if ( t.elapsed() > 1000 )
374376
{
375377
bool cancel = false;
@@ -486,6 +488,7 @@ int QgsAttributeTableModel::fieldCol( int idx ) const
486488

487489
int QgsAttributeTableModel::rowCount( const QModelIndex &parent ) const
488490
{
491+
QgsDebugMsg( QString( "Row Count %1" ).arg( mRowIdMap.size() ) );
489492
Q_UNUSED( parent );
490493
return mRowIdMap.size();
491494
}

src/gui/editorwidgets/qgsrelationwidgetwrapper.cpp

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include "qgsrelationeditorwidget.h"
1919
#include "qgsattributeeditorcontext.h"
20+
#include "qgsproject.h"
2021

2122
#include <QWidget>
2223

@@ -35,7 +36,7 @@ QWidget* QgsRelationWidgetWrapper::createWidget( QWidget* parent )
3536
void QgsRelationWidgetWrapper::setFeature( const QgsFeature& feature )
3637
{
3738
if ( mWidget && mRelation.isValid() )
38-
mWidget->setRelationFeature( mRelation, feature );
39+
mWidget->setFeature( feature );
3940
}
4041

4142
void QgsRelationWidgetWrapper::initWidget( QWidget* editor )
@@ -71,6 +72,10 @@ void QgsRelationWidgetWrapper::initWidget( QWidget* editor )
7172
}
7273
while ( ctx );
7374

75+
QgsRelation nmrel = QgsProject::instance()->relationManager()->relation( config( "nm-rel" ).toString() );
76+
77+
w->setRelations( mRelation, nmrel );
78+
7479
mWidget = w;
7580
}
7681

src/gui/qgsattributeform.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ QWidget* QgsAttributeForm::createWidgetFromDef( const QgsAttributeEditorElement
793793
}
794794
QWidget* spacer = new QWidget();
795795
spacer->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Preferred );
796-
gbLayout->addWidget( spacer, index, 0 );
796+
// gbLayout->addWidget( spacer, index, 0 );
797797

798798
labelText = QString::null;
799799
labelOnTop = true;

src/gui/qgscollapsiblegroupbox.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ void QgsCollapsibleGroupBoxBasic::init()
8989

9090
void QgsCollapsibleGroupBoxBasic::showEvent( QShowEvent * event )
9191
{
92-
//QgsDebugMsg( "Entered" );
92+
QgsDebugMsg( "Entered" );
9393
// initialise widget on first show event only
9494
if ( mShown )
9595
{

src/gui/qgsfeatureselectiondlg.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,9 @@ const QgsFeatureIds& QgsFeatureSelectionDlg::selectedFeatures()
4040
return mFeatureSelection->selectedFeaturesIds();
4141
}
4242

43+
void QgsFeatureSelectionDlg::setSelectedFeatures( const QgsFeatureIds& ids )
44+
{
45+
mFeatureSelection->setSelectedFeatures( ids );
46+
}
47+
4348

src/gui/qgsfeatureselectiondlg.h

+11
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,19 @@ class GUI_EXPORT QgsFeatureSelectionDlg : public QDialog, private Ui::QgsFeature
2727
public:
2828
explicit QgsFeatureSelectionDlg( QgsVectorLayer* vl, QgsAttributeEditorContext &context, QWidget *parent = 0 );
2929

30+
/**
31+
* Get the selected features
32+
*
33+
* @return The selected feature ids
34+
*/
3035
const QgsFeatureIds& selectedFeatures();
3136

37+
/**
38+
* Set the selected features
39+
* @param ids The feature ids to select
40+
*/
41+
void setSelectedFeatures( const QgsFeatureIds& ids );
42+
3243
private:
3344
QgsGenericFeatureSelectionManager* mFeatureSelection;
3445
QgsVectorLayer* mVectorLayer;

0 commit comments

Comments
 (0)