Skip to content

Commit 185a94f

Browse files
committed
Add new setting about layer primary key field
1 parent a2e9441 commit 185a94f

File tree

8 files changed

+136
-62
lines changed

8 files changed

+136
-62
lines changed

lizmap/dialogs/dock_html_preview.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class HtmlPreview(QDockWidget):
4444
def __init__(self, parent, *__args):
4545
""" Constructor. """
4646
super().__init__(parent, *__args)
47+
self.setObjectName("html_maptip_preview")
4748
self.setWindowTitle("Lizmap HTML Maptip Preview")
4849

4950
self._server_url = None

lizmap/dialogs/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,7 @@ def setup_icons(self):
10131013
self.gb_generalOptions,
10141014
self.gb_interface,
10151015
self.gb_baselayersOptions,
1016+
self.group_box_primary_key,
10161017
self.predefined_groups_legend,
10171018
self.predefined_groups,
10181019
self.predefined_baselayers,

lizmap/forms/base_edition_dialog.py

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
__license__ = 'GPL version 3'
2828
__email__ = '[email protected]'
2929

30-
from lizmap.toolbelt.layer import is_database_layer
30+
from lizmap.widgets.primary_key_field import enable_primary_key_field
3131
from lizmap.widgets.project_tools import is_layer_published_wfs
3232

3333

@@ -471,46 +471,11 @@ def enable_primary_key_field(self):
471471
""" Enable or not the primary key widget.
472472
473473
For a database based layer (PG, SQLite, GPKG) the widget is disabled."""
474-
if not self.layer:
475-
self.primary_key_valid = None
476-
return
477474
if not self.primary_key:
478475
self.primary_key_valid = None
479476
return
480477

481-
tooltip = self.primary_key.toolTip()
482-
extra_tooltip = tr('The primary key is defined by the dataprovider only for layer stored in a database.')
483-
self.primary_key.setToolTip('{} {}'.format(tooltip, extra_tooltip))
484-
485-
layer = self.layer.currentLayer()
486-
if not is_database_layer(layer):
487-
self.primary_key.setEnabled(True)
488-
self.primary_key.setAllowEmptyFieldName(False)
489-
self.primary_key_valid = None
490-
return
491-
492-
# We trust the datasource
493-
# And we do not trust the legacy CFG
494-
self.primary_key.setEnabled(False)
495-
self.primary_key.setAllowEmptyFieldName(True)
496-
pks = layer.primaryKeyAttributes()
497-
if len(pks) == 0:
498-
# Must be an issue for the user to validate the form, because the widget is disabled
499-
# The datasource must be fixed
500-
self.primary_key_valid = False
501-
return
502-
503-
if len(pks) >= 2:
504-
# As well, the user must add a PK and an unicity constraint
505-
# Not possible to validate the form
506-
self.primary_key_valid = False
507-
return
508-
509-
# Single field as a primary key
510-
# We do not trust the CFG anymore, let's go datasource
511-
name = layer.fields().at(pks[0]).name()
512-
self.primary_key.setField(name)
513-
self.primary_key_valid = True
478+
self.primary_key_valid = enable_primary_key_field(self.primary_key, self.layer.currentLayer())
514479

515480
def open_wizard_dialog(self, helper: str):
516481
""" Internal function to open the wizard ACL. """

lizmap/lizmap_api/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ def __init__(self, project, fix_json=False):
269269

