Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions qt/python/mantidqt/mantidqt/widgets/helpwindow/helpwindowbridge.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,62 @@
# Mantid Repository : https://github.com/mantidproject/mantid
#
# Copyright © 2017 ISIS Rutherford Appleton Laboratory UKRI,
# NScD Oak Ridge National Laboratory, European Spallation Source,
# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
# SPDX - License - Identifier: GPL - 3.0 +
import os
import sys
import argparse

from qtpy.QtWidgets import QApplication
from mantidqt.widgets.helpwindow.helpwindowpresenter import HelpWindowPresenter

_presenter = None


def show_help_page(relativeUrl, localDocs=None, onlineBaseUrl="https://docs.mantidproject.org/"):
def show_help_page(relativeUrl, onlineBaseUrl="https://docs.mantidproject.org/"):
"""
Show the help window at the given relative URL path.
Local docs path is now determined internally via ConfigService.
"""
global _presenter
if _presenter is None:
# Create a Presenter once. Re-use it on subsequent calls.
_presenter = HelpWindowPresenter(localDocs=localDocs, onlineBaseUrl=onlineBaseUrl)
_presenter = HelpWindowPresenter(onlineBaseUrl=onlineBaseUrl)

# Ask the Presenter to load the requested page
_presenter.show_help_page(relativeUrl)


def main(cmdargs=sys.argv):
"""
Run this script standalone to test the Python-based Help Window.
Local docs path is determined from Mantid's ConfigService.
"""
import argparse

parser = argparse.ArgumentParser(description="Standalone test of the Python-based Mantid Help Window.")
parser.add_argument(
"relativeUrl", nargs="?", default="", help="Relative doc path (e.g. 'algorithms/Load-v1.html'), defaults to 'index.html' if empty."
)
parser.add_argument("--local-docs", default=None, help="Path to local Mantid HTML docs. Overrides environment if set.")

parser.add_argument(
"--online-base-url",
default="https://docs.mantidproject.org/",
help="Base URL for online docs if local docs are not set or invalid.",
help="Base URL for online docs if local docs path from config is invalid or not found.",
)
args = parser.parse_args(cmdargs or sys.argv[1:])

# If user gave no --local-docs, fall back to environment
if args.local_docs is None:
args.local_docs = os.environ.get("MANTID_LOCAL_DOCS_BASE", None)
try:
import mantid.kernel

log = mantid.kernel.Logger("HelpWindowBridge")
log.information("Mantid kernel imported successfully.")
except ImportError as e:
print(f"ERROR: Failed to import Mantid Kernel: {e}", file=sys.stderr)
print(
"Ensure Mantid is built and PYTHONPATH is set correctly (e.g., export PYTHONPATH=/path/to/mantid/build/bin:$PYTHONPATH)",
file=sys.stderr,
)
sys.exit(1)

app = QApplication(sys.argv)

# Show the requested help page
show_help_page(relativeUrl=args.relativeUrl, localDocs=args.local_docs, onlineBaseUrl=args.online_base_url)
show_help_page(relativeUrl=args.relativeUrl, onlineBaseUrl=args.online_base_url)

sys.exit(app.exec_())

Expand Down
252 changes: 149 additions & 103 deletions qt/python/mantidqt/mantidqt/widgets/helpwindow/helpwindowmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,165 +6,211 @@
# SPDX - License - Identifier: GPL - 3.0 +
import os

import logging
from qtpy.QtCore import QUrl
from qtpy.QtWebEngineCore import QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo
# --- Logger and ConfigService Setup ---
try:
from mantid.kernel import Logger
from mantid.kernel import ConfigService
except ImportError:
print("Warning: Mantid Kernel (Logger/ConfigService) not found, using basic print/dummy.")

# Module-level logger for functions outside of classes
_logger = logging.getLogger(__name__)
class Logger:
def __init__(self, name):
self._name = name

def warning(self, msg):
print(f"WARNING [{self._name}]: {msg}")

def _get_version_string_for_url():
"""
Returns the Mantid version string formatted for use in documentation URLs.
For example, "v6.12.0" from version "6.12.0.1"
def debug(self, msg):
print(f"DEBUG [{self._name}]: {msg}")

Returns:
str: Formatted version string in the form "vX.Y.Z" or None if version cannot be determined
"""
versionStr = None
def information(self, msg):
print(f"INFO [{self._name}]: {msg}")

def error(self, msg):
print(f"ERROR [{self._name}]: {msg}")

class ConfigService: # Dummy for environments without Mantid
@staticmethod
def Instance():
class DummyInstance:
def getString(self, key, pathAbsolute=True):
return None

return DummyInstance()


log = Logger("HelpWindowModel")
# --------------------------------------

from qtpy.QtCore import QUrl # noqa: E402
from qtpy.QtWebEngineCore import QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo # noqa: E402


def getMantidVersionString():
"""Placeholder function to get Mantid version (e.g., 'v6.13.0')."""
try:
import mantid

