diff --git a/src/qgis_geonode/apiclient/__init__.py b/src/qgis_geonode/apiclient/__init__.py index d3c5324..3a64071 100644 --- a/src/qgis_geonode/apiclient/__init__.py +++ b/src/qgis_geonode/apiclient/__init__.py @@ -1,45 +1,43 @@ +import requests import importlib import typing -from ..network import UNSUPPORTED_REMOTE -from packaging import version as packaging_version -from packaging.specifiers import SpecifierSet +SUPPORTED_API_CLIENT = "/api/v2/" +_api_v2_cache: dict[str, bool] = {} -SUPPORTED_VERSIONS = SpecifierSet(">=4.0.0, <5.0.0dev0") +def is_api_client_supported(base_url: str) -> bool: + """ + Returns True if SUPPORTED_API_CLIENT endpoint responds with HTTP 200 + and contains valid JSON. + """ -def validate_version( - version: packaging_version.Version, supported_versions=SUPPORTED_VERSIONS -) -> bool: + if base_url in _api_v2_cache: + return _api_v2_cache[base_url] - version = version.base_version + url = f"{base_url.rstrip('/')}/api/v2/" + try: + resp = requests.get(url, timeout=5) + supported = resp.status_code == 200 and isinstance(resp.json(), dict) + except Exception: + supported = False - if version in supported_versions: - return True - else: - return False + _api_v2_cache[base_url] = supported + return supported def get_geonode_client( connection_settings: "ConnectionSettings", ) -> typing.Optional["BaseGeonodeClient"]: - version = connection_settings.geonode_version - result = None - if version is not None and version != UNSUPPORTED_REMOTE: - class_path = select_supported_client(connection_settings.geonode_version) - if class_path != None: - module_path, class_name = class_path.rpartition(".")[::2] - imported_module = importlib.import_module(module_path) - class_type = getattr(imported_module, class_name) - result = class_type.from_connection_settings(connection_settings) - return result + if not is_api_client_supported(connection_settings.base_url): + return None + module_path, class_name = select_supported_client().rpartition(".")[::2] + imported_module = importlib.import_module(module_path) + class_type = getattr(imported_module, class_name) + return class_type.from_connection_settings(connection_settings) -def select_supported_client(geonode_version: packaging_version.Version) -> str: - result = None - if validate_version(geonode_version): - result = "qgis_geonode.apiclient.geonode_api_v2.GeoNodeApiClient" - - return result +def select_supported_client() -> str: + return "qgis_geonode.apiclient.geonode_api_v2.GeoNodeApiClient" diff --git a/src/qgis_geonode/conf.py b/src/qgis_geonode/conf.py index 8e51188..42a9925 100644 --- a/src/qgis_geonode/conf.py +++ b/src/qgis_geonode/conf.py @@ -56,7 +56,6 @@ class ConnectionSettings: network_requests_timeout: int = dataclasses.field( 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 @@ -66,18 +65,12 @@ def from_qgs_settings(cls, connection_identifier: str, settings: QgsSettings): reported_auth_cfg = settings.value("auth_config").strip() except AttributeError: reported_auth_cfg = None - raw_geonode_version = settings.value("geonode_version") or UNSUPPORTED_REMOTE - if raw_geonode_version != UNSUPPORTED_REMOTE: - geonode_version = packaging_version.parse(raw_geonode_version) - else: - geonode_version = UNSUPPORTED_REMOTE return cls( id=uuid.UUID(connection_identifier), name=settings.value("name"), base_url=settings.value("base_url"), 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")), ) @@ -89,9 +82,6 @@ def to_json(self): "base_url": self.base_url, "page_size": self.page_size, "auth_config": self.auth_config, - "geonode_version": str(self.geonode_version) - if self.geonode_version is not None - else None, "wfs_version": self.wfs_version.value, } ) @@ -187,14 +177,6 @@ def save_connection_settings(self, connection_settings: ConnectionSettings): 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", - ( - str(connection_settings.geonode_version) - if connection_settings.geonode_version is not None - else "" - ), - ) def delete_connection(self, connection_id: uuid.UUID): if self.is_current_connection(connection_id): diff --git a/src/qgis_geonode/gui/connection_dialog.py b/src/qgis_geonode/gui/connection_dialog.py index a132957..ef5ad16 100644 --- a/src/qgis_geonode/gui/connection_dialog.py +++ b/src/qgis_geonode/gui/connection_dialog.py @@ -16,6 +16,7 @@ from ..tasks import network_task from .. import apiclient, network, utils from ..apiclient.base import BaseGeonodeClient +from ..apiclient import is_api_client_supported from ..conf import ConnectionSettings, WfsVersion, settings_manager, plugin_metadata from ..utils import tr from packaging import version as packaging_version @@ -43,9 +44,6 @@ class ConnectionDialog(QtWidgets.QDialog, DialogUi): detected_capabilities_lw: QtWidgets.QListWidget connection_id: uuid.UUID - remote_geonode_version: typing.Optional[ - typing.Union[packaging_version.Version, str] - ] discovery_task: typing.Optional[network_task.NetworkRequestTask] geonode_client: BaseGeonodeClient = None @@ -69,7 +67,6 @@ def __init__(self, connection_settings: typing.Optional[ConnectionSettings] = No 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) @@ -78,15 +75,12 @@ def __init__(self, connection_settings: typing.Optional[ConnectionSettings] = No connection_settings.wfs_version ) self.wfs_version_cb.setCurrentIndex(wfs_version_index) - if self.remote_geonode_version == network.UNSUPPORTED_REMOTE: - utils.show_message( - self.bar, - tr("Invalid configuration. Correct GeoNode URL and/or test again."), - level=qgis.core.Qgis.Critical, - ) + + self.detected_version_gb.setEnabled(False) + self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) + else: self.connection_id = uuid.uuid4() - self.remote_geonode_version = None self.update_connection_details() # self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) ok_signals = [ @@ -157,7 +151,6 @@ def get_connection_settings(self) -> ConnectionSettings: base_url=self.url_le.text().strip().rstrip("#/"), 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(), ) @@ -166,38 +159,22 @@ def test_connection(self): widget.setEnabled(False) current_settings = self.get_connection_settings() - self.discovery_task = network_task.NetworkRequestTask( - [ - network.RequestToPerform( - QtCore.QUrl(f"{current_settings.base_url}/version.txt") - ) - ], - network_task_timeout=current_settings.network_requests_timeout, - authcfg=current_settings.auth_config, - description="Connect to a GeoNode client", - ) - self.discovery_task.task_done.connect(self.handle_discovery_test) - utils.show_message(self.bar, tr("Connecting..."), add_loading_widget=True) - qgis.core.QgsApplication.taskManager().addTask(self.discovery_task) - def handle_discovery_test(self, task_result: bool): + # Replace old version.txt request with /api/v2/ check + success = is_api_client_supported(current_settings.base_url) + + self.handle_discovery_test(success) + + def handle_discovery_test(self, is_supported: bool): self.enable_post_test_connection_buttons() - geonode_version = network.handle_discovery_test( - task_result, self.discovery_task - ) - if geonode_version is not None: - if apiclient.validate_version(geonode_version) == False: - message = "This GeoNode version is not supported..." - level = qgis.core.Qgis.Critical - self.remote_geonode_version = network.UNSUPPORTED_REMOTE - else: - self.remote_geonode_version = geonode_version - message = "Connection is valid" - level = qgis.core.Qgis.Info + + if is_supported: + message = "Connection is valid" + level = qgis.core.Qgis.Info else: - message = "Connection is not valid" + message = "This GeoNode instance does not support the API client." level = qgis.core.Qgis.Critical - self.remote_geonode_version = network.UNSUPPORTED_REMOTE + utils.show_message(self.bar, message, level) self.update_connection_details() @@ -236,20 +213,24 @@ def handle_wfs_version_detection_test(self, task_result: bool): self.wfs_version_cb.setCurrentIndex(index) def update_connection_details(self): - invalid_version = ( - self.remote_geonode_version is None - or self.remote_geonode_version == network.UNSUPPORTED_REMOTE - ) + # Determine if API is reachable + api_supported = is_api_client_supported(self.url_le.text().strip().rstrip("#/")) + self.detected_capabilities_lw.clear() self.detected_version_le.clear() - if not invalid_version: - # Enable the detected_version_db and OK button + + if api_supported: + # Enable the detected_version group box and OK button self.detected_version_gb.setEnabled(True) self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(True) current_settings = self.get_connection_settings() client: BaseGeonodeClient = apiclient.get_geonode_client(current_settings) - self.detected_version_le.setText(str(current_settings.geonode_version)) + + # No version stored; just show "v2 API available" + self.detected_version_le.setText("API v2 available") + + # Show capabilities from client self.detected_capabilities_lw.insertItems( 0, [cap.name for cap in client.capabilities] ) diff --git a/src/qgis_geonode/gui/geonode_data_source_widget.py b/src/qgis_geonode/gui/geonode_data_source_widget.py index ef8a995..37f250f 100644 --- a/src/qgis_geonode/gui/geonode_data_source_widget.py +++ b/src/qgis_geonode/gui/geonode_data_source_widget.py @@ -17,6 +17,7 @@ base, get_geonode_client, models, + is_api_client_supported, ) from .. import ( conf, @@ -300,6 +301,7 @@ def activate_connection_configuration(self, index: int): self.clear_search_results() self.current_page = 1 self.total_pages = 1 + current_text = self.connections_cmb.itemText(index) try: current_connection = conf.settings_manager.find_connection_by_name( @@ -307,42 +309,49 @@ def activate_connection_configuration(self, index: int): ) except ValueError: self.toggle_search_buttons(enable=False) + return + + conf.settings_manager.set_current_connection(current_connection.id) + + # Check if API v2 is supported + api_supported = is_api_client_supported(current_connection.base_url) + + if not api_supported: + self.show_message( + tr(_INVALID_CONNECTION_MESSAGE), level=qgis.core.Qgis.Critical + ) + self.api_client = None + self.toggle_search_buttons(enable=False) else: - conf.settings_manager.set_current_connection(current_connection.id) - if current_connection.geonode_version == network.UNSUPPORTED_REMOTE: - self.show_message( - tr(_INVALID_CONNECTION_MESSAGE), level=qgis.core.Qgis.Critical - ) - else: - if current_connection.geonode_version: - self.api_client = get_geonode_client(current_connection) - self._load_sorting_fields() - self.api_client.dataset_list_received.connect( - self.handle_dataset_list - ) - self.api_client.search_error_received.connect( - self.handle_search_error - ) - else: - # don't know if current config is valid or not yet, need to detect it - pass - self.update_gui(current_connection) - self.toggle_search_buttons() + # API is reachable so instantiate client + self.api_client = get_geonode_client(current_connection) + if self.api_client: + self._load_sorting_fields() + self.api_client.dataset_list_received.connect(self.handle_dataset_list) + self.api_client.search_error_received.connect(self.handle_search_error) + self.toggle_search_buttons(enable=True) + + self.update_gui(current_connection) def toggle_search_buttons(self, enable: typing.Optional[bool] = None): enable_search = False enable_previous = False enable_next = False + if enable is None or enable: current_connection = conf.settings_manager.get_current_connection_settings() if current_connection is not None: - if current_connection.geonode_version != network.UNSUPPORTED_REMOTE: + # Check if /api/v2/ is reachable + api_supported = is_api_client_supported(current_connection.base_url) + + if api_supported: for check_box in self.resource_types_btngrp.buttons(): if check_box.isChecked(): enable_search = True enable_previous = self.current_page > 1 enable_next = self.current_page < self.total_pages break + self.search_btn.setEnabled(enable_search) self.previous_btn.setEnabled(enable_previous) self.next_btn.setEnabled(enable_next) @@ -457,35 +466,38 @@ def discover_api_client(self, next_: typing.Callable, *next_args, **next_kwargs) def handle_api_client_discovery( self, next_: typing.Callable, task_result: bool, *next_args, **next_kwargs ): - geonode_version = network.handle_discovery_test( - task_result, self.discovery_task - ) current_connection = conf.settings_manager.get_current_connection_settings() - current_connection.geonode_version = ( - geonode_version - if geonode_version is not None - else network.UNSUPPORTED_REMOTE - ) + + # task_result should now reflect is_api_client_supported() + api_supported = task_result + conf.settings_manager.save_connection_settings(current_connection) self.update_connections_combobox() next_(*next_args, **next_kwargs) def search_geonode(self, reset_pagination: bool = False): search_params = self.get_search_filters() - if len(search_params.layer_types) > 0: - self.search_started.emit() - if reset_pagination: - self.current_page = 1 - self.total_pages = 1 - current_connection = conf.settings_manager.get_current_connection_settings() - if not current_connection.geonode_version: - self.discover_api_client( - next_=self.search_geonode, reset_pagination=reset_pagination - ) - elif self.api_client is None: - self.search_finished.emit(tr(_INVALID_CONNECTION_MESSAGE)) - else: - self.api_client.get_dataset_list(search_params) + + if len(search_params.layer_types) == 0: + return + + self.search_started.emit() + + if reset_pagination: + self.current_page = 1 + self.total_pages = 1 + + current_connection = conf.settings_manager.get_current_connection_settings() + + # Check if the API is reachable + if not is_api_client_supported(current_connection.base_url): + self.discover_api_client( + next_=self.search_geonode, reset_pagination=reset_pagination + ) + elif self.api_client is None: + self.search_finished.emit(tr(_INVALID_CONNECTION_MESSAGE)) + else: + self.api_client.get_dataset_list(search_params) def toggle_search_controls(self, enabled: bool): for widget in self._unusable_search_filters: diff --git a/src/qgis_geonode/network.py b/src/qgis_geonode/network.py index 0613ae9..9474968 100644 --- a/src/qgis_geonode/network.py +++ b/src/qgis_geonode/network.py @@ -170,15 +170,25 @@ def create_request( def handle_discovery_test( finished_task_result: bool, finished_task: qgis.core.QgsTask -) -> typing.Optional[packaging_version.Version]: - geonode_version = None - if finished_task_result: - response_contents = finished_task.response_contents[0] - if response_contents is not None and response_contents.qt_error is None: - geonode_version = packaging_version.parse( - response_contents.response_body.data().decode() - ) - return geonode_version +) -> bool: + """ + Returns True if the API v2 endpoint responded correctly. + """ + if not finished_task_result: + return False + + response_contents = finished_task.response_contents[0] + if response_contents is None or response_contents.qt_error is not None: + return False + + try: + data = response_contents.response_body.data().decode() + import json + + parsed = json.loads(data) + return isinstance(parsed, dict) and "resources" in parsed + except Exception: + return False def build_multipart(