From f8e93edabe9013a2c1229caa4c454fab09620125 Mon Sep 17 00:00:00 2001 From: Stijn Peeters Date: Wed, 18 Sep 2024 15:11:21 +0200 Subject: [PATCH] Simple extensions page in Control Panel --- common/lib/config_definition.py | 2 +- common/lib/helpers.py | 64 +++++++++++++++++++ webtool/__init__.py | 1 + .../controlpanel/extensions-list.html | 55 ++++++++++++++++ webtool/templates/controlpanel/layout.html | 4 +- webtool/views/views_extensions.py | 28 ++++++++ 6 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 webtool/templates/controlpanel/extensions-list.html create mode 100644 webtool/views/views_extensions.py diff --git a/common/lib/config_definition.py b/common/lib/config_definition.py index d1af7b95d..ae5361d2f 100644 --- a/common/lib/config_definition.py +++ b/common/lib/config_definition.py @@ -140,7 +140,7 @@ "type": UserInput.OPTION_TOGGLE, "default": False, "help": "Can restart/upgrade", - "tooltip": "Controls whether users can restart and upgrade 4CAT via the Control Panel" + "tooltip": "Controls whether users can restart, upgrade, and manage extensions 4CAT via the Control Panel" }, "privileges.can_upgrade_to_dev": { # this is NOT an admin privilege, because all admins automatically diff --git a/common/lib/helpers.py b/common/lib/helpers.py index d98fc8ed6..2911044f5 100644 --- a/common/lib/helpers.py +++ b/common/lib/helpers.py @@ -250,6 +250,70 @@ def get_ffmpeg_version(ffmpeg_path): return version.parse(ffmpeg_version) +def find_extensions(): + """ + Find 4CAT extensions and load their metadata + + Looks for subfolders of the extension folder, and loads additional metadata + where available. + + :return tuple: A tuple with two items; the extensions, as an ID -> metadata + dictionary, and a list of (str) errors encountered while loading + """ + extension_path = config.get("PATH_ROOT").joinpath("extensions") + errors = [] + if not extension_path.exists() or not extension_path.is_dir(): + return [], None + + # each folder in the extensions folder is an extension + extensions = { + extension.name: { + "name": extension.name, + "version": "", + "url": "", + "git_url": "", + "is_git": False + } for extension in sorted(os.scandir(extension_path), key=lambda x: x.name) if extension.is_dir() + } + + # collect metadata for extensions + allowed_metadata_keys = ("name", "version", "url") + cwd = os.getcwd() + for extension in extensions: + extension_folder = extension_path.joinpath(extension) + metadata_file = extension_folder.joinpath("metadata.json") + if metadata_file.exists(): + with metadata_file.open() as infile: + try: + metadata = json.load(infile) + extensions[extension].update({k: metadata[k] for k in metadata if k in allowed_metadata_keys}) + except (TypeError, ValueError) as e: + errors.append(f"Error reading metadata file for extension '{extension}' ({e})") + continue + + extensions[extension]["is_git"] = extension_folder.joinpath(".git/HEAD").exists() + if extensions[extension]["is_git"]: + # try to get remote URL + try: + os.chdir(extension_folder) + origin = subprocess.run(["git", "config", "--get", "remote.origin.url"], stderr=subprocess.PIPE, + stdout=subprocess.PIPE) + if origin.returncode != 0 or not origin.stdout: + raise ValueError() + repository = origin.stdout.decode("utf-8").strip() + if repository.endswith(".git") and "github.com" in repository: + # use repo URL + repository = repository[:-4] + extensions[extension]["git_url"] = repository + except (subprocess.SubprocessError, IndexError, TypeError, ValueError, FileNotFoundError) as e: + print(e) + pass + finally: + os.chdir(cwd) + + return extensions, errors + + def convert_to_int(value, default=0): """ Convert a value to an integer, with a fallback diff --git a/webtool/__init__.py b/webtool/__init__.py index 0fd3ecf5d..6c1786ad5 100644 --- a/webtool/__init__.py +++ b/webtool/__init__.py @@ -105,6 +105,7 @@ # import all views import webtool.views.views_admin +import webtool.views.views_extensions import webtool.views.views_restart import webtool.views.views_user import webtool.views.views_dataset diff --git a/webtool/templates/controlpanel/extensions-list.html b/webtool/templates/controlpanel/extensions-list.html new file mode 100644 index 000000000..bd7243fde --- /dev/null +++ b/webtool/templates/controlpanel/extensions-list.html @@ -0,0 +1,55 @@ +{% extends "controlpanel/layout.html" %} + +{% block title %}4CAT Extensions{% endblock %} +{% block body_class %}plain-page admin {{ body_class }}{% endblock %} +{% block subbreadcrumbs %}{% set navigation.sub = "extensions" %}{% endblock %} + +{% block body %} +
+
+

4CAT Extensions

+ {% for notice in flashes %} +

{{ notice|safe }}

+ {% endfor %} +

4CAT extensions can be installed in the extensions folder in the 4CAT root. For more + information, see the README file in that folder. This page lists all currently installed extensions; + currently, to manage extensions you will need to access the filesystem and move files into the correct + location manually.

+
+ + + + + + + + + + + + {% if extensions %} + {% for extension_id, extension in extensions.items() %} + + + + + + {% endfor %} + {% else %} + + + + {% endif %} +
ExtensionVersionLinks
{{ extension_id }}{% if extension_id != extension.name %} + {{ extension.name }}{% endif %}{% if extension.version %}{{ extension.version }}{% else %}unknown{% endif %} + {% if extension.url and extension.url != extension.git_url %} + Website{% endif %} + {% if extension.git_url %}Remote git repository{% endif %} +
No 4CAT extensions are installed.
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/webtool/templates/controlpanel/layout.html b/webtool/templates/controlpanel/layout.html index b6fe93e07..ade0dcf7e 100644 --- a/webtool/templates/controlpanel/layout.html +++ b/webtool/templates/controlpanel/layout.html @@ -14,7 +14,9 @@ Configuration Tags{% endif %} {% if __user_config("privileges.admin.can_manage_notifications") %} Notifications{% endif %} - {% if __user_config("privileges.admin.can_view_status") %} + {% if __user_config("privileges.admin.can_restart") %} + Extensions{% endif %} + {% if __user_config("privileges.admin.can_manage_users") %} View logs{% endif %} {% if __user_config("privileges.admin.can_manipulate_all_datasets") %} Dataset bulk management{% endif %} diff --git a/webtool/views/views_extensions.py b/webtool/views/views_extensions.py new file mode 100644 index 000000000..2f120e2a3 --- /dev/null +++ b/webtool/views/views_extensions.py @@ -0,0 +1,28 @@ +""" +4CAT extension views - routes to manipulate 4CAT extensions +""" + +from flask import render_template, request, flash, get_flashed_messages +from flask_login import current_user, login_required + +from webtool import app, config +from common.lib.helpers import find_extensions + +from common.config_manager import ConfigWrapper + +config = ConfigWrapper(config, user=current_user, request=request) + + +@app.route("/admin/extensions/") +@login_required +def extensions_panel(): + extensions, load_errors = find_extensions() + + if extensions is None: + return render_template("error.html", message="No extensions folder is available - cannot " + "list or manipulate extensions in this 4CAT server."), 500 + + for error in load_errors: + flash(error) + + return render_template("controlpanel/extensions-list.html", extensions=extensions, flashes=get_flashed_messages())