diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 759d0979..4371ba45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: ci: runs-on: ubuntu-20.04 container: - image: qgis/qgis:release-3_16 + image: qgis/qgis:release-3_18 steps: - name: Checkout code uses: actions/checkout@v2 @@ -46,7 +46,7 @@ jobs: if: github.ref == 'refs/heads/main' runs-on: ubuntu-20.04 container: - image: qgis/qgis:release-3_16 + image: qgis/qgis:release-3_18 steps: - name: Checkout code uses: actions/checkout@v2 diff --git a/.github/workflows/rebuild-docs.yml b/.github/workflows/rebuild-docs.yml index 9d6b1b7b..b8467959 100644 --- a/.github/workflows/rebuild-docs.yml +++ b/.github/workflows/rebuild-docs.yml @@ -6,7 +6,7 @@ jobs: rebuild-docs: runs-on: ubuntu-20.04 container: - image: qgis/qgis:release-3_16 + image: qgis/qgis:release-3_18 steps: - name: Checkout code uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f3699a5..ebea7909 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ jobs: create-release: runs-on: ubuntu-20.04 container: - image: qgis/qgis:release-3_16 + image: qgis/qgis:release-3_18 steps: - name: Checkout code uses: actions/checkout@v2 @@ -62,7 +62,7 @@ jobs: needs: create-release runs-on: ubuntu-20.04 container: - image: qgis/qgis:release-3_16 + image: qgis/qgis:release-3_18 steps: - name: Checkout code uses: actions/checkout@v2 diff --git a/src/qgis_geonode/apiclient/base.py b/src/qgis_geonode/apiclient/base.py index 7abd8906..00d5889f 100644 --- a/src/qgis_geonode/apiclient/base.py +++ b/src/qgis_geonode/apiclient/base.py @@ -115,21 +115,22 @@ def handle_dataset_style( raise NotImplementedError def get_dataset_detail( - self, brief_dataset: models.BriefDataset, get_style_too: bool = False + self, + dataset: typing.Union[models.BriefDataset, models.Dataset], + get_style_too: bool = False, ) -> None: requests_to_perform = [ - network.RequestToPerform(url=self.get_dataset_detail_url(brief_dataset.pk)) + network.RequestToPerform(url=self.get_dataset_detail_url(dataset.pk)) ] if get_style_too: is_vector = ( - brief_dataset.dataset_sub_type - == models.GeonodeResourceType.VECTOR_LAYER + dataset.dataset_sub_type == models.GeonodeResourceType.VECTOR_LAYER ) should_load_vector_style = ( models.ApiClientCapability.LOAD_VECTOR_LAYER_STYLE in self.capabilities ) if is_vector and should_load_vector_style: - sld_url = QtCore.QUrl(brief_dataset.default_style.sld_url) + sld_url = QtCore.QUrl(dataset.default_style.sld_url) requests_to_perform.append(network.RequestToPerform(url=sld_url)) self.network_fetcher_task = network.NetworkRequestTask( diff --git a/src/qgis_geonode/apiclient/geonode_v3.py b/src/qgis_geonode/apiclient/geonode_v3.py index c4262104..cabb9d1c 100644 --- a/src/qgis_geonode/apiclient/geonode_v3.py +++ b/src/qgis_geonode/apiclient/geonode_v3.py @@ -26,30 +26,15 @@ from .base import BaseGeonodeClient -class GeonodeApiClientVersion_3_4_0(BaseGeonodeClient): +@dataclasses.dataclass() +class ExportFormat: + driver_name: str + file_extension: str - capabilities = [ - models.ApiClientCapability.FILTER_BY_TITLE, - models.ApiClientCapability.FILTER_BY_RESOURCE_TYPES, - models.ApiClientCapability.FILTER_BY_ABSTRACT, - models.ApiClientCapability.FILTER_BY_KEYWORD, - models.ApiClientCapability.FILTER_BY_TOPIC_CATEGORY, - models.ApiClientCapability.FILTER_BY_PUBLICATION_DATE, - 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, - models.ApiClientCapability.MODIFY_RASTER_LAYER_STYLE, - models.ApiClientCapability.LOAD_VECTOR_DATASET_VIA_WMS, - models.ApiClientCapability.LOAD_VECTOR_DATASET_VIA_WFS, - models.ApiClientCapability.LOAD_RASTER_DATASET_VIA_WMS, - models.ApiClientCapability.LOAD_RASTER_DATASET_VIA_WCS, - models.ApiClientCapability.UPLOAD_VECTOR_LAYER, - models.ApiClientCapability.UPLOAD_RASTER_LAYER, - ] + +class GeonodeApiClientVersion_3_x(BaseGeonodeClient): + _DATASET_NAME = "dataset" + _DATASET_NAME_PLURAL = "datasets" @property def api_url(self): @@ -57,7 +42,7 @@ def api_url(self): @property def dataset_list_url(self): - return f"{self.api_url}/datasets/" + return f"{self.api_url}/{self._DATASET_NAME_PLURAL}/" def get_ordering_fields(self) -> typing.List[typing.Tuple[str, str]]: return [ @@ -140,22 +125,19 @@ def get_dataset_list_url( def get_dataset_detail_url(self, dataset_id: int) -> QtCore.QUrl: return QtCore.QUrl(f"{self.dataset_list_url}{dataset_id}/") - def get_dataset_upload_url(self) -> QtCore.QUrl: - return QtCore.QUrl(f"{self.api_url}/uploads/upload/") - def handle_dataset_list(self, task_result: bool) -> None: deserialized_content = self._retrieve_response( task_result, 0, self.search_error_received ) if deserialized_content is not None: brief_datasets = [] - for raw_brief_dataset in deserialized_content.get("datasets", []): + for raw_brief_ds in deserialized_content.get(self._DATASET_NAME_PLURAL, []): try: - parsed_properties = _get_common_model_properties(raw_brief_dataset) + parsed_properties = self._get_common_model_properties(raw_brief_ds) brief_dataset = models.BriefDataset(**parsed_properties) except ValueError: log( - f"Could not parse {raw_brief_dataset!r} into " f"a valid item", + f"Could not parse {raw_brief_ds!r} into a valid item", debug=False, ) else: @@ -168,12 +150,15 @@ def handle_dataset_list(self, task_result: bool) -> None: self.dataset_list_received.emit(brief_datasets, pagination_info) def handle_dataset_detail(self, task_result: bool) -> None: + log("inside the API client's handle_dataset_detail") deserialized_resource = self._retrieve_response( task_result, 0, self.dataset_detail_error_received ) if deserialized_resource is not None: try: - dataset = parse_dataset_detail(deserialized_resource["dataset"]) + dataset = self._parse_dataset_detail( + deserialized_resource[self._DATASET_NAME] + ) except KeyError as exc: log( f"Could not parse server response into a dataset: {str(exc)}", @@ -196,24 +181,6 @@ def handle_dataset_detail(self, task_result: bool) -> None: dataset.default_style.sld = sld_named_layer self.dataset_detail_received.emit(dataset) - def handle_dataset_detail_from_id(self, task_result: bool) -> None: - deserialized_resource = self._retrieve_response( - task_result, 0, self.dataset_detail_error_received - ) - if deserialized_resource is not None: - try: - dataset = parse_dataset_detail(deserialized_resource["dataset"]) - except KeyError as exc: - log( - f"Could not parse server response into a dataset: {str(exc)}", - debug=False, - ) - else: - if dataset.dataset_sub_type == models.GeonodeResourceType.VECTOR_LAYER: - self.get_dataset_style(dataset, emit_dataset_detail_received=True) - else: - self.dataset_detail_received.emit(dataset) - def handle_dataset_style( self, dataset: models.Dataset, @@ -235,39 +202,6 @@ def handle_dataset_style( if emit_dataset_detail_received: self.dataset_detail_received.emit(dataset) - def get_uploader_task( - self, layer: qgis.core.QgsMapLayer, allow_public_access: bool, timeout: int - ) -> qgis.core.QgsTask: - return LayerUploaderTask( - layer, - self.get_dataset_upload_url(), - allow_public_access, - self.auth_config, - network_task_timeout=timeout, - description="Upload layer to GeoNode", - ) - - def handle_layer_upload(self, result: bool): - if result: - response_contents = self.network_fetcher_task.response_contents[0] - if response_contents.http_status_code == 201: - deserialized = network.deserialize_json_response( - response_contents.response_body - ) - catalogue_url = deserialized["url"] - dataset_pk = catalogue_url.rsplit("/")[-1] - self.dataset_uploaded.emit(int(dataset_pk)) - else: - self.dataset_upload_error_received[str, int, str].emit( - response_contents.qt_error, - response_contents.http_status_code, - response_contents.http_status_reason, - ) - else: - self.dataset_upload_error_received[str].emit( - "Could not upload layer to GeoNode" - ) - def _retrieve_response( self, task_result: bool, @@ -303,11 +237,235 @@ def _retrieve_response( error_signal[str].emit("Could not complete network request") return result + def _get_common_model_properties(self, raw_dataset: typing.Dict) -> typing.Dict: + raise NotImplementedError -@dataclasses.dataclass() -class ExportFormat: - driver_name: str - file_extension: str + def _parse_dataset_detail(self, raw_dataset: typing.Dict) -> models.Dataset: + raise NotImplementedError + + +class GeonodeApiClientVersion_3_4_0(GeonodeApiClientVersion_3_x): + + capabilities = [ + models.ApiClientCapability.FILTER_BY_TITLE, + models.ApiClientCapability.FILTER_BY_RESOURCE_TYPES, + models.ApiClientCapability.FILTER_BY_ABSTRACT, + models.ApiClientCapability.FILTER_BY_KEYWORD, + models.ApiClientCapability.FILTER_BY_TOPIC_CATEGORY, + models.ApiClientCapability.FILTER_BY_PUBLICATION_DATE, + 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, + models.ApiClientCapability.MODIFY_RASTER_LAYER_STYLE, + models.ApiClientCapability.LOAD_VECTOR_DATASET_VIA_WMS, + models.ApiClientCapability.LOAD_VECTOR_DATASET_VIA_WFS, + models.ApiClientCapability.LOAD_RASTER_DATASET_VIA_WMS, + models.ApiClientCapability.LOAD_RASTER_DATASET_VIA_WCS, + models.ApiClientCapability.UPLOAD_VECTOR_LAYER, + models.ApiClientCapability.UPLOAD_RASTER_LAYER, + ] + + def get_dataset_upload_url(self) -> QtCore.QUrl: + return QtCore.QUrl(f"{self.api_url}/uploads/upload/") + + def handle_dataset_detail_from_id(self, task_result: bool) -> None: + deserialized_resource = self._retrieve_response( + task_result, 0, self.dataset_detail_error_received + ) + if deserialized_resource is not None: + try: + dataset = self._parse_dataset_detail(deserialized_resource["dataset"]) + except KeyError as exc: + log( + f"Could not parse server response into a dataset: {str(exc)}", + debug=False, + ) + else: + if dataset.dataset_sub_type == models.GeonodeResourceType.VECTOR_LAYER: + self.get_dataset_style(dataset, emit_dataset_detail_received=True) + else: + self.dataset_detail_received.emit(dataset) + + def get_uploader_task( + self, layer: qgis.core.QgsMapLayer, allow_public_access: bool, timeout: int + ) -> qgis.core.QgsTask: + return LayerUploaderTask( + layer, + self.get_dataset_upload_url(), + allow_public_access, + self.auth_config, + network_task_timeout=timeout, + description="Upload layer to GeoNode", + ) + + def handle_layer_upload(self, result: bool): + if result: + response_contents = self.network_fetcher_task.response_contents[0] + if response_contents.http_status_code == 201: + deserialized = network.deserialize_json_response( + response_contents.response_body + ) + catalogue_url = deserialized["url"] + dataset_pk = catalogue_url.rsplit("/")[-1] + self.dataset_uploaded.emit(int(dataset_pk)) + else: + self.dataset_upload_error_received[str, int, str].emit( + response_contents.qt_error, + response_contents.http_status_code, + response_contents.http_status_reason, + ) + else: + self.dataset_upload_error_received[str].emit( + "Could not upload layer to GeoNode" + ) + + def _get_common_model_properties(self, raw_dataset: typing.Dict) -> typing.Dict: + type_ = _get_resource_type(raw_dataset) + raw_links = raw_dataset.get("links", []) + if type_ == models.GeonodeResourceType.VECTOR_LAYER: + service_urls = _get_vector_service_urls(raw_links) + elif type_ == models.GeonodeResourceType.RASTER_LAYER: + service_urls = _get_raster_service_urls(raw_links) + else: + service_urls = {} + raw_style = raw_dataset.get("default_style") or {} + return { + "pk": int(raw_dataset["pk"]), + "uuid": uuid.UUID(raw_dataset["uuid"]), + "name": raw_dataset.get("alternate", raw_dataset.get("name", "")), + "title": raw_dataset.get("title", ""), + "abstract": raw_dataset.get( + "raw_abstract", raw_dataset.get("abstract", "") + ), + "thumbnail_url": raw_dataset["thumbnail_url"], + "link": raw_dataset["link"], + "detail_url": raw_dataset["detail_url"], + "dataset_sub_type": type_, + "service_urls": service_urls, + "spatial_extent": _get_spatial_extent(raw_dataset["bbox_polygon"]), + "srid": qgis.core.QgsCoordinateReferenceSystem(raw_dataset["srid"]), + "published_date": _get_published_date(raw_dataset), + "temporal_extent": _get_temporal_extent(raw_dataset), + "keywords": [k["name"] for k in raw_dataset.get("keywords", [])], + "category": (raw_dataset.get("category") or {}).get("identifier"), + "default_style": models.BriefGeonodeStyle( + name=raw_style.get("name", ""), sld_url=raw_style.get("sld_url") + ), + } + + def _parse_dataset_detail(self, raw_dataset: typing.Dict) -> models.Dataset: + properties = self._get_common_model_properties(raw_dataset) + properties.update( + language=raw_dataset.get("language"), + license=(raw_dataset.get("license") or {}).get("identifier", ""), + constraints=raw_dataset.get("raw_constraints_other", ""), + owner=raw_dataset.get("owner", {}).get("username", ""), + metadata_author=raw_dataset.get("metadata_author", {}).get("username", ""), + ) + return models.Dataset(**properties) + + +class GeonodeApiClientVersion_3_3_0(GeonodeApiClientVersion_3_x): + """API client for GeoNode version 3.3.x. + + GeoNode version 3.3.0 still used `layers` instead of `datasets`. It also did not + allow the upload of new datasets via API when using OAuth2 auth. + + """ + + _DATASET_NAME = "layer" + _DATASET_NAME_PLURAL = "layers" + + capabilities = [ + models.ApiClientCapability.FILTER_BY_TITLE, + models.ApiClientCapability.FILTER_BY_RESOURCE_TYPES, + models.ApiClientCapability.FILTER_BY_ABSTRACT, + models.ApiClientCapability.FILTER_BY_KEYWORD, + models.ApiClientCapability.FILTER_BY_TOPIC_CATEGORY, + models.ApiClientCapability.FILTER_BY_PUBLICATION_DATE, + 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, + models.ApiClientCapability.MODIFY_RASTER_LAYER_STYLE, + models.ApiClientCapability.LOAD_VECTOR_DATASET_VIA_WMS, + models.ApiClientCapability.LOAD_VECTOR_DATASET_VIA_WFS, + models.ApiClientCapability.LOAD_RASTER_DATASET_VIA_WMS, + models.ApiClientCapability.LOAD_RASTER_DATASET_VIA_WCS, + # upload of datasets via API using OAuth2 auth does not work, so the relevant + # capabilities are not included + ] + + def build_search_query( + self, search_filters: models.GeonodeApiSearchFilters + ) -> QtCore.QUrlQuery: + # GeoNode v3.3.0 layers did not have the `subtype` property, + # but rather a `dataStore` property + query = super().build_search_query(search_filters) + subtype_key = "filter{subtype.in}" + datastore_key = "filter{storeType.in}" + while query.hasQueryItem(subtype_key): + old_value = query.queryItemValue(subtype_key) + value = { + "vector": "dataStore", + "raster": "coverageStore", + }[old_value] + query.addQueryItem(datastore_key, value) + query.removeQueryItem(subtype_key) + return query + + def _get_common_model_properties(self, raw_dataset: typing.Dict) -> typing.Dict: + type_ = { + "coverageStore": models.GeonodeResourceType.RASTER_LAYER, + "dataStore": models.GeonodeResourceType.VECTOR_LAYER, + }.get(raw_dataset.get("storeType")) + service_urls = { + models.GeonodeService.OGC_WMS: raw_dataset["ows_url"], + } + if type_ == models.GeonodeResourceType.VECTOR_LAYER: + service_urls[models.GeonodeService.OGC_WFS] = raw_dataset["ows_url"] + elif type_ == models.GeonodeResourceType.RASTER_LAYER: + service_urls[models.GeonodeService.OGC_WCS] = raw_dataset["ows_url"] + raw_style = raw_dataset.get("default_style") or {} + return { + "pk": int(raw_dataset["pk"]), + "uuid": uuid.UUID(raw_dataset["uuid"]), + "name": raw_dataset.get("alternate", raw_dataset.get("name", "")), + "title": raw_dataset.get("title", ""), + "abstract": raw_dataset.get("raw_abstract", ""), + "thumbnail_url": raw_dataset["thumbnail_url"], + "link": raw_dataset["link"], + "detail_url": raw_dataset["detail_url"], + "dataset_sub_type": type_, + "service_urls": service_urls, + "spatial_extent": _get_spatial_extent(raw_dataset["bbox_polygon"]), + "srid": qgis.core.QgsCoordinateReferenceSystem(raw_dataset["srid"]), + "published_date": _get_published_date(raw_dataset), + "temporal_extent": _get_temporal_extent(raw_dataset), + "keywords": [k["name"] for k in raw_dataset.get("keywords", [])], + "category": (raw_dataset.get("category") or {}).get("identifier"), + "default_style": models.BriefGeonodeStyle( + name=raw_style.get("name", ""), sld_url=raw_style.get("sld_url") + ), + } + + def _parse_dataset_detail(self, raw_dataset: typing.Dict) -> models.Dataset: + properties = self._get_common_model_properties(raw_dataset) + properties.update( + language=raw_dataset.get("language"), + license=(raw_dataset.get("license") or {}).get("identifier", ""), + constraints=raw_dataset.get("raw_constraints_other", ""), + owner=raw_dataset.get("owner", {}).get("username", ""), + metadata_author=raw_dataset.get("metadata_author", {}).get("username", ""), + ) + return models.Dataset(**properties) class LayerUploaderTask(network.NetworkRequestTask): @@ -535,122 +693,6 @@ def _export_layer_style(self) -> typing.Tuple[typing.Optional[Path], str]: return result -class GeonodeApiClientVersion_3_3_0(GeonodeApiClientVersion_3_4_0): - """API client for GeoNode version 3.3.x. - - GeoNode version 3.3.0 still used `layers` instead of `datasets`. It also did not - allow the upload of new datasets via API when using OAuth2 auth. - - """ - - capabilities = [ - models.ApiClientCapability.FILTER_BY_TITLE, - models.ApiClientCapability.FILTER_BY_RESOURCE_TYPES, - models.ApiClientCapability.FILTER_BY_ABSTRACT, - models.ApiClientCapability.FILTER_BY_KEYWORD, - models.ApiClientCapability.FILTER_BY_TOPIC_CATEGORY, - models.ApiClientCapability.FILTER_BY_PUBLICATION_DATE, - 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, - models.ApiClientCapability.MODIFY_RASTER_LAYER_STYLE, - models.ApiClientCapability.LOAD_VECTOR_DATASET_VIA_WMS, - models.ApiClientCapability.LOAD_VECTOR_DATASET_VIA_WFS, - models.ApiClientCapability.LOAD_RASTER_DATASET_VIA_WMS, - models.ApiClientCapability.LOAD_RASTER_DATASET_VIA_WCS, - # upload of datasets via API using OAuth2 auth does not work, so the relevant - # capabilities are not included - ] - - @property - def dataset_list_url(self): - return f"{self.api_url}/layers/" - - def build_search_query( - self, search_filters: models.GeonodeApiSearchFilters - ) -> QtCore.QUrlQuery: - # GeoNode v3.3.0 layers did not have the `subtype` property, - # but rather a `dataStore` property - query = super().build_search_query(search_filters) - subtype_key = "filter{subtype.in}" - datastore_key = "filter{storeType.in}" - while query.hasQueryItem(subtype_key): - old_value = query.queryItemValue(subtype_key) - value = { - "vector": "dataStore", - "raster": "coverageStore", - }[old_value] - query.addQueryItem(datastore_key, value) - query.removeQueryItem(subtype_key) - return query - - def handle_dataset_list(self, task_result: bool) -> None: - deserialized_content = self._retrieve_response( - task_result, 0, self.search_error_received - ) - if deserialized_content is not None: - brief_datasets = [] - for raw_brief_dataset in deserialized_content.get("layers", []): - try: - parsed_properties = _get_common_model_properties_v_3_3_x( - raw_brief_dataset - ) - brief_dataset = models.BriefDataset(**parsed_properties) - except ValueError: - log( - f"Could not parse {raw_brief_dataset!r} into a valid item", - debug=False, - ) - else: - brief_datasets.append(brief_dataset) - pagination_info = models.GeonodePaginationInfo( - total_records=deserialized_content.get("total") or 0, - current_page=deserialized_content.get("page") or 1, - page_size=deserialized_content.get("page_size") or 0, - ) - self.dataset_list_received.emit(brief_datasets, pagination_info) - - -def _get_common_model_properties_v_3_3_x(raw_dataset: typing.Dict) -> typing.Dict: - type_ = { - "coverageStore": models.GeonodeResourceType.RASTER_LAYER, - "dataStore": models.GeonodeResourceType.VECTOR_LAYER, - }.get(raw_dataset.get("storeType")) - service_urls = { - models.GeonodeService.OGC_WMS: raw_dataset["ows_url"], - } - if type_ == models.GeonodeResourceType.VECTOR_LAYER: - service_urls[models.GeonodeService.OGC_WFS] = raw_dataset["ows_url"] - elif type_ == models.GeonodeResourceType.RASTER_LAYER: - service_urls[models.GeonodeService.OGC_WCS] = raw_dataset["ows_url"] - raw_style = raw_dataset.get("default_style") or {} - return { - "pk": int(raw_dataset["pk"]), - "uuid": uuid.UUID(raw_dataset["uuid"]), - "name": raw_dataset.get("alternate", raw_dataset.get("name", "")), - "title": raw_dataset.get("title", ""), - "abstract": raw_dataset.get("raw_abstract", ""), - "thumbnail_url": raw_dataset["thumbnail_url"], - "link": raw_dataset["link"], - "detail_url": raw_dataset["detail_url"], - "dataset_sub_type": type_, - "service_urls": service_urls, - "spatial_extent": _get_spatial_extent(raw_dataset["bbox_polygon"]), - "srid": qgis.core.QgsCoordinateReferenceSystem(raw_dataset["srid"]), - "published_date": _get_published_date(raw_dataset), - "temporal_extent": _get_temporal_extent(raw_dataset), - "keywords": [k["name"] for k in raw_dataset.get("keywords", [])], - "category": (raw_dataset.get("category") or {}).get("identifier"), - "default_style": models.BriefGeonodeStyle( - name=raw_style.get("name", ""), sld_url=raw_style.get("sld_url") - ), - } - - def build_multipart( layer_metadata: qgis.core.QgsLayerMetadata, permissions: typing.Dict, @@ -719,51 +761,6 @@ def build_multipart( return multipart -def parse_dataset_detail(raw_dataset: typing.Dict) -> models.Dataset: - properties = _get_common_model_properties(raw_dataset) - properties.update( - language=raw_dataset.get("language"), - license=(raw_dataset.get("license") or {}).get("identifier", ""), - constraints=raw_dataset.get("raw_constraints_other", ""), - owner=raw_dataset.get("owner", {}).get("username", ""), - metadata_author=raw_dataset.get("metadata_author", {}).get("username", ""), - ) - return models.Dataset(**properties) - - -def _get_common_model_properties(raw_dataset: typing.Dict) -> typing.Dict: - type_ = _get_resource_type(raw_dataset) - raw_links = raw_dataset.get("links", []) - if type_ == models.GeonodeResourceType.VECTOR_LAYER: - service_urls = _get_vector_service_urls(raw_links) - elif type_ == models.GeonodeResourceType.RASTER_LAYER: - service_urls = _get_raster_service_urls(raw_links) - else: - service_urls = {} - raw_style = raw_dataset.get("default_style") or {} - return { - "pk": int(raw_dataset["pk"]), - "uuid": uuid.UUID(raw_dataset["uuid"]), - "name": raw_dataset.get("alternate", raw_dataset.get("name", "")), - "title": raw_dataset.get("title", ""), - "abstract": raw_dataset.get("raw_abstract", raw_dataset.get("abstract", "")), - "thumbnail_url": raw_dataset["thumbnail_url"], - "link": raw_dataset["link"], - "detail_url": raw_dataset["detail_url"], - "dataset_sub_type": type_, - "service_urls": service_urls, - "spatial_extent": _get_spatial_extent(raw_dataset["bbox_polygon"]), - "srid": qgis.core.QgsCoordinateReferenceSystem(raw_dataset["srid"]), - "published_date": _get_published_date(raw_dataset), - "temporal_extent": _get_temporal_extent(raw_dataset), - "keywords": [k["name"] for k in raw_dataset.get("keywords", [])], - "category": (raw_dataset.get("category") or {}).get("identifier"), - "default_style": models.BriefGeonodeStyle( - name=raw_style.get("name", ""), sld_url=raw_style.get("sld_url") - ), - } - - def _get_link(raw_links: typing.List, link_type: str) -> typing.Optional[str]: for link_info in raw_links: if link_info.get("link_type") == link_type: @@ -777,8 +774,8 @@ def _get_link(raw_links: typing.List, link_type: str) -> typing.Optional[str]: def _get_temporal_extent( payload: typing.Dict, ) -> typing.Optional[typing.List[typing.Optional[dt.datetime]]]: - start = payload["temporal_extent_start"] - end = payload["temporal_extent_end"] + start = payload.get("temporal_extent_start") or None + end = payload.get("temporal_extent_end") or None if start is not None and end is not None: result = [_parse_datetime(start), _parse_datetime(end)] elif start is not None and end is None: @@ -800,14 +797,18 @@ def _get_resource_type( return result -def _get_vector_service_urls(raw_links: typing.Dict): +def _get_vector_service_urls( + raw_links: typing.Dict, +) -> typing.Dict[models.GeonodeService, str]: return { models.GeonodeService.OGC_WMS: _get_link(raw_links, "OGC:WMS"), models.GeonodeService.OGC_WFS: _get_link(raw_links, "OGC:WFS"), } -def _get_raster_service_urls(raw_links: typing.Dict): +def _get_raster_service_urls( + raw_links: typing.Dict, +) -> typing.Dict[models.GeonodeService, str]: return { models.GeonodeService.OGC_WMS: _get_link(raw_links, "OGC:WMS"), models.GeonodeService.OGC_WCS: _get_link(raw_links, "OGC:WCS"), diff --git a/src/qgis_geonode/apiclient/models.py b/src/qgis_geonode/apiclient/models.py index d8714265..00f1e0f6 100644 --- a/src/qgis_geonode/apiclient/models.py +++ b/src/qgis_geonode/apiclient/models.py @@ -249,7 +249,7 @@ class GeonodeApiSearchFilters: page: typing.Optional[int] = 1 title: typing.Optional[str] = None abstract: typing.Optional[str] = None - keyword: typing.Optional[typing.List[str]] = None + keyword: typing.Optional[str] = None topic_category: typing.Optional[IsoTopicCategory] = None layer_types: typing.Optional[typing.List[GeonodeResourceType]] = dataclasses.field( default_factory=list 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 102f7159..61f0af2e 100644 --- a/src/qgis_geonode/gui/geonode_map_layer_config_widget.py +++ b/src/qgis_geonode/gui/geonode_map_layer_config_widget.py @@ -1,7 +1,6 @@ import json import typing import xml.etree.ElementTree as ET -from functools import partial from pathlib import Path from uuid import UUID @@ -56,6 +55,7 @@ class GeonodeMapLayerConfigWidget(qgis.gui.QgsMapLayerConfigWidget, WidgetUi): _apply_geonode_style: bool _apply_geonode_metadata: bool _layer_upload_api_client: typing.Optional[base.BaseGeonodeClient] + _api_client: typing.Optional[base.BaseGeonodeClient] @property def connection_settings(self) -> typing.Optional[conf.ConnectionSettings]: @@ -70,15 +70,6 @@ def connection_settings(self) -> typing.Optional[conf.ConnectionSettings]: result = None return result - @property - def api_client(self) -> typing.Optional[base.BaseGeonodeClient]: - connection_settings = self.connection_settings - if connection_settings is not None: - result = get_geonode_client(connection_settings) - else: - result = None - return result - def __init__(self, layer, canvas, parent): super().__init__(layer, canvas, parent) self.setupUi(self) @@ -100,8 +91,9 @@ def __init__(self, layer, canvas, parent): self.network_task = None self._apply_geonode_style = False self._apply_geonode_metadata = False - self._layer_upload_api_client = None self.layer = layer + self._layer_upload_api_client = None + self._api_client = get_geonode_client(self.connection_settings) self.upload_layer_pb.clicked.connect(self.upload_layer_to_geonode) suitable_connections = self._get_suitable_upload_connections() if len(suitable_connections) > 0: @@ -140,9 +132,7 @@ def get_dataset(self) -> typing.Optional[models.Dataset]: models.DATASET_CUSTOM_PROPERTY_KEY ) if serialized_dataset is not None: - result = models.Dataset.from_json( - self.layer.customProperty(models.DATASET_CUSTOM_PROPERTY_KEY) - ) + result = models.Dataset.from_json(serialized_dataset) else: result = None return result @@ -155,7 +145,7 @@ def download_style(self): dataset = self.get_dataset() self.network_task = network.NetworkRequestTask( [network.RequestToPerform(QtCore.QUrl(dataset.default_style.sld_url))], - self.api_client.network_requests_timeout, + self._api_client.network_requests_timeout, self.connection_settings.auth_config, description="Get dataset style", ) @@ -204,7 +194,7 @@ def upload_style(self): content_type=content_type, ) ], - self.api_client.network_requests_timeout, + self._api_client.network_requests_timeout, self.connection_settings.auth_config, description="Upload dataset style to GeoNode", ) @@ -295,28 +285,18 @@ def handle_style_uploaded(self, task_result: bool) -> None: ) def download_metadata(self) -> None: - """Initiate download of metadata from the remote GeoNode. + """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._api_client.dataset_detail_received.connect( + self.handle_metadata_downloaded + ) + self._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, get_style_too=False) + dataset = self.get_dataset() + self._api_client.get_dataset_detail(dataset, get_style_too=False) def handle_metadata_download_error(self) -> None: log("inside handle_metadata_download_error") @@ -352,8 +332,8 @@ def upload_metadata(self) -> None: content_type="application/json", ) ], - self.api_client.network_requests_timeout, - self.api_client.auth_config, + self._api_client.network_requests_timeout, + self._api_client.auth_config, description="Upload metadata", ) self.network_task.task_done.connect(self.handle_metadata_uploaded) @@ -478,11 +458,11 @@ def _toggle_style_controls(self, enabled: bool) -> None: widgets = [] if enabled and self.connection_settings is not None: can_load_style = models.loading_style_supported( - self.layer.type(), self.api_client.capabilities + self.layer.type(), self._api_client.capabilities ) log(f"can_load_style: {can_load_style}") can_modify_style = models.modifying_style_supported( - self.layer.type(), self.api_client.capabilities + self.layer.type(), self._api_client.capabilities ) dataset = self.get_dataset() is_service = self.layer.dataProvider().name().lower() in ("wfs", "wcs") @@ -503,13 +483,13 @@ def _toggle_metadata_controls(self, enabled: bool) -> None: if enabled and self.connection_settings is not None: can_load_metadata = ( models.ApiClientCapability.LOAD_LAYER_METADATA - in self.api_client.capabilities + 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 + in self._api_client.capabilities ) if can_modify_metadata: widgets.append(self.upload_metadata_pb) diff --git a/test/test_apiclient_geonode_v3.py b/test/test_apiclient_geonode_v3.py new file mode 100644 index 00000000..37ad1cf2 --- /dev/null +++ b/test/test_apiclient_geonode_v3.py @@ -0,0 +1,425 @@ +import datetime as dt +import typing +import uuid + +import pytest + +import qgis.core +from qgis.PyQt import QtCore + +from qgis_geonode.apiclient import ( + geonode_v3, + models, +) + + +@pytest.mark.parametrize( + "raw_links, link_type, expected", + [ + pytest.param([{"link_type": "foo", "url": "bar"}], "foo", "bar"), + pytest.param([{}], "foo", None), + ], +) +def test_get_link(raw_links, link_type, expected): + result = geonode_v3._get_link(raw_links, link_type) + assert result == expected + + +@pytest.mark.parametrize( + "payload, expected", + [ + pytest.param( + { + "temporal_extent_start": "2021-01-30T10:23:22Z", + "temporal_extent_end": "2021-01-30T10:23:22Z", + }, + [ + dt.datetime(2021, 1, 30, 10, 23, 22), + dt.datetime(2021, 1, 30, 10, 23, 22), + ], + ), + pytest.param( + { + "temporal_extent_start": "2021-01-30T10:23:22Z", + }, + [dt.datetime(2021, 1, 30, 10, 23, 22), None], + ), + pytest.param( + { + "temporal_extent_end": "2021-01-30T10:23:22Z", + }, + [ + None, + dt.datetime(2021, 1, 30, 10, 23, 22), + ], + ), + pytest.param( + {}, + None, + ), + ], +) +def test_get_temporal_extent(payload, expected): + result = geonode_v3._get_temporal_extent(payload) + assert result == expected + + +@pytest.mark.parametrize( + "raw_dataset, expected", + [ + pytest.param({"subtype": "raster"}, models.GeonodeResourceType.RASTER_LAYER), + pytest.param({"subtype": "vector"}, models.GeonodeResourceType.VECTOR_LAYER), + pytest.param({}, None), + ], +) +def test_get_resource_type(raw_dataset, expected): + result = geonode_v3._get_resource_type(raw_dataset) + assert result == expected + + +# @pytest.mark.parametrize("geojson_geom, expected", [ +# pytest.param( +# {}, +# qgis.core.QgsRectangle(0, 0, 10, 10) +# ), +# ]) +# def test_get_spatial_extent(geojson_geom, expected): +# result = geonode_v3._get_spatial_extent(geojson_geom) +# assert result == expected + + +@pytest.mark.parametrize( + "raw_value, expected", + [ + pytest.param("2021-10-02T09:22:01Z", dt.datetime(2021, 10, 2, 9, 22, 1)), + pytest.param( + "2021-10-02T09:22:01.123456Z", dt.datetime(2021, 10, 2, 9, 22, 1, 123456) + ), + ], +) +def test_parse_datetime(raw_value, expected): + result = geonode_v3._parse_datetime(raw_value) + assert result == expected + + +@pytest.mark.parametrize( + "base_url, expected", + [ + pytest.param("http://fake.com", "http://fake.com/api/v2/layers/"), + ], +) +def test_apiclient_v_3_3_0_dataset_list_url(base_url, expected): + client = geonode_v3.GeonodeApiClientVersion_3_3_0( + base_url, 10, network_requests_timeout=0 + ) + assert client.dataset_list_url == expected + + +@pytest.mark.parametrize( + "client_class, base_url, expected", + [ + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + "http://fake.com", + "http://fake.com/api/v2/layers/", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + "http://fake.com", + "http://fake.com/api/v2/datasets/", + ), + ], +) +def test_apiclient_dataset_list_url(client_class: typing.Type, base_url, expected): + client = client_class(base_url, 10, network_requests_timeout=0) + assert client.dataset_list_url == expected + + +@pytest.mark.parametrize( + "client_class, base_url, dataset_id, expected", + [ + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + "http://fake.com", + 1, + "http://fake.com/api/v2/layers/1/", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + "http://fake.com", + 1, + "http://fake.com/api/v2/datasets/1/", + ), + ], +) +def test_apiclient_get_dataset_detail_url( + client_class: typing.Type, base_url, dataset_id, expected +): + client = client_class(base_url, 10, network_requests_timeout=0) + result = client.get_dataset_detail_url(dataset_id) + assert result.toString() == expected + + +@pytest.mark.parametrize( + "client_class, search_filters, expected", + [ + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters(), + "page=1&page_size=10", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters(title="fake-title"), + "page=1&page_size=10&filter%7Btitle.icontains%7D=fake-title", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters(abstract="fake-abstract"), + "page=1&page_size=10&filter%7Babstract.icontains%7D=fake-abstract", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters(keyword="fake-keyword"), + "page=1&page_size=10&filter%7Bkeywords.name.icontains%7D=fake-keyword", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters( + topic_category=models.IsoTopicCategory.biota + ), + "page=1&page_size=10&filter%7Bcategory.identifier%7D=biota", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters( + temporal_extent_start=QtCore.QDateTime(2021, 7, 31, 10, 22) + ), + "page=1&page_size=10&filter%7Btemporal_extent_start.gte%7D=2021-07-31T10:22:00", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters( + temporal_extent_end=QtCore.QDateTime(2021, 7, 31, 10, 22) + ), + "page=1&page_size=10&filter%7Btemporal_extent_end.lte%7D=2021-07-31T10:22:00", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters( + publication_date_start=QtCore.QDateTime(2021, 7, 31, 10, 22) + ), + "page=1&page_size=10&filter%7Bdate.gte%7D=2021-07-31T10:22:00", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters( + publication_date_end=QtCore.QDateTime(2021, 7, 31, 10, 22) + ), + "page=1&page_size=10&filter%7Bdate.lte%7D=2021-07-31T10:22:00", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters( + layer_types=[models.GeonodeResourceType.VECTOR_LAYER] + ), + "page=1&page_size=10&filter%7Bsubtype.in%7D=vector", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters( + layer_types=[models.GeonodeResourceType.RASTER_LAYER] + ), + "page=1&page_size=10&filter%7Bsubtype.in%7D=raster", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters( + layer_types=[ + models.GeonodeResourceType.VECTOR_LAYER, + models.GeonodeResourceType.RASTER_LAYER, + ] + ), + "page=1&page_size=10&filter%7Bsubtype.in%7D=vector&filter%7Bsubtype.in%7D=raster", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters(ordering_field="name"), + "page=1&page_size=10&sort[]=name", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_4_0, + models.GeonodeApiSearchFilters( + ordering_field="name", reverse_ordering=True + ), + "page=1&page_size=10&sort[]=-name", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters(), + "page=1&page_size=10", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters(title="fake-title"), + "page=1&page_size=10&filter%7Btitle.icontains%7D=fake-title", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters(abstract="fake-abstract"), + "page=1&page_size=10&filter%7Babstract.icontains%7D=fake-abstract", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters(keyword="fake-keyword"), + "page=1&page_size=10&filter%7Bkeywords.name.icontains%7D=fake-keyword", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters( + topic_category=models.IsoTopicCategory.biota + ), + "page=1&page_size=10&filter%7Bcategory.identifier%7D=biota", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters( + temporal_extent_start=QtCore.QDateTime(2021, 7, 31, 10, 22) + ), + "page=1&page_size=10&filter%7Btemporal_extent_start.gte%7D=2021-07-31T10:22:00", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters( + temporal_extent_end=QtCore.QDateTime(2021, 7, 31, 10, 22) + ), + "page=1&page_size=10&filter%7Btemporal_extent_end.lte%7D=2021-07-31T10:22:00", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters( + publication_date_start=QtCore.QDateTime(2021, 7, 31, 10, 22) + ), + "page=1&page_size=10&filter%7Bdate.gte%7D=2021-07-31T10:22:00", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters( + publication_date_end=QtCore.QDateTime(2021, 7, 31, 10, 22) + ), + "page=1&page_size=10&filter%7Bdate.lte%7D=2021-07-31T10:22:00", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters( + layer_types=[models.GeonodeResourceType.VECTOR_LAYER] + ), + "page=1&page_size=10&filter%7BstoreType.in%7D=dataStore", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters( + layer_types=[models.GeonodeResourceType.RASTER_LAYER] + ), + "page=1&page_size=10&filter%7BstoreType.in%7D=coverageStore", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters( + layer_types=[ + models.GeonodeResourceType.VECTOR_LAYER, + models.GeonodeResourceType.RASTER_LAYER, + ] + ), + "page=1&page_size=10&filter%7BstoreType.in%7D=dataStore&filter%7BstoreType.in%7D=coverageStore", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters(ordering_field="name"), + "page=1&page_size=10&sort[]=name", + ), + pytest.param( + geonode_v3.GeonodeApiClientVersion_3_3_0, + models.GeonodeApiSearchFilters( + ordering_field="name", reverse_ordering=True + ), + "page=1&page_size=10&sort[]=-name", + ), + ], +) +def test_apiclient_build_search_filters( + client_class: typing.Type, search_filters, expected +): + client = client_class("phony-base-url", 10, network_requests_timeout=0) + result = client.build_search_query(search_filters) + assert result.toString() == expected + + +def test_get_common_model_properties_client_v_3_4_0(): + dataset_uuid = "c22e838f-9503-484e-8769-b5b09a2b6104" + raw_dataset = { + "pk": 1, + "uuid": dataset_uuid, + "alternate": "fake name", + "title": "fake title", + "raw_abstract": "fake abstract", + "thumbnail_url": "fake thumbnail url", + "link": "fake link", + "detail_url": "fake detail url", + "subtype": "vector", + "links": [ + {"link_type": "OGC:WMS", "url": "fake-wms-url"}, + {"link_type": "OGC:WFS", "url": "fake-wfs-url"}, + ], + "bbox_polygon": { + "type": "Polygon", + "coordinates": [ + [ + [-180.0, -90.0], + [-180.0, 90.0], + [180.0, 90.0], + [180.0, -90.0], + [-180.0, -90.0], + ] + ], + }, + "srid": "EPSG:4326", + "date_type": "publication", + "date": "2021-02-12T23:00:00Z", + "temporal_extent_start": "2021-03-02T10:45:22Z", + "temporal_extent_end": "2021-03-02T19:45:22Z", + "keywords": [{"name": "fake-keyword1"}, {"name": "fake-keyword2"}], + "category": {"identifier": "fake-category"}, + "default_style": {"name": "fake-style-name", "sld_url": "fake-sld-url"}, + } + expected = { + "pk": 1, + "uuid": uuid.UUID(dataset_uuid), + "name": "fake name", + "title": "fake title", + "abstract": "fake abstract", + "thumbnail_url": "fake thumbnail url", + "link": "fake link", + "detail_url": "fake detail url", + "dataset_sub_type": models.GeonodeResourceType.VECTOR_LAYER, + "service_urls": { + models.GeonodeService.OGC_WMS: "fake-wms-url", + models.GeonodeService.OGC_WFS: "fake-wfs-url", + }, + "spatial_extent": qgis.core.QgsRectangle(-180.0, -90.0, 180.0, 90.0), + "srid": qgis.core.QgsCoordinateReferenceSystem("EPSG:4326"), + "published_date": dt.datetime(2021, 2, 12, 23), + "temporal_extent": [ + dt.datetime(2021, 3, 2, 10, 45, 22), + dt.datetime(2021, 3, 2, 19, 45, 22), + ], + "keywords": ["fake-keyword1", "fake-keyword2"], + "category": "fake-category", + "default_style": models.BriefGeonodeStyle( + name="fake-style-name", sld_url="fake-sld-url" + ), + } + client = geonode_v3.GeonodeApiClientVersion_3_4_0("fake-base-url", 10, 0) + result = client._get_common_model_properties(raw_dataset) + for k, v in expected.items(): + assert result[k] == v