# Use the mantid version object (proper way)
versionObj = mantid.version()
# Retrieve the patch
patch = versionObj.patch.split(".")[0]
versionStr = f"v{versionObj.major}.{versionObj.minor}.{patch}"
if hasattr(mantid, "__version__"):
versionParts = str(mantid.__version__).split(".")
if len(versionParts) >= 2:
return f"v{versionParts[0]}.{versionParts[1]}.0"
except ImportError:
_logger.warning("Could not determine Mantid version for documentation URL.")
except Exception as e:
_logger.warning(f"Error determining Mantid version for documentation URL: {e}")

return versionStr
pass
log.warning("Could not determine Mantid version for documentation URL.")
return None


class NoOpRequestInterceptor(QWebEngineUrlRequestInterceptor):
"""
A no-op interceptor that does nothing. Used if we're not loading local docs.
"""
"""A no-op interceptor used when loading online docs."""

def interceptRequest(self, info: QWebEngineUrlRequestInfo):
pass


class LocalRequestInterceptor(QWebEngineUrlRequestInterceptor):
"""
Intercepts requests so we can relax the CORS policy for loading MathJax fonts
from cdn.jsdelivr.net when using local docs.
"""
"""Intercepts requests for local docs (e.g., handle CORS)."""

def interceptRequest(self, info: QWebEngineUrlRequestInfo):
url = info.requestUrl()
if url.host() == "cdn.jsdelivr.net":
if url.host() == "cdn.jsdelivr.net": # Allow MathJax CDN
info.setHttpHeader(b"Access-Control-Allow-Origin", b"*")


class HelpWindowModel:
_logger = logging.getLogger(__name__)

MODE_LOCAL = "Local Docs"
MODE_OFFLINE = "Offline Docs"
MODE_ONLINE = "Online Docs"

def __init__(self, localDocsBase=None, onlineBase="https://docs.mantidproject.org/"):
self._rawLocalDocsBase = localDocsBase
self._rawOnlineBase = onlineBase.rstrip("/")

self._isLocal = False
self._modeString = self.MODE_ONLINE
self._baseUrl = self._rawOnlineBase
self._versionString = None

self._determine_mode_and_base_url()

def _determine_mode_and_base_url(self):
def __init__(self, online_base="https://docs.mantidproject.org/"):
# Store raw online base early, needed for fallback logic below
self._raw_online_base = online_base.rstrip("/")

# --- Step 1: Attempt to get local path from ConfigService ---
local_docs_path_from_config = None # Default if lookup fails or path is empty
try:
# ConfigService is imported at the top level now
config_service = ConfigService.Instance()
raw_path = config_service.getString("docs.html.root", True) # pathAbsolute=True
if raw_path: # Only assign if not empty
local_docs_path_from_config = raw_path
log.debug(f"Retrieved 'docs.html.root' from ConfigService: '{local_docs_path_from_config}'")
else:
log.debug("'docs.html.root' property is empty or not found in ConfigService.")
except Exception as e:
# Catch potential errors during ConfigService interaction
# This includes cases where the dummy ConfigService might be used
log.error(f"Error retrieving 'docs.html.root' from ConfigService: {e}. Defaulting to online mode.")
# local_docs_path_from_config remains None

# --- Step 2: Determine final mode and set ALL related state variables ---
# This method now sets _is_local, _mode_string, _base_url, _version_string
self._determine_mode_and_set_state(local_docs_path_from_config)

def _determine_mode_and_set_state(self, local_docs_path):
"""
Sets the internal state (_isLocal, _modeString, _baseWrl, _versionString)
based on the validity of localDocsBase and attempts to find a versioned URL.
Sets the final operational state (_is_local, _mode_string, _base_url, _version_string)
based *only* on the validity of the provided local_docs_path argument, which is the
result of the ConfigService lookup (can be a path string or None).
"""
if self._rawLocalDocsBase and os.path.isdir(self._rawLocalDocsBase):
self._isLocal = True
self._modeString = self.MODE_LOCAL
absLocalPath = os.path.abspath(self._rawLocalDocsBase)
self._baseUrl = QUrl.fromLocalFile(absLocalPath).toString()
self._versionString = None
self._logger.debug(f"Using {self._modeString} from {self._baseUrl}")
else:
if self._rawLocalDocsBase:
self._logger.warning(f"Local docs path '{self._rawLocalDocsBase}' is invalid or not found. Falling back to online docs.")

self._isLocal = False
self._modeString = self.MODE_ONLINE

isLikelyRelease = self._rawLocalDocsBase is None
if isLikelyRelease:
self._versionString = _get_version_string_for_url()

if self._versionString:
baseOnline = self._rawOnlineBase
if baseOnline.endswith("/stable"):
baseOnline = baseOnline[: -len("/stable")]

if self._versionString not in baseOnline:
self._baseUrl = f"{baseOnline.rstrip('/')}/{self._versionString}"
self._logger.debug(f"Using {self._modeString} (Version: {self._versionString}) from {self._baseUrl}")
else:
self._baseUrl = self._rawOnlineBase
self._logger.debug(f"Using {self._modeString} (Using provided base URL, possibly stable/latest): {self._baseUrl}")
else:
self._baseUrl = self._rawOnlineBase
self._logger.debug(f"Using {self._modeString} (Version: Unknown/Stable) from {self._baseUrl}")
log.debug(f"Determining final mode and state with local_docs_path='{local_docs_path}'")

