Skip to content

Commit

Permalink
Add new configuration option to choose WFS version (GeoNode#226)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ricardo Garcia Silva authored Feb 9, 2022
1 parent afe9b10 commit 55b3efb
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 21 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- Remove unsupported f-string formatting on Python3.7
- Assign `UNKNOWN` as dataset type when the remote does not report it
- Add new WFS version config option and default to WFS v1.1.0



## [0.9.4] - 2022-02-07
Expand Down
9 changes: 9 additions & 0 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ In order to add a new GeoNode connection:
| GeoNode URL | The base URL of the GeoNode being connected to (_e.g._ <https://stable.demo.geonde.org>) |
| Authentication | Whether to use authentication to connect to GeoNode or not. See the [Configure authentication](#configure-authentication) section below for more details on how to configure authenticated access to GeoNode |
| Page size | How many search results per page shall be shown by QGIS. This defaults to `10` |
| WFS version | Which version of the Web Feature Service (WFS) to use for requesting vector layers from the remote GeoNode. Defaults to `v1.1.0`. |

!!! Note
There is currently a bug in QGIS which prevents using WFS version 2.0.0 for editing a vector layer's geometry,

<https://github.com/qgis/QGIS/issues/47254>

Therefore, for the time being, we reccomend using WFS version 1.1.0, which works OK.


5. Optionally you may now click the `Test Connection` button. QGIS will then
try to connect to GeoNode in order to discover what version of GeoNode is
Expand Down
4 changes: 4 additions & 0 deletions src/qgis_geonode/apiclient/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class BaseGeonodeClient(QtCore.QObject):
network_fetcher_task: typing.Optional[network.NetworkRequestTask]
capabilities: typing.List[models.ApiClientCapability]
page_size: int
wfs_version: conf.WfsVersion
network_requests_timeout: int

dataset_list_received = QtCore.pyqtSignal(list, models.GeonodePaginationInfo)
Expand All @@ -38,13 +39,15 @@ def __init__(
self,
base_url: str,
page_size: int,
wfs_version: conf.WfsVersion,
network_requests_timeout: int,
auth_config: typing.Optional[str] = None,
):
super().__init__()
self.auth_config = auth_config or ""
self.base_url = base_url.rstrip("/")
self.page_size = page_size
self.wfs_version = wfs_version
self.network_requests_timeout = network_requests_timeout
self.network_fetcher_task = None

Expand All @@ -53,6 +56,7 @@ def from_connection_settings(cls, connection_settings: conf.ConnectionSettings):
return cls(
base_url=connection_settings.base_url,
page_size=connection_settings.page_size,
wfs_version=connection_settings.wfs_version,
auth_config=connection_settings.auth_config,
network_requests_timeout=connection_settings.network_requests_timeout,
)
Expand Down
6 changes: 5 additions & 1 deletion src/qgis_geonode/apiclient/geonode_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,13 @@ def get_uploader_task(
)

def handle_layer_upload(self, result: bool):
success_statuses = (
200,
201,
)
if result:
response_contents = self.network_fetcher_task.response_contents[0]
if response_contents.http_status_code == 201:
if response_contents.http_status_code in success_statuses:
deserialized = network.deserialize_json_response(
response_contents.response_body
)
Expand Down
12 changes: 12 additions & 0 deletions src/qgis_geonode/conf.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import contextlib
import dataclasses
import enum
import json
import typing
import uuid
Expand Down Expand Up @@ -33,6 +34,13 @@ def _get_network_requests_timeout():
)


class WfsVersion(enum.Enum):
V_1_0_0 = "1.0.0"
V_1_1_0 = "1.1.0"
V_2_0_0 = "2.0.0"
AUTO = "auto"


@dataclasses.dataclass
class ConnectionSettings:
"""Helper class to manage settings for a Connection"""
Expand All @@ -45,6 +53,7 @@ class ConnectionSettings:
default_factory=_get_network_requests_timeout, init=False
)
geonode_version: typing.Optional[packaging_version.Version] = None
wfs_version: typing.Optional[WfsVersion] = WfsVersion.AUTO
auth_config: typing.Optional[str] = None

@classmethod
Expand All @@ -65,6 +74,7 @@ def from_qgs_settings(cls, connection_identifier: str, settings: QgsSettings):
page_size=int(settings.value("page_size", defaultValue=10)),
auth_config=reported_auth_cfg,
geonode_version=geonode_version,
wfs_version=WfsVersion(settings.value("wfs_version", "1.1.0")),
)

