6
0
mirror of https://github.com/ChristianLight/tutor.git synced 2025-02-12 06:08:26 +00:00

Add convenient fmt.echo_* functions

This commit is contained in:
Régis Behmo 2019-05-12 00:10:14 +02:00
parent 407659ff06
commit 0199a1e916
14 changed files with 92 additions and 97 deletions

View File

@ -21,7 +21,8 @@ class ConfigTests(unittest.TestCase):
self.assertEqual("abcd", config["MYSQL_ROOT_PASSWORD"]) self.assertEqual("abcd", config["MYSQL_ROOT_PASSWORD"])
def test_save_twice(self): @unittest.mock.patch.object(tutor_config.fmt, "echo")
def test_save_twice(self, mock_echo):
with tempfile.TemporaryDirectory() as root: with tempfile.TemporaryDirectory() as root:
tutor_config.save(root, silent=True) tutor_config.save(root, silent=True)
config1 = tutor_config.load_user(root) config1 = tutor_config.load_user(root)
@ -31,7 +32,8 @@ class ConfigTests(unittest.TestCase):
self.assertEqual(config1, config2) self.assertEqual(config1, config2)
def test_removed_entry_is_added_on_save(self): @unittest.mock.patch.object(tutor_config.fmt, "echo")
def test_removed_entry_is_added_on_save(self, mock_echo):
with tempfile.TemporaryDirectory() as root: with tempfile.TemporaryDirectory() as root:
with unittest.mock.patch.object( with unittest.mock.patch.object(
tutor_config.utils, "random_string" tutor_config.utils, "random_string"

View File

@ -1,5 +1,6 @@
import tempfile import tempfile
import unittest import unittest
import unittest.mock
from tutor.commands import config as tutor_config from tutor.commands import config as tutor_config
from tutor import env from tutor import env
@ -7,6 +8,7 @@ from tutor import exceptions
class EnvTests(unittest.TestCase): class EnvTests(unittest.TestCase):
def test_walk_templates(self): def test_walk_templates(self):
templates = list(env.walk_templates("local")) templates = list(env.walk_templates("local"))
self.assertIn("local/docker-compose.yml", templates) self.assertIn("local/docker-compose.yml", templates)
@ -32,7 +34,8 @@ class EnvTests(unittest.TestCase):
rendered = env.render_file(config, "scripts", "create_databases.sh") rendered = env.render_file(config, "scripts", "create_databases.sh")
self.assertIn("testpassword", rendered) self.assertIn("testpassword", rendered)
def test_render_file_missing_configuration(self): @unittest.mock.patch.object(tutor_config.fmt, "echo")
def test_render_file_missing_configuration(self, _):
self.assertRaises( self.assertRaises(
exceptions.TutorError, env.render_file, {}, "local", "docker-compose.yml" exceptions.TutorError, env.render_file, {}, "local", "docker-compose.yml"
) )

View File

@ -21,26 +21,22 @@ def build():
@opts.root @opts.root
def debug(root): def debug(root):
docker_run(root) docker_run(root)
click.echo( fmt.echo_info(
fmt.info(
"The debuggable APK file is available in {}".format( "The debuggable APK file is available in {}".format(
tutor_env.data_path(root, "android") tutor_env.data_path(root, "android")
) )
) )
)
@click.command(help="Build the application in release mode") @click.command(help="Build the application in release mode")
@opts.root @opts.root
def release(root): def release(root):
docker_run(root, "./gradlew", "assembleProdRelease") docker_run(root, "./gradlew", "assembleProdRelease")
click.echo( fmt.echo_info(
fmt.info(
"The production APK file is available in {}".format( "The production APK file is available in {}".format(
tutor_env.data_path(root, "android") tutor_env.data_path(root, "android")
) )
) )
)
@click.command(help="Pull the docker image") @click.command(help="Pull the docker image")

View File

@ -21,7 +21,7 @@ def main():
try: try:
cli() cli()
except exceptions.TutorError as e: except exceptions.TutorError as e:
sys.stderr.write(fmt.error("Error: {}\n".format(e.args[0]))) fmt.echo_error("Error: {}".format(e.args[0]))
sys.exit(1) sys.exit(1)

View File

@ -58,7 +58,7 @@ def printvalue(root, key):
config = load_current(root, defaults) config = load_current(root, defaults)
merge(config, defaults) merge(config, defaults)
try: try:
print(config[key]) fmt.echo(config[key])
except KeyError: except KeyError:
raise exceptions.TutorError("Missing configuration value: {}".format(key)) raise exceptions.TutorError("Missing configuration value: {}".format(key))
@ -84,22 +84,33 @@ def load(root):
if should_update_env: if should_update_env:
save_env(root, config) save_env(root, config)
merge(config, defaults)
return config return config
def merge(config, defaults):
"""
Merge default values with user configuration.
"""
for key, value in defaults.items():
if key not in config:
if isinstance(value, str):
config[key] = env.render_str(config, value)
else:
config[key] = value
def pre_upgrade_announcement(root): def pre_upgrade_announcement(root):
""" """
Inform the user that the current environment is not up-to-date. Crash if running in Inform the user that the current environment is not up-to-date. Crash if running in
non-interactive mode. non-interactive mode.
""" """
click.echo( fmt.echo_alert(
fmt.alert(
"The current environment stored at {} is not up-to-date: it is at " "The current environment stored at {} is not up-to-date: it is at "
"v{} while the 'tutor' binary is at v{}.".format( "v{} while the 'tutor' binary is at v{}.".format(
env.base_dir(root), env.version(root), __version__ env.base_dir(root), env.version(root), __version__
) )
) )
)
if os.isatty(sys.stdin.fileno()): if os.isatty(sys.stdin.fileno()):
# Interactive mode: ask the user permission to proceed # Interactive mode: ask the user permission to proceed
click.confirm( click.confirm(
@ -339,11 +350,9 @@ def convert_json2yml(root):
config = json.load(fi) config = json.load(fi)
save_config(root, config) save_config(root, config)
os.remove(json_path) os.remove(json_path)
click.echo( fmt.echo_info(
fmt.info(
"File config.json detected in {} and converted to config.yml".format(root) "File config.json detected in {} and converted to config.yml".format(root)
) )
)
def save_config(root, config): def save_config(root, config):
@ -351,12 +360,12 @@ def save_config(root, config):
utils.ensure_file_directory_exists(path) utils.ensure_file_directory_exists(path)
with open(path, "w") as of: with open(path, "w") as of:
serialize.dump(config, of) serialize.dump(config, of)
click.echo(fmt.info("Configuration saved to {}".format(path))) fmt.echo_info("Configuration saved to {}".format(path))
def save_env(root, config): def save_env(root, config):
env.render_full(root, config) env.render_full(root, config)
click.echo(fmt.info("Environment generated in {}".format(env.base_dir(root)))) fmt.echo_info("Environment generated in {}".format(env.base_dir(root)))
def config_path(root): def config_path(root):

View File

@ -49,7 +49,7 @@ def build(root, image, no_cache, build_arg):
config = tutor_config.load(root) config = tutor_config.load(root)
for img in openedx_image_names(config, image): for img in openedx_image_names(config, image):
tag = get_tag(config, img) tag = get_tag(config, img)
click.echo(fmt.info("Building image {}".format(tag))) fmt.echo_info("Building image {}".format(tag))
command = ["build", "-t", tag, tutor_env.pathjoin(root, "build", img)] command = ["build", "-t", tag, tutor_env.pathjoin(root, "build", img)]
if no_cache: if no_cache:
command.append("--no-cache") command.append("--no-cache")
@ -65,7 +65,7 @@ def pull(root, image):
config = tutor_config.load(root) config = tutor_config.load(root)
for img in image_names(config, image): for img in image_names(config, image):
tag = get_tag(config, img) tag = get_tag(config, img)
click.echo(fmt.info("Pulling image {}".format(tag))) fmt.echo_info("Pulling image {}".format(tag))
utils.execute("docker", "pull", tag) utils.execute("docker", "pull", tag)
@ -75,7 +75,7 @@ def pull(root, image):
def push(root, image): def push(root, image):
config = tutor_config.load(root) config = tutor_config.load(root)
for tag in openedx_image_tags(config, image): for tag in openedx_image_tags(config, image):
click.echo(fmt.info("Pushing image {}".format(tag))) fmt.echo_info("Pushing image {}".format(tag))
utils.execute("docker", "push", tag) utils.execute("docker", "push", tag)

View File

@ -116,9 +116,9 @@ def createuser(root, superuser, staff, name, email):
@click.command(help="Import the demo course") @click.command(help="Import the demo course")
@opts.root @opts.root
def importdemocourse(root): def importdemocourse(root):
click.echo(fmt.info("Importing demo course")) fmt.echo_info("Importing demo course")
scripts.import_demo_course(root, run_sh) scripts.import_demo_course(root, run_sh)
click.echo(fmt.info("Re-indexing courses")) fmt.echo_info("Re-indexing courses")
indexcourses.callback(root) indexcourses.callback(root)

View File

@ -59,7 +59,7 @@ def start(root, detach):
docker_compose(root, config, *command) docker_compose(root, config, *command)
if detach: if detach:
click.echo(fmt.info("The Open edX platform is now running in detached mode")) fmt.echo_info("The Open edX platform is now running in detached mode")
http = "https" if config["ACTIVATE_HTTPS"] else "http" http = "https" if config["ACTIVATE_HTTPS"] else "http"
urls = [] urls = []
if not config["ACTIVATE_HTTPS"] and not config["WEB_PROXY"]: if not config["ACTIVATE_HTTPS"] and not config["WEB_PROXY"]:
@ -70,15 +70,13 @@ def start(root, detach):
urls.append( urls.append(
"{http}://{cms_host}".format(http=http, cms_host=config["CMS_HOST"]) "{http}://{cms_host}".format(http=http, cms_host=config["CMS_HOST"])
) )
click.echo( fmt.echo_info(
fmt.info(
"""Your Open edX platform is ready and can be accessed at the following urls: """Your Open edX platform is ready and can be accessed at the following urls:
{}""".format( {}""".format(
"\n ".join(urls) "\n ".join(urls)
) )
) )
)
@click.command(help="Stop a running platform") @click.command(help="Stop a running platform")
@ -166,14 +164,13 @@ def https_create(root):
""" """
config = tutor_config.load(root) config = tutor_config.load(root)
if not config["ACTIVATE_HTTPS"]: if not config["ACTIVATE_HTTPS"]:
click.echo(fmt.info("HTTPS is not activated: certificate generation skipped")) fmt.echo_info("HTTPS is not activated: certificate generation skipped")
return return
script = scripts.render_template(config, "https_create.sh") script = scripts.render_template(config, "https_create.sh")
if config["WEB_PROXY"]: if config["WEB_PROXY"]:
click.echo( fmt.echo_info(
fmt.info(
"""You are running Tutor behind a web proxy (WEB_PROXY=true): SSL/TLS """You are running Tutor behind a web proxy (WEB_PROXY=true): SSL/TLS
certificates must be generated on the host. For instance, to generate certificates must be generated on the host. For instance, to generate
certificates with Let's Encrypt, run: certificates with Let's Encrypt, run:
@ -184,7 +181,6 @@ See the official certbot documentation for your platform: https://certbot.eff.or
indent(script, " ") indent(script, " ")
) )
) )
)
return return
utils.docker_run( utils.docker_run(
@ -205,11 +201,10 @@ See the official certbot documentation for your platform: https://certbot.eff.or
def https_renew(root): def https_renew(root):
config = tutor_config.load(root) config = tutor_config.load(root)
if not config["ACTIVATE_HTTPS"]: if not config["ACTIVATE_HTTPS"]:
click.echo(fmt.info("HTTPS is not activated: certificate renewal skipped")) fmt.echo_info("HTTPS is not activated: certificate renewal skipped")
return return
if config["WEB_PROXY"]: if config["WEB_PROXY"]:
click.echo( fmt.echo_info(
fmt.info(
"""You are running Tutor behind a web proxy (WEB_PROXY=true): SSL/TLS """You are running Tutor behind a web proxy (WEB_PROXY=true): SSL/TLS
certificates must be renewed on the host. For instance, to renew Let's Encrypt certificates must be renewed on the host. For instance, to renew Let's Encrypt
certificates, run: certificates, run:
@ -218,7 +213,6 @@ certificates, run:
See the official certbot documentation for your platform: https://certbot.eff.org/""" See the official certbot documentation for your platform: https://certbot.eff.org/"""
) )
)
return return
docker_run = [ docker_run = [
"--volume", "--volume",
@ -264,9 +258,9 @@ def createuser(root, superuser, staff, name, email):
def importdemocourse(root): def importdemocourse(root):
config = tutor_config.load(root) config = tutor_config.load(root)
check_service_is_activated(config, "cms") check_service_is_activated(config, "cms")
click.echo(fmt.info("Importing demo course")) fmt.echo_info("Importing demo course")
scripts.import_demo_course(root, run_sh) scripts.import_demo_course(root, run_sh)
click.echo(fmt.info("Re-indexing courses")) fmt.echo_info("Re-indexing courses")
indexcourses.callback(root) indexcourses.callback(root)
@ -295,9 +289,7 @@ def portainer(root, port):
"portainer/portainer:latest", "portainer/portainer:latest",
"--bind=:{}".format(port), "--bind=:{}".format(port),
] ]
click.echo( fmt.echo_info("View the Portainer UI at http://localhost:{port}".format(port=port))
fmt.info("View the Portainer UI at http://localhost:{port}".format(port=port))
)
utils.docker_run(*docker_run) utils.docker_run(*docker_run)

View File

@ -38,7 +38,7 @@ def webui():
) )
def start(root, port, host): def start(root, port, host):
check_gotty_binary(root) check_gotty_binary(root)
click.echo(fmt.info("Access the Tutor web UI at http://{}:{}".format(host, port))) fmt.echo_info("Access the Tutor web UI at http://{}:{}".format(host, port))
while True: while True:
config = load_config(root) config = load_config(root)
user = config["user"] user = config["user"]
@ -57,11 +57,9 @@ def start(root, port, host):
credential = "{}:{}".format(user, password) credential = "{}:{}".format(user, password)
command += ["--credential", credential] command += ["--credential", credential]
else: else:
click.echo( fmt.echo_alert(
fmt.alert(
"Running web UI without user authentication. Run 'tutor webui configure' to setup authentication" "Running web UI without user authentication. Run 'tutor webui configure' to setup authentication"
) )
)
command += [sys.argv[0], "ui"] command += [sys.argv[0], "ui"]
p = subprocess.Popen(command) p = subprocess.Popen(command)
while True: while True:
@ -91,20 +89,18 @@ def start(root, port, host):
) )
def configure(root, user, password): def configure(root, user, password):
save_config(root, {"user": user, "password": password}) save_config(root, {"user": user, "password": password})
click.echo( fmt.echo_info(
fmt.info(
"The web UI configuration has been updated. " "The web UI configuration has been updated. "
"If at any point you wish to reset your username and password, " "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(root))
) )
)
def check_gotty_binary(root): def check_gotty_binary(root):
path = gotty_path(root) path = gotty_path(root)
if os.path.exists(path): if os.path.exists(path):
return return
click.echo(fmt.info("Downloading gotty to {}...".format(path))) fmt.echo_info("Downloading gotty to {}...".format(path))
# Generate release url # Generate release url
# Note: I don't know how to handle arm # Note: I don't know how to handle arm