# Check if the path from config is valid and points to an existing directory
if local_docs_path and os.path.isdir(local_docs_path):
# --- Configure for LOCAL/OFFLINE Mode ---
log.debug("Valid local docs path found. Configuring for Offline Mode.")
self._is_local = True
self._mode_string = self.MODE_OFFLINE
abs_local_path = os.path.abspath(local_docs_path) # Ensure absolute
# Base URL for local files needs 'file:///' prefix and correct path format
self._base_url = QUrl.fromLocalFile(abs_local_path).toString()
self._version_string = None # Version string not applicable for local docs mode
log.debug(f"Final state: Mode='{self._mode_string}', Base URL='{self._base_url}'")

else:
# --- Configure for ONLINE Mode ---
# Log reason if applicable
if local_docs_path: # Path was provided but invalid
log.warning(
f"Local docs path '{local_docs_path}' from ConfigService ('docs.html.root') is invalid or not found. Falling back to Online Mode." # noqa: E501
)
else: # Path was None (not found in config or error during lookup)
log.debug("No valid local docs path found from ConfigService. Configuring for Online Mode.")

self._is_local = False
self._mode_string = self.MODE_ONLINE

# Attempt to get versioned URL for online mode
self._version_string = getMantidVersionString() # Might return None

# Set final base URL based on online path and version string
if self._version_string:
base_online = self._raw_online_base
if base_online.endswith("/stable"):
base_online = base_online[: -len("/stable")]
# Avoid double versioning if base_online already has it
if self._version_string not in base_online:
self._base_url = f"{base_online.rstrip('/')}/{self._version_string}"
log.debug(f"Using versioned online URL: {self._base_url}")
else: # Use provided base as-is (likely includes 'stable' or version)
self._base_url = self._raw_online_base
log.debug(f"Using provided online base URL (version/stable implied): {self._base_url}")
else: # No version string found, use raw online base
self._base_url = self._raw_online_base
log.debug(f"Using default online base URL (version unknown): {self._base_url}")

log.debug(f"Final state: Mode='{self._mode_string}', Base URL='{self._base_url}', Version='{self._version_string}'")

# --- Getter methods remain the same ---
def is_local_docs_mode(self):
"""
:return: True if using local docs, False otherwise. Based on initial check.
:return: True if using local docs, False otherwise. Based on state set during init.
"""
return self._isLocal
return self._is_local

def get_mode_string(self):
"""
:return: User-friendly string indicating the mode ("Local Docs" or "Online Docs").
:return: User-friendly string indicating the mode ("Offline Docs" or "Online Docs").
"""
return self._modeString
return self._mode_string

def get_base_url(self):
"""`
:return: The determined base URL (either file:///path or https://docs...[/version])
"""
return self._baseUrl.rstrip("/") + "/"
:return: The determined base URL (either file:///path/ or https://docs...[/version]/) with trailing slash.
"""
# Ensure trailing slash for correct relative URL joining
return self._base_url.rstrip("/") + "/"

def build_help_url(self, relativeUrl):
# --- URL building methods use the state set during init ---
def build_help_url(self, relative_url):
"""
Returns a QUrl pointing to the determined doc source for the given relative URL.
"""
if not relativeUrl or not relativeUrl.lower().endswith((".html", ".htm")):
relativeUrl = "index.html"
if not relative_url or not relative_url.lower().endswith((".html", ".htm")):
relative_url = "index.html"

relativeUrl = relativeUrl.lstrip("/")
fullUrlStr = f"{self.get_base_url()}{relativeUrl}"
relative_url = relative_url.lstrip("/")
base = self.get_base_url() # Uses the final URL determined during init
full_url_str = f"{base}{relative_url}"

url = QUrl(fullUrlStr)
url = QUrl(full_url_str)
if not url.isValid():
self._logger.warning(f"Constructed invalid URL: {fullUrlStr} from base '{self.get_base_url()}' and relative '{relativeUrl}'")
log.warning(f"Constructed invalid URL: {full_url_str} from base '{base}' and relative '{relative_url}'")
return url

def get_home_url(self):
"""
Return the 'home' page URL:
- local 'index.html' if local docs are enabled
- online docs homepage otherwise
Return the 'home' page URL (index.html) based on the determined mode/base URL.
"""
return self.build_help_url("index.html")

# --- Interceptor creation uses the state set during init ---
def create_request_interceptor(self):
"""
Return an appropriate request interceptor:
- LocalRequestInterceptor if local docs are used (for mathjax CORS)
- NoOpRequestInterceptor otherwise
Return an appropriate request interceptor based on the determined mode (_is_local).
"""
if self._isLocal:
self._logger.debug("Using LocalRequestInterceptor.")
if self._is_local:
log.debug("Using LocalRequestInterceptor.")
return LocalRequestInterceptor()
else:
self._logger.debug("Using NoOpRequestInterceptor.")
log.debug("Using NoOpRequestInterceptor.")
return NoOpRequestInterceptor()
Loading