tutor/tutor/commands/plugins.py

150 lines
5.0 KiB
Python

import os
import shutil
import urllib.request
from typing import List
import click
from .. import config as tutor_config
from .. import env as tutor_env
from .. import exceptions, fmt, plugins
from .context import Context
@click.group(
name="plugins",
short_help="Manage Tutor plugins",
help="Manage Tutor plugins to add new features and customize your Open edX platform",
)
def plugins_command() -> None:
"""
All plugin commands should work even if there is no existing config file. This is
because users might enable plugins prior to configuration or environment generation.
"""
@click.command(name="list", help="List installed plugins")
@click.pass_obj
def list_command(context: Context) -> None:
config = tutor_config.load_full(context.root)
for plugin in plugins.iter_installed():
status = "" if plugins.is_enabled(config, plugin.name) else " (disabled)"
print(
"{plugin}=={version}{status}".format(
plugin=plugin.name, status=status, version=plugin.version
)
)
@click.command(help="Enable a plugin")
@click.argument("plugin_names", metavar="plugin", nargs=-1)
@click.pass_obj
def enable(context: Context, plugin_names: List[str]) -> None:
config = tutor_config.load_minimal(context.root)
for plugin in plugin_names:
plugins.enable(config, plugin)
fmt.echo_info("Plugin {} enabled".format(plugin))
tutor_config.save_config_file(context.root, config)
fmt.echo_info(
"You should now re-generate your environment with `tutor config save`."
)
@click.command(
short_help="Disable a plugin",
help="Disable one or more plugins. Specify 'all' to disable all enabled plugins at once.",
)
@click.argument("plugin_names", metavar="plugin", nargs=-1)
@click.pass_obj
def disable(context: Context, plugin_names: List[str]) -> None:
config = tutor_config.load_minimal(context.root)
disable_all = "all" in plugin_names
for plugin in plugins.iter_enabled(config):
if disable_all or plugin.name in plugin_names:
fmt.echo_info("Disabling plugin {}...".format(plugin.name))
for key, value in plugin.config_set.items():
value = tutor_env.render_unknown(config, value)
fmt.echo_info(" Removing config entry {}={}".format(key, value))
plugins.disable(config, plugin)
delete_plugin(context.root, plugin.name)
fmt.echo_info(" Plugin disabled")
tutor_config.save_config_file(context.root, config)
fmt.echo_info(
"You should now re-generate your environment with `tutor config save`."
)
def delete_plugin(root: str, name: str) -> None:
plugin_dir = tutor_env.pathjoin(root, "plugins", name)
if os.path.exists(plugin_dir):
try:
shutil.rmtree(plugin_dir)
except PermissionError as e:
raise exceptions.TutorError(
"Could not delete file {} from plugin {} in folder {}".format(
e.filename, name, plugin_dir
)
)
@click.command(
short_help="Print the location of yaml-based plugins",
help="""Print the location of yaml-based plugins. This location can be manually
defined by setting the {} environment variable""".format(
plugins.DictPlugin.ROOT_ENV_VAR_NAME
),
)
def printroot() -> None:
fmt.echo(plugins.DictPlugin.ROOT)
@click.command(
short_help="Install a plugin",
help="""Install a plugin, either from a local YAML file or a remote, web-hosted
location. The plugin will be installed to {}.""".format(
plugins.DictPlugin.ROOT_ENV_VAR_NAME
),
)
@click.argument("location")
def install(location: str) -> None:
basename = os.path.basename(location)
if not basename.endswith(".yml"):
basename += ".yml"
plugin_path = os.path.join(plugins.DictPlugin.ROOT, basename)
if location.startswith("http"):
# Download file
response = urllib.request.urlopen(location)
content = response.read().decode()
elif os.path.isfile(location):
# Read file
with open(location) as f:
content = f.read()
else:
raise exceptions.TutorError("No plugin found at {}".format(location))
# Save file
if not os.path.exists(plugins.DictPlugin.ROOT):
os.makedirs(plugins.DictPlugin.ROOT)
with open(plugin_path, "w", newline="\n") as f:
f.write(content)
fmt.echo_info("Plugin installed at {}".format(plugin_path))
def add_plugin_commands(command_group: click.Group) -> None:
"""
Add commands provided by all plugins to the given command group. Each command is
added with a name that is equal to the plugin name.
"""
for plugin in plugins.iter_installed():
if isinstance(plugin.command, click.Command):
plugin.command.name = plugin.name
command_group.add_command(plugin.command)
plugins_command.add_command(list_command)
plugins_command.add_command(enable)
plugins_command.add_command(disable)
plugins_command.add_command(printroot)
plugins_command.add_command(install)