def to_json(self):
Expand All @@ -78,6 +88,7 @@ def to_json(self):
"geonode_version": str(self.geonode_version)
if self.geonode_version is not None
else None,
"wfs_version": self.wfs_version.value,
}
)

Expand Down Expand Up @@ -151,6 +162,7 @@ def save_connection_settings(self, connection_settings: ConnectionSettings):
settings.setValue("name", connection_settings.name)
settings.setValue("base_url", connection_settings.base_url)
settings.setValue("page_size", connection_settings.page_size)
settings.setValue("wfs_version", connection_settings.wfs_version.value)
settings.setValue("auth_config", connection_settings.auth_config)
settings.setValue(
"geonode_version",
Expand Down
118 changes: 105 additions & 13 deletions src/qgis_geonode/gui/connection_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
QtWidgets,
QtCore,
QtGui,
QtXml,
)
from qgis.PyQt.uic import loadUiType

from .. import apiclient, network, utils
from ..apiclient.base import BaseGeonodeClient
from ..conf import (
ConnectionSettings,
WfsVersion,
settings_manager,
)
from ..utils import tr
Expand All @@ -32,6 +34,8 @@ class ConnectionDialog(QtWidgets.QDialog, DialogUi):
url_le: QtWidgets.QLineEdit
authcfg_acs: qgis.gui.QgsAuthConfigSelect
page_size_sb: QtWidgets.QSpinBox
wfs_version_cb: QtWidgets.QComboBox
detect_wfs_version_pb: QtWidgets.QPushButton
network_timeout_sb: QtWidgets.QSpinBox
test_connection_pb: QtWidgets.QPushButton
buttonBox: QtWidgets.QDialogButtonBox
Expand Down Expand Up @@ -66,18 +70,23 @@ def __init__(self, connection_settings: typing.Optional[ConnectionSettings] = No
)
self.layout().insertWidget(0, self.bar, alignment=QtCore.Qt.AlignTop)
self.discovery_task = None

self._populate_wfs_version_combobox()
if connection_settings is not None:
self.connection_id = connection_settings.id
self.remote_geonode_version = connection_settings.geonode_version
self.name_le.setText(connection_settings.name)
self.url_le.setText(connection_settings.base_url)
self.authcfg_acs.setConfigId(connection_settings.auth_config)
self.page_size_sb.setValue(connection_settings.page_size)
wfs_version_index = self.wfs_version_cb.findData(
connection_settings.wfs_version
)
self.wfs_version_cb.setCurrentIndex(wfs_version_index)
if self.remote_geonode_version == network.UNSUPPORTED_REMOTE:
self.show_progress(
utils.show_message(
self.bar,
tr("Invalid configuration. Correct GeoNode URL and/or test again."),
message_level=qgis.core.Qgis.Critical,
level=qgis.core.Qgis.Critical,
)
else:
self.connection_id = uuid.uuid4()
Expand All @@ -90,6 +99,7 @@ def __init__(self, connection_settings: typing.Optional[ConnectionSettings] = No
]
for signal in ok_signals:
signal.connect(self.update_ok_buttons)
self.detect_wfs_version_pb.clicked.connect(self.detect_wfs_version)
self.test_connection_pb.clicked.connect(self.test_connection)
# disallow names that have a slash since that is not compatible with how we
# are storing plugin state in QgsSettings
Expand All @@ -98,6 +108,32 @@ def __init__(self, connection_settings: typing.Optional[ConnectionSettings] = No
)
self.update_ok_buttons()

def _populate_wfs_version_combobox(self):
self.wfs_version_cb.clear()
for name, member in WfsVersion.__members__.items():
self.wfs_version_cb.addItem(member.value, member)

def detect_wfs_version(self):
for widget in self._widgets_to_toggle_during_connection_test:
widget.setEnabled(False)
current_settings = self.get_connection_settings()
query = QtCore.QUrlQuery()
query.addQueryItem("service", "WFS")
query.addQueryItem("request", "GetCapabilities")
url = QtCore.QUrl(f"{current_settings.base_url}/gs/ows")
url.setQuery(query)
self.discovery_task = network.NetworkRequestTask(
[network.RequestToPerform(url)],
network_task_timeout=current_settings.network_requests_timeout,
authcfg=current_settings.auth_config,
description="Detect WFS version",
)
self.discovery_task.task_done.connect(self.handle_wfs_version_detection_test)
utils.show_message(
self.bar, tr("Detecting WFS version..."), add_loading_widget=True
)
qgis.core.QgsApplication.taskManager().addTask(self.discovery_task)

def get_connection_settings(self) -> ConnectionSettings:
return ConnectionSettings(
id=self.connection_id,
Expand All @@ -106,6 +142,7 @@ def get_connection_settings(self) -> ConnectionSettings:
auth_config=self.authcfg_acs.configId(),
page_size=self.page_size_sb.value(),
geonode_version=self.remote_geonode_version,
wfs_version=self.wfs_version_cb.currentData(),
)

def test_connection(self):
Expand All @@ -123,7 +160,9 @@ def test_connection(self):
description="Test GeoNode connection",
)
self.discovery_task.task_done.connect(self.handle_discovery_test)
self.show_progress(tr("Testing connection..."), include_progress_bar=True)
utils.show_message(
self.bar, tr("Testing connection..."), add_loading_widget=True
)
qgis.core.QgsApplication.taskManager().addTask(self.discovery_task)

