diff --git a/src/qgis_geonode/apiclient/base.py b/src/qgis_geonode/apiclient/base.py
index 3b75c030..7757f5d3 100644
--- a/src/qgis_geonode/apiclient/base.py
+++ b/src/qgis_geonode/apiclient/base.py
@@ -23,11 +23,10 @@ class BaseGeonodeClient(QtCore.QObject):
dataset_list_received = QtCore.pyqtSignal(list, models.GeonodePaginationInfo)
dataset_detail_received = QtCore.pyqtSignal(object)
+ dataset_detail_error_received = QtCore.pyqtSignal([str], [str, int, str])
style_detail_received = QtCore.pyqtSignal(QtXml.QDomElement)
keyword_list_received = QtCore.pyqtSignal(list)
-
search_error_received = QtCore.pyqtSignal([str], [str, int, str])
- dataset_detail_error_received = QtCore.pyqtSignal([str], [str, int, str])
def __init__(
self,
diff --git a/src/qgis_geonode/apiclient/version_postv2.py b/src/qgis_geonode/apiclient/version_postv2.py
index 4d6abd99..daad1a5c 100644
--- a/src/qgis_geonode/apiclient/version_postv2.py
+++ b/src/qgis_geonode/apiclient/version_postv2.py
@@ -26,6 +26,7 @@ class GeonodePostV2ApiClient(BaseGeonodeClient):
models.ApiClientCapability.FILTER_BY_TEMPORAL_EXTENT,
models.ApiClientCapability.LOAD_LAYER_METADATA,
models.ApiClientCapability.LOAD_VECTOR_LAYER_STYLE,
+ models.ApiClientCapability.MODIFY_LAYER_METADATA,
# NOTE: loading raster layer style is not present here
# because QGIS does not currently support loading SLD for raster layers
models.ApiClientCapability.MODIFY_VECTOR_LAYER_STYLE,
@@ -265,7 +266,7 @@ def _get_common_model_properties(raw_dataset: typing.Dict) -> typing.Dict:
"title": raw_dataset.get("title", ""),
"abstract": raw_dataset.get("raw_abstract", ""),
"thumbnail_url": raw_dataset["thumbnail_url"],
- "link": raw_dataset.get("link"),
+ "link": raw_dataset["link"],
"detail_url": raw_dataset["detail_url"],
"dataset_sub_type": type_,
"service_urls": service_urls,
diff --git a/src/qgis_geonode/gui/geonode_map_layer_config_widget.py b/src/qgis_geonode/gui/geonode_map_layer_config_widget.py
index a86575fa..a8f9a21e 100644
--- a/src/qgis_geonode/gui/geonode_map_layer_config_widget.py
+++ b/src/qgis_geonode/gui/geonode_map_layer_config_widget.py
@@ -1,3 +1,4 @@
+import json
import typing
import xml.etree.ElementTree as ET
from pathlib import Path
@@ -25,6 +26,7 @@
get_geonode_client,
models,
)
+from ..metadata import populate_metadata
from ..utils import (
log,
)
@@ -35,12 +37,15 @@
class GeonodeMapLayerConfigWidget(qgis.gui.QgsMapLayerConfigWidget, WidgetUi):
download_style_pb: QtWidgets.QPushButton
upload_style_pb: QtWidgets.QPushButton
+ download_metadata_pb: QtWidgets.QPushButton
+ upload_metadata_pb: QtWidgets.QPushButton
open_detail_url_pb: QtWidgets.QPushButton
open_link_url_pb: QtWidgets.QPushButton
message_bar: qgis.gui.QgsMessageBar
network_task: typing.Optional[network.NetworkRequestTask]
_apply_geonode_style: bool
+ _apply_geonode_metadata: bool
@property
def connection_settings(self) -> typing.Optional[conf.ConnectionSettings]:
@@ -83,17 +88,22 @@ def __init__(self, layer, canvas, parent):
self.layout().insertWidget(0, self.message_bar)
self.network_task = None
self._apply_geonode_style = False
+ self._apply_geonode_metadata = False
self.layer = layer
self._toggle_style_controls(enabled=False)
self._toggle_link_controls(enabled=False)
+ self._toggle_metadata_controls(enabled=False)
if self.layer.customProperty(models.DATASET_CUSTOM_PROPERTY_KEY) is not None:
# this layer came from GeoNode
self.download_style_pb.clicked.connect(self.download_style)
self.upload_style_pb.clicked.connect(self.upload_style)
self.open_detail_url_pb.clicked.connect(self.open_detail_url)
self.open_link_url_pb.clicked.connect(self.open_link_url)
+ self.download_metadata_pb.clicked.connect(self.download_metadata)
+ self.upload_metadata_pb.clicked.connect(self.upload_metadata)
self._toggle_style_controls(enabled=True)
self._toggle_link_controls(enabled=True)
+ self._toggle_metadata_controls(enabled=True)
else: # this is not a GeoNode layer
pass
@@ -102,6 +112,9 @@ def apply(self):
if self._apply_geonode_style:
self._apply_sld()
self._apply_geonode_style = False
+ if self._apply_geonode_metadata:
+ self._apply_metadata()
+ self._apply_geonode_metadata = False
def get_dataset(self) -> typing.Optional[models.Dataset]:
serialized_dataset = self.layer.customProperty(
@@ -115,7 +128,7 @@ def get_dataset(self) -> typing.Optional[models.Dataset]:
result = None
return result
- def update_dataset(self, new_dataset: models.Dataset):
+ def update_dataset(self, new_dataset: models.Dataset) -> None:
serialized = new_dataset.to_json()
self.layer.setCustomProperty(models.DATASET_CUSTOM_PROPERTY_KEY, serialized)
@@ -176,10 +189,10 @@ def upload_style(self):
network_task_timeout=self.api_client.network_requests_timeout,
description="Upload dataset style to GeoNode",
)
- qgis.core.QgsApplication.taskManager().addTask(self.network_task)
self.network_task.task_done.connect(self.handle_style_uploaded)
self._toggle_style_controls(enabled=False)
self._show_message(message="Uploading style...", add_loading_widget=True)
+ qgis.core.QgsApplication.taskManager().addTask(self.network_task)
def _prepare_style_for_upload(self) -> typing.Optional[typing.Tuple[str, str]]:
doc = QtXml.QDomDocument()
@@ -240,7 +253,7 @@ def _prepare_raster_style_for_upload(
content_type = "application/vnd.ogc.sld+xml"
return new_serialized, content_type
- def handle_style_uploaded(self, task_result: bool):
+ def handle_style_uploaded(self, task_result: bool) -> None:
self._toggle_style_controls(enabled=True)
if task_result:
parsed_reply = self.network_task.response_contents[0]
@@ -262,6 +275,97 @@ def handle_style_uploaded(self, task_result: bool):
level=qgis.core.Qgis.Warning,
)
+ def download_metadata(self) -> None:
+ """Initiate download of metadata from the remote GeoNode.
+
+ The process of updating a QGIS layer's metadata from the corresponding GeoNode
+ dataset involves the following steps:
+
+ 1. Perform a network request in order to retrieve the updated metadata.
+ 2. Update our internal representation of the remote dataset with the retrieved
+ metadata.
+ 3. When the QGIS layer properties dialogue has its apply button clicked, update
+ the QGIS layer metadata with the relevant information.
+
+ """
+
+ dataset = self.get_dataset()
+ api_client = self.api_client
+ api_client.dataset_detail_received.connect(self.handle_metadata_downloaded)
+ api_client.dataset_detail_error_received.connect(
+ self.handle_metadata_downloaded
+ )
+ self._toggle_metadata_controls(enabled=False)
+ self._show_message("Retrieving metadata...", add_loading_widget=True)
+ api_client.get_dataset_detail(dataset)
+
+ def handle_metadata_download_error(self) -> None:
+ log("inside handle_metadata_download_error")
+
+ def handle_metadata_downloaded(self, downloaded_dataset: models.Dataset) -> None:
+ self._toggle_metadata_controls(enabled=True)
+ self.update_dataset(downloaded_dataset)
+ self._apply_geonode_metadata = True
+ self.apply()
+
+ def _apply_metadata(self) -> None:
+ dataset = self.get_dataset()
+ updated_metadata = populate_metadata(self.layer.metadata(), dataset)
+ self.layer.setMetadata(updated_metadata)
+ layer_properties_dialog = self._get_layer_properties_dialog()
+ layer_properties_dialog.syncToLayer()
+
+ def upload_metadata(self) -> None:
+ self.apply()
+ current_metadata = self.layer.metadata()
+ self.network_task = network.NetworkRequestTask(
+ [
+ network.RequestToPerform(
+ QtCore.QUrl(self.get_dataset().link),
+ method=network.HttpMethod.PATCH,
+ payload=json.dumps(
+ {
+ "title": current_metadata.title(),
+ "abstract": current_metadata.abstract(),
+ }
+ ),
+ content_type="application/json",
+ )
+ ],
+ self.api_client.auth_config,
+ network_task_timeout=self.api_client.network_requests_timeout,
+ description="Upload metadata",
+ )
+ self.network_task.task_done.connect(self.handle_metadata_uploaded)
+ self._toggle_metadata_controls(enabled=False)
+ self._show_message(message="Uploading metadata...", add_loading_widget=True)
+ qgis.core.QgsApplication.taskManager().addTask(self.network_task)
+
+ def handle_metadata_uploaded(self, task_result: bool) -> None:
+ self._toggle_metadata_controls(enabled=True)
+ if task_result:
+ parsed_reply = self.network_task.response_contents[0]
+ if parsed_reply is not None:
+ if parsed_reply.http_status_code == 200:
+ self._show_message("Metadata uploaded successfully!")
+ else:
+ error_message_parts = [
+ "Could not upload metadata",
+ parsed_reply.qt_error,
+ f"HTTP {parsed_reply.http_status_code}",
+ parsed_reply.http_status_reason,
+ ]
+ error_message = " - ".join(i for i in error_message_parts if i)
+ self._show_message(error_message, level=qgis.core.Qgis.Warning)
+ else:
+ self._show_message(
+ "Could not upload metadata", level=qgis.core.Qgis.Warning
+ )
+ else:
+ self._show_message(
+ "Could not upload metadata", level=qgis.core.Qgis.Warning
+ )
+
def open_detail_url(self) -> None:
dataset = self.get_dataset()
QtGui.QDesktopServices.openUrl(QtCore.QUrl(dataset.detail_url))
@@ -294,9 +398,8 @@ def _show_message(
utils.show_message(self.message_bar, message, level, add_loading_widget)
def _get_layer_properties_dialog(self):
- # FIXME: This is a very hacky way to get the layer properties dialog, and it
- # may not even work for layers that are not vector, but I've not been able
- # to find a more elegant way to retrieve it yet
+ # FIXME: This is a very hacky way to get the layer properties dialog
+ # but I've not been able to find a more elegant way to retrieve it yet
return self.parent().parent().parent().parent()
def _toggle_link_controls(self, enabled: bool) -> None:
@@ -331,3 +434,27 @@ def _toggle_style_controls(self, enabled: bool) -> None:
]
for widget in widgets:
widget.setEnabled(enabled)
+
+ def _toggle_metadata_controls(self, enabled: bool) -> None:
+ if enabled:
+ widgets = []
+ if self.connection_settings is not None:
+ can_load_metadata = (
+ models.ApiClientCapability.LOAD_LAYER_METADATA
+ in self.api_client.capabilities
+ )
+ if can_load_metadata:
+ widgets.append(self.download_metadata_pb)
+ can_modify_metadata = (
+ models.ApiClientCapability.MODIFY_LAYER_METADATA
+ in self.api_client.capabilities
+ )
+ if can_modify_metadata:
+ widgets.append(self.upload_metadata_pb)
+ else:
+ widgets = [
+ self.upload_metadata_pb,
+ self.download_metadata_pb,
+ ]
+ for widget in widgets:
+ widget.setEnabled(enabled)
diff --git a/src/qgis_geonode/gui/search_result_widget.py b/src/qgis_geonode/gui/search_result_widget.py
index 60c61397..55985eac 100644
--- a/src/qgis_geonode/gui/search_result_widget.py
+++ b/src/qgis_geonode/gui/search_result_widget.py
@@ -20,6 +20,7 @@
from .. import network
from ..apiclient.models import ApiClientCapability
from ..conf import settings_manager
+from ..metadata import populate_metadata
from ..resources import *
from ..utils import log, tr
@@ -396,62 +397,3 @@ def _load_wfs(self) -> qgis.core.QgsMapLayer:
params["authcfg"] = self.api_client.auth_config
uri = " ".join(f"{key}='{value}'" for key, value in params.items())
return qgis.core.QgsVectorLayer(uri, self.brief_dataset.title, "WFS")
-
-
-def populate_metadata(metadata: qgis.core.QgsLayerMetadata, dataset: models.Dataset):
- metadata.setIdentifier(str(dataset.uuid))
- metadata.setTitle(dataset.title)
- metadata.setAbstract(dataset.abstract)
- metadata.setLanguage(dataset.language)
- metadata.setKeywords({"layer": dataset.keywords})
- if dataset.category:
- metadata.setCategories([dataset.category])
- if dataset.license:
- metadata.setLicenses([dataset.license])
- if dataset.constraints:
- constraints = [qgis.core.QgsLayerMetadata.Constraint(dataset.constraints)]
- metadata.setConstraints(constraints)
- metadata.setCrs(dataset.srid)
- spatial_extent = qgis.core.QgsLayerMetadata.SpatialExtent()
- spatial_extent.extentCrs = dataset.srid
- if dataset.spatial_extent:
- spatial_extent.bounds = dataset.spatial_extent.toBox3d(0, 0)
- if dataset.temporal_extent:
- metadata.extent().setTemporalExtents(
- [
- qgis.core.QgsDateTimeRange(
- dataset.temporal_extent[0],
- dataset.temporal_extent[1],
- )
- ]
- )
-
- metadata.extent().setSpatialExtents([spatial_extent])
- if dataset.owner:
- owner_contact = qgis.core.QgsAbstractMetadataBase.Contact(dataset.owner)
- owner_contact.role = tr("owner")
- metadata.addContact(owner_contact)
- if dataset.metadata_author:
- metadata_author = qgis.core.QgsAbstractMetadataBase.Contact(
- dataset.metadata_author
- )
- metadata_author.role = tr("metadata_author")
- metadata.addContact(metadata_author)
- links = []
- if dataset.thumbnail_url:
- link = qgis.core.QgsAbstractMetadataBase.Link(
- tr("Thumbnail"), tr("Thumbail_link"), dataset.thumbnail_url
- )
- links.append(link)
- if dataset.link:
- link = qgis.core.QgsAbstractMetadataBase.Link(
- tr("API"), tr("API_URL"), dataset.link
- )
- links.append(link)
- if dataset.detail_url:
- link = qgis.core.QgsAbstractMetadataBase.Link(
- tr("Detail"), tr("Detail_URL"), dataset.detail_url
- )
- links.append(link)
- metadata.setLinks(links)
- return metadata
diff --git a/src/qgis_geonode/metadata.py b/src/qgis_geonode/metadata.py
new file mode 100644
index 00000000..b049ef0f
--- /dev/null
+++ b/src/qgis_geonode/metadata.py
@@ -0,0 +1,64 @@
+import qgis.core
+
+from .apiclient import models
+from .utils import tr
+
+
+def populate_metadata(
+ metadata: qgis.core.QgsLayerMetadata, dataset: models.Dataset
+) -> qgis.core.QgsLayerMetadata:
+ metadata.setIdentifier(str(dataset.uuid))
+ metadata.setTitle(dataset.title)
+ metadata.setAbstract(dataset.abstract)
+ metadata.setLanguage(dataset.language)
+ metadata.setKeywords({"layer": dataset.keywords})
+ if dataset.category:
+ metadata.setCategories([dataset.category])
+ if dataset.license:
+ metadata.setLicenses([dataset.license])
+ if dataset.constraints:
+ constraints = [qgis.core.QgsLayerMetadata.Constraint(dataset.constraints)]
+ metadata.setConstraints(constraints)
+ metadata.setCrs(dataset.srid)
+ spatial_extent = qgis.core.QgsLayerMetadata.SpatialExtent()
+ spatial_extent.extentCrs = dataset.srid
+ if dataset.spatial_extent:
+ spatial_extent.bounds = dataset.spatial_extent.toBox3d(0, 0)
+ if dataset.temporal_extent:
+ metadata.extent().setTemporalExtents(
+ [
+ qgis.core.QgsDateTimeRange(
+ dataset.temporal_extent[0],
+ dataset.temporal_extent[1],
+ )
+ ]
+ )
+ metadata.extent().setSpatialExtents([spatial_extent])
+ if dataset.owner:
+ owner_contact = qgis.core.QgsAbstractMetadataBase.Contact(dataset.owner)
+ owner_contact.role = tr("owner")
+ metadata.addContact(owner_contact)
+ if dataset.metadata_author:
+ metadata_author = qgis.core.QgsAbstractMetadataBase.Contact(
+ dataset.metadata_author
+ )
+ metadata_author.role = tr("metadata_author")
+ metadata.addContact(metadata_author)
+ links = []
+ if dataset.thumbnail_url:
+ link = qgis.core.QgsAbstractMetadataBase.Link(
+ tr("Thumbnail"), tr("Thumbail_link"), dataset.thumbnail_url
+ )
+ links.append(link)
+ if dataset.link:
+ link = qgis.core.QgsAbstractMetadataBase.Link(
+ tr("API"), tr("API_URL"), dataset.link
+ )
+ links.append(link)
+ if dataset.detail_url:
+ link = qgis.core.QgsAbstractMetadataBase.Link(
+ tr("Detail"), tr("Detail_URL"), dataset.detail_url
+ )
+ links.append(link)
+ metadata.setLinks(links)
+ return metadata
diff --git a/src/qgis_geonode/network.py b/src/qgis_geonode/network.py
index df911bd5..e153468e 100644
--- a/src/qgis_geonode/network.py
+++ b/src/qgis_geonode/network.py
@@ -18,9 +18,10 @@
class HttpMethod(enum.Enum):
- GET = "get"
- POST = "post"
- PUT = "put"
+ GET = "GET"
+ POST = "POST"
+ PUT = "PUT"
+ PATCH = "PATCH"
@dataclasses.dataclass()
@@ -219,6 +220,12 @@ def _dispatch_request(
elif method == HttpMethod.PUT:
data_ = QtCore.QByteArray(payload.encode())
reply = self.network_access_manager.put(request, data_)
+ elif method == HttpMethod.PATCH:
+ data_ = QtCore.QByteArray(payload.encode())
+ # QNetworkAccess manager does not have a patch() method
+ reply = self.network_access_manager.sendCustomRequest(
+ request, QtCore.QByteArray(HttpMethod.PATCH.value.encode()), data_
+ )
else:
raise NotImplementedError
return reply
diff --git a/src/qgis_geonode/ui/qgis_geonode_layer_dialog.ui b/src/qgis_geonode/ui/qgis_geonode_layer_dialog.ui
index b8deee3d..15fb82e5 100644
--- a/src/qgis_geonode/ui/qgis_geonode_layer_dialog.ui
+++ b/src/qgis_geonode/ui/qgis_geonode_layer_dialog.ui
@@ -7,13 +7,13 @@
0
0
664
- 333
+ 366
Geonode Layer
-
+
-
@@ -90,6 +90,36 @@
+ -
+
+
+ Metadata
+
+
+ false
+
+
+
-
+
+
-
+
+
+ Reload metadata from GeoNode
+
+
+
+ -
+
+
+ Save current metadata to GeoNode
+
+
+
+
+
+
+
+
-