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.
+
+
+
+
+
+
+
+
+ Extension |
+ Version |
+ Links |
+
+ {% if extensions %}
+ {% for extension_id, extension in extensions.items() %}
+
+ {{ 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 %}
+ |
+
+ {% endfor %}
+ {% else %}
+
+ No 4CAT extensions are installed. |
+
+ {% endif %}
+
+
+
+
+{% 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())