7
0
mirror of https://github.com/ChristianLight/tutor.git synced 2024-06-01 13:50:47 +00:00

Move "-r/--root" option to parent command level

This commit is contained in:
Régis Behmo 2019-12-12 17:05:56 +01:00
parent e9f200a102
commit 13de3c8adc
14 changed files with 266 additions and 233 deletions

View File

@ -4,6 +4,7 @@ Note: Breaking changes between versions are indicated by "💥".
## Unreleased
- [Improvement] Move ``-r/--root`` option to parent command level
- [Bugfix] Fix course about page visibility
- [Improvement] Print gunicorn access logs in the console
- 💥[Improvement] Get rid of the `indexcourses` and `portainer` command (#269)

View File

@ -5,7 +5,12 @@ Local deployment
This method is for deploying Open edX locally on a single server, where docker images are orchestrated with `docker-compose <https://docs.docker.com/compose/overview/>`_.
In the following, environment and data files will be generated in a user-specific project folder which will be referred to as the "**project root**". On Linux, the default project root is ``~/.local/share/tutor``. An alternative project root can be defined by passing the ``--root=...`` option to most commands, or define the ``TUTOR_ROOT=...`` environment variable.
In the following, environment and data files will be generated in a user-specific project folder which will be referred to as the "**project root**". On Linux, the default project root is ``~/.local/share/tutor``. An alternative project root can be defined by passing the ``--root=...`` option to the ``tutor`` command, or define the ``TUTOR_ROOT=...`` environment variable::
tutor --root=/path/to/tutorroot run ...
# Or equivalently:
export TUTOR_ROOT=/path/to/tutorroot
tutor run ...
Main commands
-------------

View File

@ -3,7 +3,6 @@ import click
from .. import config as tutor_config
from .. import env as tutor_env
from .. import fmt
from .. import opts
from .. import utils
@ -18,31 +17,31 @@ def build():
@click.command(help="Build the application in debug mode")
@opts.root
def debug(root):
docker_run(root)
@click.pass_obj
def debug(context):
docker_run(context.root)
fmt.echo_info(
"The debuggable APK file is available in {}".format(
tutor_env.data_path(root, "android")
tutor_env.data_path(context.root, "android")
)
)
@click.command(help="Build the application in release mode")
@opts.root
def release(root):
docker_run(root, "./gradlew", "assembleProdRelease")
@click.pass_obj
def release(context):
docker_run(context.root, "./gradlew", "assembleProdRelease")
fmt.echo_info(
"The production APK file is available in {}".format(
tutor_env.data_path(root, "android")
tutor_env.data_path(context.root, "android")
)
)
@click.command(help="Pull the docker image")
@opts.root
def pullimage(root):
config = tutor_config.load(root)
@click.pass_obj
def pullimage(context):
config = tutor_config.load(context.root)
utils.execute("docker", "pull", config["DOCKER_IMAGE_ANDROID"])

View File

@ -1,6 +1,7 @@
#! /usr/bin/env python3
import sys
import appdirs
import click
import click_repl
@ -18,9 +19,15 @@ from .. import exceptions
from .. import fmt
# pylint: disable=too-few-public-methods
class Context:
def __init__(self, root):
self.root = root
def main():
try:
cli()
cli() # pylint: disable=no-value-for-parameter
except exceptions.TutorError as e:
fmt.echo_error("Error: {}".format(e.args[0]))
sys.exit(1)
@ -28,8 +35,18 @@ def main():
@click.group(context_settings={"help_option_names": ["-h", "--help", "help"]})
@click.version_option(version=__version__)
def cli():
pass
@click.option(
"-r",
"--root",
envvar="TUTOR_ROOT",
default=appdirs.user_data_dir(appname="tutor"),
show_default=True,
type=click.Path(resolve_path=True),
help="Root project directory (environment variable: TUTOR_ROOT)",
)
@click.pass_context
def cli(context, root):
context.obj = Context(root)
@click.command(help="Print this help", name="help")

View File

@ -5,7 +5,7 @@ from .. import env
from .. import exceptions
from .. import fmt
from .. import interactive as interactive_config
from .. import opts
from .. import serialize
@click.group(
@ -17,14 +17,24 @@ def config_command():
pass
class YamlParamType(click.ParamType):
name = "yaml"
def convert(self, value, param, ctx):
try:
k, v = value.split("=")
except ValueError:
self.fail("'{}' is not of the form 'key=value'.".format(value), param, ctx)
return k, serialize.parse(v)
@click.command(help="Create and save configuration interactively")
@opts.root
@click.option("-i", "--interactive", is_flag=True, help="Run interactively")
@click.option(
"-s",
"--set",
"set_",
type=opts.YamlParamType(),
type=YamlParamType(),
multiple=True,
metavar="KEY=VAL",
help="Set a configuration value (can be used multiple times)",
@ -35,28 +45,31 @@ def config_command():
multiple=True,
help="Remove a configuration value (can be used multiple times)",
)
def save(root, interactive, set_, unset):
config, defaults = interactive_config.load_all(root, interactive=interactive)
@click.pass_obj
def save(context, interactive, set_, unset):
config, defaults = interactive_config.load_all(
context.root, interactive=interactive
)
if set_:
tutor_config.merge(config, dict(set_), force=True)
for key in unset:
config.pop(key, None)
tutor_config.save(root, config)
tutor_config.save(context.root, config)
tutor_config.merge(config, defaults)
env.save(root, config)
env.save(context.root, config)
@click.command(help="Print the project root")
@opts.root
def printroot(root):
click.echo(root)
@click.pass_obj
def printroot(context):
click.echo(context.root)
@click.command(help="Print a configuration value")
@opts.root
@click.argument("key")
def printvalue(root, key):
config = tutor_config.load(root)
@click.pass_obj
def printvalue(context, key):
config = tutor_config.load(context.root)
try:
fmt.echo(config[key])
except KeyError:

View File

@ -2,7 +2,6 @@ import click
from .. import env as tutor_env
from .. import fmt
from .. import opts
from .. import utils
@ -11,53 +10,72 @@ def dev():
pass
edx_platform_path_option = click.option(
"-P",
"--edx-platform-path",
envvar="TUTOR_EDX_PLATFORM_PATH",
type=click.Path(exists=True, dir_okay=True, resolve_path=True),
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)",
)
edx_platform_development_settings_option = click.option(
"-S",
"--edx-platform-settings",
envvar="TUTOR_EDX_PLATFORM_SETTINGS",
default="tutor.development",
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)",
)
@click.command(
help="Run a command in one of the containers",
context_settings={"ignore_unknown_options": True},
)
@opts.root
@opts.edx_platform_path
@opts.edx_platform_development_settings
@edx_platform_path_option
@edx_platform_development_settings_option
@click.argument("service")
@click.argument("command", default=None, required=False)
@click.argument("args", nargs=-1)
def run(root, edx_platform_path, edx_platform_settings, service, command, args):
@click.pass_obj
def run(context, edx_platform_path, edx_platform_settings, service, command, args):
run_command = [service]
if command:
run_command.append(command)
if args:
run_command += args
docker_compose_run(root, edx_platform_path, edx_platform_settings, *run_command)
docker_compose_run(
context.root, edx_platform_path, edx_platform_settings, *run_command
)
@click.command(
help="Exec a command in a running container",
context_settings={"ignore_unknown_options": True},
)
@opts.root
@click.argument("service")
@click.argument("command")
@click.argument("args", nargs=-1)
def execute(root, service, command, args):
@click.pass_obj
def execute(context, service, command, args):
exec_command = ["exec", service, command]
if args:
exec_command += args
docker_compose(root, *exec_command)
docker_compose(context.root, *exec_command)
@click.command(help="Run a development server")
@opts.root
@opts.edx_platform_path
@opts.edx_platform_development_settings
@edx_platform_path_option
@edx_platform_development_settings_option
@click.argument("service", type=click.Choice(["lms", "cms"]))
def runserver(root, edx_platform_path, edx_platform_settings, service):
@click.pass_obj
def runserver(context, edx_platform_path, edx_platform_settings, service):
port = service_port(service)
fmt.echo_info(
"The {} service will be available at http://localhost:{}".format(service, port)
)
docker_compose_run(
root,
context.root,
edx_platform_path,
edx_platform_settings,
"-p",
@ -71,9 +89,9 @@ def runserver(root, edx_platform_path, edx_platform_settings, service):
@click.command(help="Stop a running development platform")
@opts.root
def stop(root):
docker_compose(root, "rm", "--stop", "--force")
@click.pass_obj
def stop(context):
docker_compose(context.root, "rm", "--stop", "--force")
def docker_compose_run(root, edx_platform_path, edx_platform_settings, *command):

View File

@ -5,7 +5,6 @@ import click
from .. import config as tutor_config
from .. import env as tutor_env
from .. import images
from .. import opts
from .. import plugins
BASE_IMAGE_NAMES = ["openedx", "forum", "android"]
@ -21,7 +20,6 @@ def images_command():
short_help="Build docker images",
help="Build the docker images necessary for an Open edX platform.",
)
@opts.root
@click.argument("image", nargs=-1)
@click.option(
"--no-cache", is_flag=True, help="Do not use cache when building the image"
@ -32,10 +30,11 @@ def images_command():
multiple=True,
help="Set build-time docker ARGS in the form 'myarg=value'. This option may be specified multiple times.",
)
def build(root, image, no_cache, build_arg):
config = tutor_config.load(root)
@click.pass_obj
def build(context, image, no_cache, build_arg):
config = tutor_config.load(context.root)
for i in image:
build_image(root, config, i, no_cache, build_arg)
build_image(context.root, config, i, no_cache, build_arg)
def build_image(root, config, image, no_cache, build_arg):
@ -77,10 +76,10 @@ def build_image(root, config, image, no_cache, build_arg):
@click.command(short_help="Pull images from the Docker registry")
@opts.root
@click.argument("image", nargs=-1)
def pull(root, image):
config = tutor_config.load(root)
@click.pass_obj
def pull(context, image):
config = tutor_config.load(context.root)
for i in image:
pull_image(config, i)
@ -101,15 +100,15 @@ def pull_image(config, image):
@click.command(short_help="Push images to the Docker registry")
@opts.root
@click.argument("image", nargs=-1)
def push(root, image):
config = tutor_config.load(root)
@click.pass_obj
def push(context, image):
config = tutor_config.load(context.root)
for i in image:
push_image(root, config, i)
push_image(config, i)
def push_image(root, config, image):
def push_image(config, image):
# Push base images
for img in BASE_IMAGE_NAMES:
if image in [img, "all"]:

View File

@ -4,7 +4,6 @@ from .. import config as tutor_config
from .. import env as tutor_env
from .. import fmt
from .. import interactive as interactive_config
from .. import opts
from .. import scripts
from .. import utils
@ -15,11 +14,11 @@ def k8s():
@click.command(help="Configure and run Open edX from scratch")
@opts.root
@click.option("-I", "--non-interactive", is_flag=True, help="Run non-interactively")
def quickstart(root, non_interactive):
@click.pass_obj
def quickstart(context, non_interactive):
click.echo(fmt.title("Interactive platform configuration"))
config = interactive_config.update(root, interactive=(not non_interactive))
config = interactive_config.update(context.root, interactive=(not non_interactive))
if config["ACTIVATE_HTTPS"] and not config["WEB_PROXY"]:
fmt.echo_alert(
"Potentially invalid configuration: ACTIVATE_HTTPS=true WEB_PROXY=false\n"
@ -28,21 +27,21 @@ def quickstart(root, non_interactive):
" more information."
)
click.echo(fmt.title("Updating the current environment"))
tutor_env.save(root, config)
tutor_env.save(context, config)
click.echo(fmt.title("Starting the platform"))
start.callback(root)
start.callback()
click.echo(fmt.title("Database creation and migrations"))
init.callback(root)
init.callback()
@click.command(help="Run all configured Open edX services")
@opts.root
def start(root):
@click.pass_obj
def start(context):
# Create namespace
utils.kubectl(
"apply",
"--kustomize",
tutor_env.pathjoin(root),
tutor_env.pathjoin(context.root),
"--wait",
"--selector",
"app.kubernetes.io/component=namespace",
@ -51,29 +50,29 @@ def start(root):
utils.kubectl(
"apply",
"--kustomize",
tutor_env.pathjoin(root),
tutor_env.pathjoin(context.root),
"--wait",
"--selector",
"app.kubernetes.io/component=volume",
)
# Create everything else
utils.kubectl("apply", "--kustomize", tutor_env.pathjoin(root))
utils.kubectl("apply", "--kustomize", tutor_env.pathjoin(context.root))
@click.command(help="Stop a running platform")
@opts.root
def stop(root):
config = tutor_config.load(root)
@click.pass_obj
def stop(context):
config = tutor_config.load(context.root)
utils.kubectl(
"delete", *resource_selector(config), "deployments,services,ingress,configmaps"
)
@click.command(help="Reboot an existing platform")
@opts.root
def reboot(root):
stop.callback(root)
start.callback(root)
@click.pass_obj
def reboot(context):
stop.callback()
start.callback()
def resource_selector(config, *selectors):
@ -87,24 +86,28 @@ def resource_selector(config, *selectors):
@click.command(help="Completely delete an existing platform")
@opts.root
@click.option("-y", "--yes", is_flag=True, help="Do not ask for confirmation")
def delete(root, yes):
@click.pass_obj
def delete(context, yes):
if not yes:
click.confirm(
"Are you sure you want to delete the platform? All data will be removed.",
abort=True,
)
utils.kubectl(
"delete", "-k", tutor_env.pathjoin(root), "--ignore-not-found=true", "--wait"
"delete",
"-k",
tutor_env.pathjoin(context.root),
"--ignore-not-found=true",
"--wait",
)
@click.command(help="Initialise all applications")
@opts.root
def init(root):
config = tutor_config.load(root)
runner = K8sScriptRunner(root, config)
@click.pass_obj
def init(context):
config = tutor_config.load(context.root)
runner = K8sScriptRunner(context.root, config)
for service in ["mysql", "elasticsearch", "mongodb"]:
if runner.is_activated(service):
wait_for_pod_ready(config, service)
@ -112,7 +115,6 @@ def init(root):
@click.command(help="Create an Open edX user and interactively set their password")
@opts.root
@click.option("--superuser", is_flag=True, help="Make superuser")
@click.option("--staff", is_flag=True, help="Make staff user")
@click.option(
@ -122,9 +124,10 @@ def init(root):
)
@click.argument("name")
@click.argument("email")
def createuser(root, superuser, staff, password, name, email):
config = tutor_config.load(root)
runner = K8sScriptRunner(root, config)
@click.pass_obj
def createuser(context, superuser, staff, password, name, email):
config = tutor_config.load(context.root)
runner = K8sScriptRunner(context.root, config)
runner.check_service_is_activated("lms")
command = scripts.create_user_command(
superuser, staff, name, email, password=password
@ -133,31 +136,31 @@ def createuser(root, superuser, staff, password, name, email):
@click.command(help="Import the demo course")
@opts.root
def importdemocourse(root):
@click.pass_obj
def importdemocourse(context):
fmt.echo_info("Importing demo course")
config = tutor_config.load(root)
runner = K8sScriptRunner(root, config)
config = tutor_config.load(context.root)
runner = K8sScriptRunner(context.root, config)
scripts.import_demo_course(runner)
@click.command(name="exec", help="Execute a command in a pod of the given application")
@opts.root
@click.argument("service")
@click.argument("command")
def exec_command(root, service, command):
config = tutor_config.load(root)
@click.pass_obj
def exec_command(context, service, command):
config = tutor_config.load(context.root)
kubectl_exec(config, service, command, attach=True)
@click.command(help="View output from containers")
@opts.root
@click.option("-c", "--container", help="Print the logs of this specific container")
@click.option("-f", "--follow", is_flag=True, help="Follow log output")
@click.option("--tail", type=int, help="Number of lines to show from each container")
@click.argument("service")
def logs(root, container, follow, tail, service):
config = tutor_config.load(root)
@click.pass_obj
def logs(context, container, follow, tail, service):
config = tutor_config.load(context.root)
command = ["logs"]
selectors = ["app.kubernetes.io/name=" + service] if service else []

View File

@ -6,7 +6,6 @@ from .. import config as tutor_config
from .. import env as tutor_env
from .. import fmt
from .. import interactive as interactive_config
from .. import opts
from .. import scripts
from .. import utils
@ -20,48 +19,48 @@ def local():
@click.command(help="Configure and run Open edX from scratch")
@opts.root
@click.option("-I", "--non-interactive", is_flag=True, help="Run non-interactively")
@click.option(
"-p", "--pullimages", "pullimages_", is_flag=True, help="Update docker images"
)
def quickstart(root, non_interactive, pullimages_):
@click.pass_obj
def quickstart(context, non_interactive, pullimages_):
click.echo(fmt.title("Interactive platform configuration"))
config = interactive_config.update(root, interactive=(not non_interactive))
config = interactive_config.update(context.root, interactive=(not non_interactive))
click.echo(fmt.title("Updating the current environment"))
tutor_env.save(root, config)
tutor_env.save(context.root, config)
click.echo(fmt.title("Stopping any existing platform"))
stop.callback(root, [])
stop.callback([])
click.echo(fmt.title("HTTPS certificates generation"))
https_create.callback(root)
https_create.callback()
if pullimages_:
click.echo(fmt.title("Docker image updates"))
pullimages.callback(root)
pullimages.callback()
click.echo(fmt.title("Starting the platform in detached mode"))
start.callback(root, True, [])
start.callback(True, [])
click.echo(fmt.title("Database creation and migrations"))
init.callback(root)
init.callback()
echo_platform_info(config)
@click.command(help="Update docker images")
@opts.root
def pullimages(root):
config = tutor_config.load(root)
docker_compose(root, config, "pull")
@click.pass_obj
def pullimages(context):
config = tutor_config.load(context.root)
docker_compose(context.root, config, "pull")
@click.command(help="Run all or a selection of configured Open edX services")
@opts.root
@click.option("-d", "--detach", is_flag=True, help="Start in daemon mode")
@click.argument("services", metavar="service", nargs=-1)
def start(root, detach, services):
@click.pass_obj
def start(context, detach, services):
command = ["up", "--remove-orphans"]
if detach:
command.append("-d")
config = tutor_config.load(root)
docker_compose(root, config, *command, *services)
config = tutor_config.load(context.root)
docker_compose(context.root, config, *command, *services)
def echo_platform_info(config):
@ -82,23 +81,22 @@ def echo_platform_info(config):
@click.command(help="Stop a running platform")
@opts.root
@click.argument("services", metavar="service", nargs=-1)
def stop(root, services):
config = tutor_config.load(root)
docker_compose(root, config, "rm", "--stop", "--force", *services)
@click.pass_obj
def stop(context, services):
config = tutor_config.load(context.root)
docker_compose(context.root, config, "rm", "--stop", "--force", *services)
@click.command(
short_help="Reboot an existing platform",
help="This is more than just a restart: with reboot, the platform is fully stopped before being restarted again",
)
@opts.root
@click.option("-d", "--detach", is_flag=True, help="Start in daemon mode")
@click.argument("services", metavar="service", nargs=-1)
def reboot(root, detach, services):
stop.callback(root, services)
start.callback(root, detach, services)
def reboot(detach, services):
stop.callback(services)
start.callback(detach, services)
@click.command(
@ -108,10 +106,10 @@ restart all services. Note that this performs a 'docker-compose restart', so new
may not be taken into account. It is useful for reloading settings, for instance. To
fully stop the platform, use the 'reboot' command.""",
)
@opts.root
@click.argument("service")
def restart(root, service):
config = tutor_config.load(root)
@click.pass_obj
def restart(context, service):
config = tutor_config.load(context.root)
command = ["restart"]
if service == "openedx":
if config["ACTIVATE_LMS"]:
@ -120,19 +118,19 @@ def restart(root, service):
command += ["cms", "cms_worker"]
elif service != "all":
command += [service]
docker_compose(root, config, *command)
docker_compose(context.root, config, *command)
@click.command(
help="Run a command in one of the containers",
context_settings={"ignore_unknown_options": True},
)
@opts.root
@click.option("--entrypoint", help="Override the entrypoint of the image")
@click.argument("service")
@click.argument("command", default=None, required=False)
@click.argument("args", nargs=-1)
def run(root, entrypoint, service, command, args):
@click.pass_obj
def run(context, entrypoint, service, command, args):
run_command = ["run", "--rm"]
if entrypoint:
run_command += ["--entrypoint", entrypoint]
@ -141,31 +139,31 @@ def run(root, entrypoint, service, command, args):
run_command.append(command)
if args:
run_command += args
config = tutor_config.load(root)
docker_compose(root, config, *run_command)
config = tutor_config.load(context.root)
docker_compose(context.root, config, *run_command)
@click.command(
help="Exec a command in a running container",
context_settings={"ignore_unknown_options": True},
)
@opts.root
@click.argument("service")
@click.argument("command")
@click.argument("args", nargs=-1)
def execute(root, service, command, args):
@click.pass_obj
def execute(context, service, command, args):
exec_command = ["exec", service, command]
if args:
exec_command += args
config = tutor_config.load(root)
docker_compose(root, config, *exec_command)
config = tutor_config.load(context.root)
docker_compose(context.root, config, *exec_command)
@click.command(help="Initialise all applications")
@opts.root
def init(root):
config = tutor_config.load(root)
runner = ScriptRunner(root, config)
@click.pass_obj
def init(context):
config = tutor_config.load(context.root)
runner = ScriptRunner(context.root, config)
scripts.initialise(runner)
@ -179,12 +177,12 @@ that is convenient when developing new plugins. Ex:
tutor local hook discovery discovery hooks discovery init""",
name="hook",
)
@opts.root
@click.argument("service")
@click.argument("path", nargs=-1)
def run_hook(root, service, path):
config = tutor_config.load(root)
runner = ScriptRunner(root, config)
@click.pass_obj
def run_hook(context, service, path):
config = tutor_config.load(context.root)
runner = ScriptRunner(context.root, config)
fmt.echo_info(
"Running '{}' hook in '{}' container...".format(".".join(path), service)
)
@ -197,8 +195,8 @@ def https():
@click.command(help="Create https certificates", name="create")
@opts.root
def https_create(root):
@click.pass_obj
def https_create(context):
"""
Note: there are a couple issues with https certificate generation.
1. Certificates are generated and renewed by using port 80, which is not necessarily open.
@ -206,8 +204,8 @@ def https_create(root):
b. It may be occupied by an external web server
2. On certificate renewal, nginx is not reloaded
"""
config = tutor_config.load(root)
runner = ScriptRunner(root, config)
config = tutor_config.load(context.root)
runner = ScriptRunner(context.root, config)
if not config["ACTIVATE_HTTPS"]:
fmt.echo_info("HTTPS is not activated: certificate generation skipped")
return
@ -230,7 +228,7 @@ See the official certbot documentation for your platform: https://certbot.eff.or
utils.docker_run(
"--volume",
"{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")),
"{}:/etc/letsencrypt/".format(tutor_env.data_path(context.root, "letsencrypt")),
"-p",
"80:80",
"--entrypoint=sh",
@ -242,9 +240,9 @@ See the official certbot documentation for your platform: https://certbot.eff.or
@click.command(help="Renew https certificates", name="renew")
@opts.root
def https_renew(root):
config = tutor_config.load(root)
@click.pass_obj
def https_renew(context):
config = tutor_config.load(context.root)
if not config["ACTIVATE_HTTPS"]:
fmt.echo_info("HTTPS is not activated: certificate renewal skipped")
return
@ -261,7 +259,7 @@ See the official certbot documentation for your platform: https://certbot.eff.or
return
docker_run = [
"--volume",
"{}:/etc/letsencrypt/".format(tutor_env.data_path(root, "letsencrypt")),
"{}:/etc/letsencrypt/".format(tutor_env.data_path(context.root, "letsencrypt")),
"-p",
"80:80",
"certbot/certbot:latest",
@ -271,23 +269,22 @@ See the official certbot documentation for your platform: https://certbot.eff.or
@click.command(help="View output from containers")
@opts.root
@click.option("-f", "--follow", is_flag=True, help="Follow log output")
@click.option("--tail", type=int, help="Number of lines to show from each container")
@click.argument("service", nargs=-1)
def logs(root, follow, tail, service):
@click.pass_obj
def logs(context, follow, tail, service):
command = ["logs"]
if follow:
command += ["--follow"]
if tail is not None:
command += ["--tail", str(tail)]
command += service
config = tutor_config.load(root)
docker_compose(root, config, *command)
config = tutor_config.load(context.root)
docker_compose(context.root, config, *command)
@click.command(help="Create an Open edX user and interactively set their password")
@opts.root
@click.option("--superuser", is_flag=True, help="Make superuser")
@click.option("--staff", is_flag=True, help="Make staff user")
@click.option(
@ -297,9 +294,10 @@ def logs(root, follow, tail, service):
)
@click.argument("name")
@click.argument("email")
def createuser(root, superuser, staff, password, name, email):
config = tutor_config.load(root)
runner = ScriptRunner(root, config)
@click.pass_obj
def createuser(context, superuser, staff, password, name, email):
config = tutor_config.load(context.root)
runner = ScriptRunner(context.root, config)
runner.check_service_is_activated("lms")
command = scripts.create_user_command(
superuser, staff, name, email, password=password
@ -308,10 +306,10 @@ def createuser(root, superuser, staff, password, name, email):
@click.command(help="Import the demo course")
@opts.root
def importdemocourse(root):
config = tutor_config.load(root)
runner = ScriptRunner(root, config)
@click.pass_obj
def importdemocourse(context):
config = tutor_config.load(context.root)
runner = ScriptRunner(context.root, config)
fmt.echo_info("Importing demo course")
scripts.import_demo_course(runner)

View File

@ -5,7 +5,6 @@ import click
from .. import config as tutor_config
from .. import env as tutor_env
from .. import fmt
from .. import opts
from .. import plugins
@ -22,47 +21,57 @@ def plugins_command():
@click.command(name="list", help="List installed plugins")
@opts.root
def list_command(root):
config = tutor_config.load_user(root)
@click.pass_obj
def list_command(context):
config = tutor_config.load_user(context.root)
for name, _ in plugins.iter_installed():
status = "" if plugins.is_enabled(config, name) else " (disabled)"
print("{plugin}{status}".format(plugin=name, status=status))
@click.command(help="Enable a plugin")
@opts.root
@click.argument("plugin_names", metavar="plugin", nargs=-1)
def enable(root, plugin_names):
config = tutor_config.load_user(root)
@click.pass_obj
def enable(context, plugin_names):
config = tutor_config.load_user(context.root)
for plugin in plugin_names:
plugins.enable(config, plugin)
fmt.echo_info("Plugin {} enabled".format(plugin))
tutor_config.save(root, config)
tutor_config.save(context.root, config)
fmt.echo_info(
"You should now re-generate your environment with `tutor config save`."
)
@click.command(help="Disable a plugin")
@opts.root
@click.argument("plugin_names", metavar="plugin", nargs=-1)
def disable(root, plugin_names):
config = tutor_config.load_user(root)
@click.pass_obj
def disable(context, plugin_names):
config = tutor_config.load_user(context.root)
for plugin in plugin_names:
plugins.disable(config, plugin)
plugin_dir = tutor_env.pathjoin(root, "plugins", plugin)
plugin_dir = tutor_env.pathjoin(context.root, "plugins", plugin)
if os.path.exists(plugin_dir):
shutil.rmtree(plugin_dir)
fmt.echo_info("Plugin {} disabled".format(plugin))
tutor_config.save(root, config)
tutor_config.save(context.root, config)
fmt.echo_info(
"You should now re-generate your environment with `tutor config save`."
)
def iter_extra_plugin_commands():
"""
TODO document this. Merge with plugins.iter_commands? It's good to keepo
click-related stuff outside of the plugins module.
"""
for plugin_name, command in plugins.iter_commands():
command.name = plugin_name
yield command
plugins_command.add_command(list_command)
plugins_command.add_command(enable)
plugins_command.add_command(disable)

View File

@ -11,7 +11,6 @@ import click
# Note: it is important that this module does not depend on config, such that
# the web ui can be launched even where there is no configuration.
from .. import fmt
from .. import opts
from .. import env as tutor_env
from .. import serialize
@ -24,7 +23,6 @@ def webui():
@click.command(help="Start the web UI")
@opts.root
@click.option(
"-p",
"--port",
@ -36,15 +34,16 @@ def webui():
@click.option(
"-h", "--host", default="0.0.0.0", show_default=True, help="Host address to listen"
)
def start(root, port, host):
check_gotty_binary(root)
@click.pass_obj
def start(context, port, host):
check_gotty_binary(context.root)
fmt.echo_info("Access the Tutor web UI at http://{}:{}".format(host, port))
while True:
config = load_config(root)
config = load_config(context.root)
user = config["user"]
password = config["password"]
command = [
gotty_path(root),
gotty_path(context.root),
"--permit-write",
"--address",
host,
@ -66,7 +65,7 @@ def start(root, port, host):
try:
p.wait(timeout=2)
except subprocess.TimeoutExpired:
new_config = load_config(root)
new_config = load_config(context.root)
if new_config != config:
click.echo(
"WARNING configuration changed. Tutor web UI is now going to restart. Reload this page to continue."
@ -77,7 +76,6 @@ def start(root, port, host):
@click.command(help="Configure authentication")
@opts.root
@click.option("-u", "--user", prompt="User name", help="Authentication user name")
@click.option(
"-p",
@ -87,12 +85,13 @@ def start(root, port, host):
confirmation_prompt=True,
help="Authentication password",
)
def configure(root, user, password):
save_config(root, {"user": user, "password": password})
@click.pass_obj
def configure(context, user, password):
save_config(context.root, {"user": user, "password": password})
fmt.echo_info(
"The web UI configuration has been updated. "
"If at any point you wish to reset your username and password, "
"just delete the following file:\n\n {}".format(config_path(root))
"just delete the following file:\n\n {}".format(config_path(context.root))
)

View File

@ -185,7 +185,9 @@ def check_existing_config(root):
"""
if not os.path.exists(config_path(root)):
raise exceptions.TutorError(
"Project root does not exist. Make sure to generate the initial configuration with `tutor config save --interactive` or `tutor config quickstart` prior to running other commands."
"Project root does not exist. Make sure to generate the initial "
"configuration with `tutor config save --interactive` or `tutor config "
"quickstart` prior to running other commands."
)
env.check_is_up_to_date(root)

View File

@ -1,42 +0,0 @@
import appdirs
import click
from . import serialize
root = click.option(
"-r",
"--root",
envvar="TUTOR_ROOT",
default=appdirs.user_data_dir(appname="tutor"),
show_default=True,
type=click.Path(resolve_path=True),
help="Root project directory (environment variable: TUTOR_ROOT)",
)
edx_platform_path = click.option(
"-P",
"--edx-platform-path",
envvar="TUTOR_EDX_PLATFORM_PATH",
type=click.Path(exists=True, dir_okay=True, resolve_path=True),
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)",
)
edx_platform_development_settings = click.option(
"-S",
"--edx-platform-settings",
envvar="TUTOR_EDX_PLATFORM_SETTINGS",
default="tutor.development",
help="Mount a local edx-platform from the host (environment variable: TUTOR_EDX_PLATFORM_PATH)",
)
class YamlParamType(click.ParamType):
name = "yaml"
def convert(self, value, param, ctx):
try:
k, v = value.split("=")
except ValueError:
self.fail("'{}' is not of the form 'key=value'.".format(value), param, ctx)
return k, serialize.parse(v)

View File

@ -156,3 +156,15 @@ def iter_hooks(config, hook_name):
def iter_templates(config):
yield from Plugins.instance(config).iter_templates()
def iter_commands():
"""
TODO doesn't this slow down the cli? (we need to import all plugins)
Also, do we really need the `config` argument? Do we want to make it possible for disabled plugins to be loaded?
TODO document this
"""
for plugin_name, plugin in iter_installed():
command = getattr(plugin, "command", None)
if command:
yield plugin_name, command