Skip to content

Commit

Permalink
Implement update of metadata fields (GeoNode#192)
Browse files Browse the repository at this point in the history
* Add support for making HTTP PATCH requests

* Implement metadata uploads
  • Loading branch information
Ricardo Garcia Silva authored Dec 20, 2021
1 parent ddbce65 commit 4fe0d22
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 73 deletions.
3 changes: 1 addition & 2 deletions src/qgis_geonode/apiclient/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/qgis_geonode/apiclient/version_postv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
139 changes: 133 additions & 6 deletions src/qgis_geonode/gui/geonode_map_layer_config_widget.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import typing
import xml.etree.ElementTree as ET
from pathlib import Path
Expand Down Expand Up @@ -25,6 +26,7 @@
get_geonode_client,
models,
)
from ..metadata import populate_metadata
from ..utils import (
log,
)
Expand All @@ -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]:
Expand Down Expand Up @@ -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

Expand All @@ -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(
Expand All @@ -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)

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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]
Expand All @@ -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))
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
60 changes: 1 addition & 59 deletions src/qgis_geonode/gui/search_result_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
64 changes: 64 additions & 0 deletions src/qgis_geonode/metadata.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 4fe0d22

Please sign in to comment.