Skip to content

Commit

Permalink
Simple extensions page in Control Panel
Browse files Browse the repository at this point in the history
  • Loading branch information
stijn-uva committed Sep 18, 2024
1 parent b5be128 commit f8e93ed
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 2 deletions.
2 changes: 1 addition & 1 deletion common/lib/config_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions common/lib/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions webtool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions webtool/templates/controlpanel/extensions-list.html
Original file line number Diff line number Diff line change
@@ -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 %}
<article class="small">
<section class="result-list">
<h2><span>4CAT Extensions</span></h2>
{% for notice in flashes %}
<p class="form-notice">{{ notice|safe }}</p>
{% endfor %}
<p>4CAT extensions can be installed in the <code>extensions</code> 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.</p>
<div class="user-panel">
<table class="fullwidth notification-table cp-table">
<colgroup>
<col>
<col>
<col class="actions">
</colgroup>
<tr>
<th>Extension</th>
<th>Version</th>
<th>Links</th>
</tr>
{% if extensions %}
{% for extension_id, extension in extensions.items() %}
<tr>
<td><span class="property-badge">{{ extension_id }}</span>{% if extension_id != extension.name %}
{{ extension.name }}{% endif %}</td>
<td>{% if extension.version %}{{ extension.version }}{% else %}unknown{% endif %}</td>
<td>
{% if extension.url and extension.url != extension.git_url %}
<a href="{{ extension.url }}"><i class="fa fa-fw fa-link" aria-hidden="true"></i><span
class="sr-only">Website</span></a>{% endif %}
{% if extension.git_url %}<a href="{{ extension.git_url }}"><i class="fab fa-fw fa-git{% if "github.com" in extension.git_url %}hub{% endif %}"
aria-hidden="true"></i><span
class="sr-only">Remote git repository</span></a>{% endif %}
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3">No 4CAT extensions are installed.</td>
</tr>
{% endif %}
</table>
</div>
</section>
</article>
{% endblock %}
4 changes: 3 additions & 1 deletion webtool/templates/controlpanel/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
<li{% if navigation.sub == "tags" %} class="current"{% endif %}><a href="{{ url_for("manipulate_tags") }}">Configuration Tags</a></li>{% endif %}
{% if __user_config("privileges.admin.can_manage_notifications") %}
<li{% if navigation.sub == "notifications" %} class="current"{% endif %}><a href="{{ url_for("manipulate_notifications") }}">Notifications</a></li>{% endif %}
{% if __user_config("privileges.admin.can_view_status") %}
{% if __user_config("privileges.admin.can_restart") %}
<li{% if navigation.sub == "extensions" %} class="current"{% endif %}><a href="{{ url_for("extensions_panel") }}">Extensions</a></li>{% endif %}
{% if __user_config("privileges.admin.can_manage_users") %}
<li{% if navigation.sub == "logs" %} class="current"{% endif %}><a href="{{ url_for("view_logs") }}">View logs</a></li>{% endif %}
{% if __user_config("privileges.admin.can_manipulate_all_datasets") %}
<li{% if navigation.sub == "dataset-bulk" %} class="current"{% endif %}><a href="{{ url_for("dataset_bulk") }}">Dataset bulk management</a></li>{% endif %}
Expand Down
28 changes: 28 additions & 0 deletions webtool/views/views_extensions.py
Original file line number Diff line number Diff line change
@@ -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())

0 comments on commit f8e93ed

Please sign in to comment.