270270
# We want to translate some items, do not make this variable static
271271
self.layerOptionDefinitions = {
272+
'primary_key': {
273+
'wType': 'list', 'type': 'field', 'default': '',
274+
},
272275
'title': {
273276
'wType': 'text', 'type': 'string', 'default': '', 'isMetadata': True
274277
},

lizmap/plugin.py

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
from lizmap.table_manager.layouts import TableManagerLayouts
166166
from lizmap.toolbelt.convert import cast_to_group, cast_to_layer
167167
from lizmap.widgets.check_project import Check, SourceField
168+
from lizmap.widgets.primary_key_field import enable_primary_key_field
168169
from lizmap.widgets.project_tools import (
169170
empty_baselayers,
170171
is_layer_published_wfs,
@@ -529,6 +530,7 @@ def write_log_message(message, tag, level):
529530

530531
self.layer_options_list = lizmap_config.layerOptionDefinitions
531532
# Add widget information
533+
self.layer_options_list['primary_key']['widget'] = self.dlg.primary_key
532534
self.layer_options_list['title']['widget'] = self.dlg.inLayerTitle
533535
self.layer_options_list['abstract']['widget'] = self.dlg.teLayerAbstract
534536
self.layer_options_list['link']['widget'] = self.dlg.inLayerLink
@@ -699,23 +701,30 @@ def write_log_message(message, tag, level):
699701

700702
# Connect widget signals to setLayerProperty method depending on widget type
701703
for key, item in self.layer_options_list.items():
702-
if item.get('widget'):
703-
control = item['widget']
704-
slot = partial(self.save_value_layer_group_data, key)
705-
if item['wType'] in ('text', 'spinbox'):
706-
control.editingFinished.connect(slot)
707-
elif item['wType'] == 'textarea':
708-
control.textChanged.connect(slot)
709-
elif item['wType'] == 'checkbox':
710-
control.stateChanged.connect(slot)
711-
elif item['wType'] == 'radio':
712-
control.toggled.connect(slot)
713-
elif item['wType'] == 'list':
714-
control.currentIndexChanged.connect(slot)
715-
elif item['wType'] == 'layers':
716-
control.layerChanged.connect(slot)
717-
elif item['wType'] == 'fields':
704+
control = item.get('widget')
705+
if not isinstance(control, QWidget):
706+
# Be careful, big waste of time if control is not checked to be a QWidget for QgsFieldComboBox
707+
# https://github.com/qgis/QGIS/issues/62317
708+
continue
709+
710+
slot = partial(self.save_value_layer_group_data, key)
711+
if item['wType'] in ('text', 'spinbox'):
712+
control.editingFinished.connect(slot)
713+
elif item['wType'] == 'textarea':
714+
control.textChanged.connect(slot)
715+
elif item['wType'] == 'checkbox':
716+
control.stateChanged.connect(slot)
717+
elif item['wType'] == 'radio':
718+
control.toggled.connect(slot)
719+
elif item['wType'] == 'list':
720+
if item['type'] == 'field':
718721
control.fieldChanged.connect(slot)
722+
else:
723+
control.currentIndexChanged.connect(slot)
724+
elif item['wType'] == 'layers':
725+
control.layerChanged.connect(slot)
726+
elif item['wType'] == 'fields':
727+
control.fieldChanged.connect(slot)
719728

720729
self.crs_3857_base_layers_list = {
721730
'osm-mapnik': self.dlg.cbOsmMapnik,
@@ -2243,9 +2252,13 @@ def set_tree_item_data(self, item_type, item_key, json_layers):
22432252
self.myDic[item_key][key] = json_layers[json_key][key]
22442253
# lists
22452254
elif item['wType'] == 'list':
2246-
# New way with data, label, tooltip and icon
2247-
datas = [j[0] for j in item['list']]
2248-
if json_layers[json_key][key] in datas:
2255+
if item.get('list'):
2256+
# New way with data, label, tooltip and icon
2257+
datas = [j[0] for j in item['list']]
2258+
if json_layers[json_key][key] in datas:
2259+
self.myDic[item_key][key] = json_layers[json_key][key]
2260+
else:
2261+
# The list is managed by the layer and function enable_primary_key_field()
22492262
self.myDic[item_key][key] = json_layers[json_key][key]
22502263

22512264
else:
@@ -2414,6 +2427,8 @@ def from_data_to_ui_for_layer_group(self):
24142427
# if val.get('widget'):
24152428
# val.get('widget').setEnabled(True)
24162429

2430+
self.dlg.group_box_primary_key.setVisible(False)
2431+
24172432
i_key = self._current_selected_item_in_config()
24182433
if i_key:
24192434
self.enable_check_box_in_layer_tab(True)
@@ -2501,6 +2516,11 @@ def from_data_to_ui_for_layer_group(self):
25012516
self.dlg.button_generate_html_table.setEnabled(is_vector)
25022517
self.layer_options_list['popupSource']['widget'].setEnabled(is_vector)
25032518

2519+
if is_vector:
2520+
self.dlg.primary_key.setLayer(layer)
2521+
enable_primary_key_field(self.dlg.primary_key, layer)
2522+
self.dlg.group_box_primary_key.setVisible(is_vector)
2523+
25042524
if self.current_lwc_version() >= LwcVersions.Lizmap_3_7 and not self.dlg.cbLayerIsBaseLayer.isChecked():
25052525
# Starting from LWC 3.7, this checkbox is deprecated
25062526
self.dlg.cbLayerIsBaseLayer.setEnabled(False)
@@ -2555,9 +2575,13 @@ def from_data_to_ui_for_layer_group(self):
25552575
val['widget'].setChecked(val['default'])
25562576
elif val['wType'] == 'list':
25572577

2558-
# New way with data, label, tooltip and icon
2559-
index = val['widget'].findData(val['default'])
2560-
val['widget'].setCurrentIndex(index)
2578+
if val.get('default'):
2579+
# New way with data, label, tooltip and icon
2580+
index = val['widget'].findData(val['default'])
2581+
val['widget'].setCurrentIndex(index)
2582+
else:
2583+
# The list is managed by the layer and function enable_primary_key_field()
2584+
val['widget'].setCurrentIndex(0)
25612585

25622586
self.enable_popup_source_button()
25632587
self.dlg.follow_map_theme_toggled()
@@ -2806,9 +2830,13 @@ def save_value_layer_group_data(self, key: str):
28062830
if self.layer_options_list[children]['widget'].isChecked():
28072831
self.layer_options_list[children]['widget'].setChecked(False)
28082832
elif layer_option['wType'] == 'list':
2809-
# New way with data, label, tooltip and icon
2810-
datas = [j[0] for j in layer_option['list']]
2811-
self.layerList[layer_or_group_text][key] = datas[layer_option['widget'].currentIndex()]
2833+
if layer_option.get('list'):
2834+
# New way with data, label, tooltip and icon
2835+
datas = [j[0] for j in layer_option['list']]
2836+
self.layerList[layer_or_group_text][key] = datas[layer_option['widget'].currentIndex()]
2837+
else:
2838+
# The list is managed by the layer and function enable_primary_key_field()
2839+
self.layerList[layer_or_group_text][key] = layer_option['widget'].currentField()
28122840

28132841
# Deactivate the "exclude" widget if necessary
28142842
if 'exclude' in layer_option \

lizmap/resources/ui/ui_lizmap.ui

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,28 @@ This is different to the map maximum extent (defined in QGIS project properties,
15581558
</rect>
15591559
</property>
15601560
<layout class="QVBoxLayout" name="verticalLayout_9">
1561+
<item>
1562+
<widget class="QGroupBox" name="group_box_primary_key">
1563+
<property name="title">
1564+
<string>Datasource</string>
1565+
</property>
1566+
<layout class="QFormLayout" name="formLayout_11">
1567+
<item row="0" column="0">
1568+
<widget class="QLabel" name="label_55">
1569+
<property name="text">
1570+
<string>Primary key</string>
1571+
</property>
1572+
</widget>
1573+
</item>
1574+
<item row="0" column="1">
1575+
<widget class="QgsFieldComboBox" name="primary_key"/>
1576+
</item>
1577+
</layout>
1578+
</widget>
1579+
</item>
1580+
<item>
1581+
<layout class="QFormLayout" name="formLayout_10"/>
1582+
</item>
15611583
<item>
15621584
<widget class="QgsCollapsibleGroupBox" name="group_layer_metadata">
15631585
<property name="title">

lizmap/test/test_ui.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ def test_legend_options(self):
8282
'hide_at_startup',
8383
lizmap.myDic.get('legend_hidden_startup_layer_id').get('legend_image_option'))
8484

85+
self.assertEqual(
86+
'id',
87+
lizmap.myDic.get('legend_hidden_startup_layer_id').get('primary_key'))
88+
8589
# For LWC 3.6
8690
output = lizmap.project_config_file(LwcVersions.Lizmap_3_6, check_server=False, ignore_error=True)
8791
self.assertTrue('<table>' in output['options']['datavizTemplate'])
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
__copyright__ = 'Copyright 2025, 3Liz'
2+
__license__ = 'GPL version 3'
3+
__email__ = '[email protected]'
4+
5+
from typing import Optional
6+
7+
from qgis.core import QgsVectorLayer
8+
from qgis.gui import QgsFieldComboBox
9+
10+
from lizmap.toolbelt.i18n import tr
11+
from lizmap.toolbelt.layer import is_database_layer
12+
13+
14+
def enable_primary_key_field(primary_key: QgsFieldComboBox, layer: Optional[QgsVectorLayer]) -> Optional[bool]:
15+
""" Enable or not the primary key widget.
16+
17+
For a database based layer (PG, SQLite, GPKG) the widget is disabled."""
18+
if not layer:
19+
return None
20+
21+
tooltip = primary_key.toolTip()
22+
extra_tooltip = tr('The primary key is defined by the dataprovider only for layer stored in a database.')
23+
if extra_tooltip not in tooltip:
24+
primary_key.setToolTip('{} {}'.format(tooltip, extra_tooltip))
25+
26+
if not is_database_layer(layer):
27+
primary_key.setEnabled(True)
28+
primary_key.setAllowEmptyFieldName(False)
29+
return None
30+
31+
# We trust the datasource
32+
# And we do not trust the legacy CFG
33+
primary_key.setEnabled(False)
34+
primary_key.setAllowEmptyFieldName(True)
35+
pks = layer.primaryKeyAttributes()
36+
if len(pks) == 0:
37+
# Must be an issue for the user to validate the form, because the widget is disabled
38+
# The datasource must be fixed
39+
return False
40+
41+
if len(pks) >= 2:
42+
# As well, the user must add a PK and an unicity constraint
43+
# Not possible to validate the form
44+
return False
45+
46+
# Single field as a primary key
47+
# We do not trust the CFG anymore, let's go datasource
48+
name = layer.fields().at(pks[0]).name()
49+
primary_key.setField(name)
50+
return True

0 commit comments

Comments
 (0)