View File

@ -5,6 +5,7 @@ import shutil
import jinja2 import jinja2
from . import exceptions from . import exceptions
from . import fmt
from . import utils from . import utils
from .__about__ import __version__ from .__about__ import __version__
@ -45,10 +46,10 @@ class Renderer:
try: try:
return cls.__render(template, config) return cls.__render(template, config)
except (jinja2.exceptions.TemplateError, exceptions.TutorError): except (jinja2.exceptions.TemplateError, exceptions.TutorError):
print("Error rendering template", path) fmt.echo_error("Error rendering template " + path)
raise raise
except Exception: except Exception:
print("Unknown error rendering template", path) fmt.echo_error("Unknown error rendering template " + path)
raise raise
@classmethod @classmethod

View File

@ -10,6 +10,10 @@ def title(text):
return click.style(message, fg="green") return click.style(message, fg="green")
def echo_info(text):
echo(info(text))
def info(text): def info(text):
return click.style(text, fg="blue") return click.style(text, fg="blue")
@ -18,6 +22,10 @@ def error(text):
return click.style(text, fg="red") return click.style(text, fg="red")
def echo_error(text):
echo(error(text), err=True)
def command(text): def command(text):
return click.style(text, fg="magenta") return click.style(text, fg="magenta")
@ -26,5 +34,13 @@ def question(text):
return click.style(text, fg="yellow") return click.style(text, fg="yellow")
def echo_alert(text):
echo(alert(text))
def alert(text): def alert(text):
return click.style("⚠️ " + text, fg="yellow", bold=True) return click.style("⚠️ " + text, fg="yellow", bold=True)
def echo(text, err=False):
click.echo(text, err=err)