def handle_discovery_test(self, task_result: bool):
Expand All @@ -142,6 +181,40 @@ def handle_discovery_test(self, task_result: bool):
utils.show_message(self.bar, message, level)
self.update_connection_details()

def handle_wfs_version_detection_test(self, task_result: bool):
self.enable_post_test_connection_buttons()
# TODO: set the default to WfsVersion.AUTO when this QGIS issue has been resolved:
#
# https://github.com/qgis/QGIS/issues/47254
#
default_version = WfsVersion.V_1_1_0
version = default_version
if task_result:
response_contents = self.discovery_task.response_contents[0]
if response_contents is not None and response_contents.qt_error is None:
raw_response = response_contents.response_body
detected_versions = _get_wfs_declared_versions(raw_response)
preference_order = [
"1.1.0",
"2.0.0",
"1.0.0",
]
for preference in preference_order:
if preference in detected_versions:
version = WfsVersion(preference)
break
else:
version = default_version
self.bar.clearWidgets()
else:
utils.show_message(
self.bar,
tr("Unable to detect WFS version"),
level=qgis.core.Qgis.Warning,
)
index = self.wfs_version_cb.findData(version)
self.wfs_version_cb.setCurrentIndex(index)

def update_connection_details(self):
invalid_version = (
self.remote_geonode_version is None
Expand Down Expand Up @@ -194,12 +267,31 @@ def update_ok_buttons(self):
self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(enabled_state)
self.test_connection_pb.setEnabled(enabled_state)

def show_progress(
self,
message: str,
message_level: typing.Optional[qgis.core.Qgis] = qgis.core.Qgis.Info,
include_progress_bar: typing.Optional[bool] = False,
):
return utils.show_message(
self.bar, message, message_level, add_loading_widget=include_progress_bar
)

def _get_wfs_declared_versions(raw_response: QtCore.QByteArray) -> typing.List[str]:
"""
Parse capabilities response and retrieve WFS versions supported by the WFS server.
"""

capabilities_doc = QtXml.QDomDocument()
loaded = capabilities_doc.setContent(raw_response, True)
result = []
if loaded:
root = capabilities_doc.documentElement()
if not root.isNull():
operations_meta_elements = root.elementsByTagName("ows:OperationsMetadata")
operations_meta_element = operations_meta_elements.at(0)
if not operations_meta_element.isNull():
for operation_node in operations_meta_element.childNodes():
op_name = operation_node.attributes().namedItem("name").nodeValue()
if op_name == "GetCapabilities":
operation_el = operation_node.toElement()
for par_node in operation_el.elementsByTagName("ows:Parameter"):
param_name = (
par_node.attributes().namedItem("name").nodeValue()
)
if param_name == "AcceptVersions":
param_el = par_node.toElement()
for val_node in param_el.elementsByTagName("ows:Value"):
result.append(val_node.firstChild().nodeValue())
return result
2 changes: 1 addition & 1 deletion src/qgis_geonode/gui/search_result_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ def _load_wfs(self) -> qgis.core.QgsMapLayer:
"srsname": f"EPSG:{self.brief_dataset.srid.postgisSrid()}",
"typename": self.brief_dataset.name,
"url": self.brief_dataset.service_urls[self.service_type].rstrip("/"),
"version": "auto",
"version": self.api_client.wfs_version.value,
}
if self.api_client.auth_config:
params["authcfg"] = self.api_client.auth_config
Expand Down
21 changes: 21 additions & 0 deletions src/qgis_geonode/ui/connection_dialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>WFS version</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QComboBox" name="wfs_version_cb"/>
</item>
<item>
<widget class="QPushButton" name="detect_wfs_version_pb">
<property name="text">
<string>Detect</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
Expand Down
Loading

0 comments on commit 55b3efb

Please sign in to comment.