View File

@ -5,28 +5,28 @@ from . import fmt
def migrate(root, config, run_func): def migrate(root, config, run_func):
click.echo(fmt.info("Creating all databases...")) fmt.echo_info("Creating all databases...")
run_script(root, config, "mysql-client", "create_databases.sh", run_func) run_script(root, config, "mysql-client", "create_databases.sh", run_func)
if config["ACTIVATE_LMS"]: if config["ACTIVATE_LMS"]:
click.echo(fmt.info("Running lms migrations...")) fmt.echo_info("Running lms migrations...")
run_script(root, config, "lms", "migrate_lms.sh", run_func) run_script(root, config, "lms", "migrate_lms.sh", run_func)
if config["ACTIVATE_CMS"]: if config["ACTIVATE_CMS"]:
click.echo(fmt.info("Running cms migrations...")) fmt.echo_info("Running cms migrations...")
run_script(root, config, "cms", "migrate_cms.sh", run_func) run_script(root, config, "cms", "migrate_cms.sh", run_func)
if config["ACTIVATE_FORUM"]: if config["ACTIVATE_FORUM"]:
click.echo(fmt.info("Running forum migrations...")) fmt.echo_info("Running forum migrations...")
run_script(root, config, "forum", "migrate_forum.sh", run_func) run_script(root, config, "forum", "migrate_forum.sh", run_func)
if config["ACTIVATE_NOTES"]: if config["ACTIVATE_NOTES"]:
click.echo(fmt.info("Running notes migrations...")) fmt.echo_info("Running notes migrations...")
run_script(root, config, "notes", "migrate_django.sh", run_func) run_script(root, config, "notes", "migrate_django.sh", run_func)
if config["ACTIVATE_XQUEUE"]: if config["ACTIVATE_XQUEUE"]:
click.echo(fmt.info("Running xqueue migrations...")) fmt.echo_info("Running xqueue migrations...")
run_script(root, config, "xqueue", "migrate_django.sh", run_func) run_script(root, config, "xqueue", "migrate_django.sh", run_func)
if config["ACTIVATE_LMS"]: if config["ACTIVATE_LMS"]:
click.echo(fmt.info("Creating oauth2 users...")) fmt.echo_info("Creating oauth2 users...")
run_script(root, config, "lms", "oauth2.sh", run_func) run_script(root, config, "lms", "oauth2.sh", run_func)
click.echo(fmt.info("Databases ready.")) fmt.echo_info("Databases ready.")
def create_user(root, run_func, superuser, staff, name, email): def create_user(root, run_func, superuser, staff, name, email):

View File

@ -1,20 +0,0 @@
---
LMS_HOST: "www.myopenedx.com"
CMS_HOST: "studio.{{ LMS_HOST }}"
PLATFORM_NAME: "My Open edX"
CONTACT_EMAIL: "contact@{{ LMS_HOST }}"
LANGUAGE_CODE: "en"
ACTIVATE_HTTPS: false
ACTIVATE_NOTES: false
ACTIVATE_XQUEUE: false
SECRET_KEY: "{{ RAND24 }}"
MYSQL_ROOT_PASSWORD: "{{ RAND8 }}"
OPENEDX_MYSQL_PASSWORD: "{{ RAND8 }}"
NOTES_MYSQL_PASSWORD: "{{ RAND8 }}"
NOTES_SECRET_KEY: "{{ RAND24 }}"
NOTES_OAUTH2_SECRET: "{{ RAND24 }}"
XQUEUE_AUTH_PASSWORD: "{{ RAND8 }}"
XQUEUE_MYSQL_PASSWORD: "{{ RAND8 }}"
XQUEUE_SECRET_KEY: "{{ RAND24 }}"
ANDROID_OAUTH2_SECRET: "{{ RAND24 }}"
ID: "{{ RAND8 }}"