Skip to content

Commit

Permalink
refactor: deduplicate jobs code
Browse files Browse the repository at this point in the history
createuser, importdemocourse and settheme were 100% duplicated code between
k8s.py and compose.py.
  • Loading branch information
regisb committed Nov 8, 2022
1 parent 19f3f32 commit 109dd34
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 283 deletions.
25 changes: 7 additions & 18 deletions tests/test_jobs.py → tests/commands/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from tests.helpers import TestContext, temporary_root
from tutor import config as tutor_config
from tutor import jobs
from tutor.commands import jobs


class JobsTests(unittest.TestCase):
Expand All @@ -21,26 +21,21 @@ def test_initialise(self, mock_stdout: StringIO) -> None:
self.assertTrue(output.endswith("All services initialised."))

def test_create_user_command_without_staff(self) -> None:
command = jobs.create_user_command("superuser", False, "username", "email")
command = jobs.create_user_template("superuser", False, "username", "email", "p4ssw0rd")
self.assertNotIn("--staff", command)
self.assertIn("set_password", command)

def test_create_user_command_with_staff(self) -> None:
command = jobs.create_user_command("superuser", True, "username", "email")
command = jobs.create_user_template("superuser", True, "username", "email", "p4ssw0rd")
self.assertIn("--staff", command)

def test_create_user_command_with_staff_with_password(self) -> None:
command = jobs.create_user_command(
"superuser", True, "username", "email", "command"
)
self.assertIn("set_password", command)

@patch("sys.stdout", new_callable=StringIO)
def test_import_demo_course(self, mock_stdout: StringIO) -> None:
with temporary_root() as root:
context = TestContext(root)
config = tutor_config.load_full(root)
runner = context.job_runner(config)
jobs.import_demo_course(runner)
runner.run_job_from_str("cms", jobs.import_demo_course_template())

output = mock_stdout.getvalue()
service = re.search(r"Service: (\w*)", output)
Expand All @@ -60,7 +55,8 @@ def test_set_theme(self, mock_stdout: StringIO) -> None:
context = TestContext(root)
config = tutor_config.load_full(root)
runner = context.job_runner(config)
jobs.set_theme("sample_theme", ["domain1", "domain2"], runner)
command = jobs.set_theme_template("sample_theme", ["domain1", "domain2"])
runner.run_job_from_str("lms", command)

output = mock_stdout.getvalue()
service = re.search(r"Service: (\w*)", output)
Expand All @@ -73,10 +69,3 @@ def test_set_theme(self, mock_stdout: StringIO) -> None:
.strip()
.startswith('echo "Loading settings $DJANGO_SETTINGS_MODULE"')
)

def test_get_all_openedx_domains(self) -> None:
with temporary_root() as root:
config = tutor_config.load_full(root)
domains = jobs.get_all_openedx_domains(config)
self.assertTrue(domains)
self.assertEqual(6, len(domains))
68 changes: 5 additions & 63 deletions tutor/commands/compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

from tutor import config as tutor_config
from tutor import env as tutor_env
from tutor import fmt, hooks, jobs, serialize, utils
from tutor import fmt, hooks, serialize, utils
from tutor.commands import jobs
from tutor.commands.context import BaseJobContext
from tutor.exceptions import TutorError
from tutor.jobs import BaseComposeJobRunner
from tutor.types import Config


class ComposeJobRunner(jobs.BaseComposeJobRunner):
class ComposeJobRunner(BaseComposeJobRunner):
def __init__(self, root: str, config: Config):
super().__init__(root, config)
self.project_name = ""
Expand Down Expand Up @@ -310,64 +312,6 @@ def init(
jobs.initialise(runner, limit_to=limit)


@click.command(help="Create an Open edX user and interactively set their password")
@click.option("--superuser", is_flag=True, help="Make superuser")
@click.option("--staff", is_flag=True, help="Make staff user")
@click.option(
"-p",
"--password",
help="Specify password from the command line. If undefined, you will be prompted to input a password",
)
@click.argument("name")
@click.argument("email")
@click.pass_obj
def createuser(
context: BaseComposeContext,
superuser: str,
staff: bool,
password: str,
name: str,
email: str,
) -> None:
config = tutor_config.load(context.root)
runner = context.job_runner(config)
command = jobs.create_user_command(superuser, staff, name, email, password=password)
runner.run_job("lms", command)


@click.command(
help="Assign a theme to the LMS and the CMS. To reset to the default theme , use 'default' as the theme name."
)
@click.option(
"-d",
"--domain",
"domains",
multiple=True,
help=(
"Limit the theme to these domain names. By default, the theme is "
"applied to the LMS and the CMS, both in development and production mode"
),
)
@click.argument("theme_name")
@click.pass_obj
def settheme(
context: BaseComposeContext, domains: t.List[str], theme_name: str
) -> None:
config = tutor_config.load(context.root)
runner = context.job_runner(config)
domains = domains or jobs.get_all_openedx_domains(config)
jobs.set_theme(theme_name, domains, runner)


@click.command(help="Import the demo course")
@click.pass_obj
def importdemocourse(context: BaseComposeContext) -> None:
config = tutor_config.load(context.root)
runner = context.job_runner(config)
fmt.echo_info("Importing demo course")
jobs.import_demo_course(runner)


@click.command(
short_help="Run a command in a new container",
help=(
Expand Down Expand Up @@ -527,12 +471,10 @@ def add_commands(command_group: click.Group) -> None:
command_group.add_command(restart)
command_group.add_command(reboot)
command_group.add_command(init)
command_group.add_command(createuser)
command_group.add_command(importdemocourse)
command_group.add_command(settheme)
command_group.add_command(dc_command)
command_group.add_command(run)
command_group.add_command(copyfrom)
command_group.add_command(execute)
command_group.add_command(logs)
command_group.add_command(status)
jobs.add_commands(command_group)
187 changes: 187 additions & 0 deletions tutor/commands/jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"""
Common jobs that must be added both to local, dev and k8s commands.
"""

import typing as t

import click

from tutor import config as tutor_config
from tutor import fmt, hooks, jobs

from .context import BaseJobContext

BASE_OPENEDX_COMMAND = """
echo "Loading settings $DJANGO_SETTINGS_MODULE"
"""


@hooks.Actions.CORE_READY.add()
def _add_core_init_tasks() -> None:
"""
Declare core init scripts at runtime.
The context is important, because it allows us to select the init scripts based on
the --limit argument.
"""
with hooks.Contexts.APP("mysql").enter():
hooks.Filters.COMMANDS_INIT.add_item(("mysql", ("hooks", "mysql", "init")))
with hooks.Contexts.APP("lms").enter():
hooks.Filters.COMMANDS_INIT.add_item(("lms", ("hooks", "lms", "init")))
with hooks.Contexts.APP("cms").enter():
hooks.Filters.COMMANDS_INIT.add_item(("cms", ("hooks", "cms", "init")))


def initialise(runner: jobs.BaseJobRunner, limit_to: t.Optional[str] = None) -> None:
fmt.echo_info("Initialising all services...")
filter_context = hooks.Contexts.APP(limit_to).name if limit_to else None

# Pre-init tasks
iter_pre_init_tasks: t.Iterator[
t.Tuple[str, t.Iterable[str]]
] = hooks.Filters.COMMANDS_PRE_INIT.iterate(context=filter_context)
for service, path in iter_pre_init_tasks:
fmt.echo_info(f"Running pre-init task: {'/'.join(path)}")
runner.run_job_from_template(service, *path)

# Init tasks
iter_init_tasks: t.Iterator[
t.Tuple[str, t.Iterable[str]]
] = hooks.Filters.COMMANDS_INIT.iterate(context=filter_context)
for service, path in iter_init_tasks:
fmt.echo_info(f"Running init task: {'/'.join(path)}")
runner.run_job_from_template(service, *path)

fmt.echo_info("All services initialised.")


@click.command(help="Create an Open edX user and interactively set their password")
@click.option("--superuser", is_flag=True, help="Make superuser")
@click.option("--staff", is_flag=True, help="Make staff user")
@click.option(
"-p",
"--password",
help="Specify password from the command line. If undefined, you will be prompted to input a password",
prompt=True,
hide_input=True,
)
@click.argument("name")
@click.argument("email")
@click.pass_obj
def createuser(
context: BaseJobContext,
superuser: str,
staff: bool,
password: str,
name: str,
email: str,
) -> None:
run_job(
context, "lms", create_user_template(superuser, staff, name, email, password)
)


@click.command(help="Import the demo course")
@click.pass_obj
def importdemocourse(context: BaseJobContext) -> None:
run_job(context, "cms", import_demo_course_template())


@click.command(
help="Assign a theme to the LMS and the CMS. To reset to the default theme , use 'default' as the theme name."
)
@click.option(
"-d",
"--domain",
"domains",
multiple=True,
help=(
"Limit the theme to these domain names. By default, the theme is "
"applied to the LMS and the CMS, both in development and production mode"
),
)
@click.argument("theme_name")
@click.pass_obj
def settheme(context: BaseJobContext, domains: t.List[str], theme_name: str) -> None:
run_job(context, "lms", set_theme_template(theme_name, domains))


def run_job(context: BaseJobContext, service: str, command: str) -> None:
config = tutor_config.load(context.root)
runner = context.job_runner(config)
runner.run_job_from_str(service, command)


def create_user_template(
superuser: str, staff: bool, username: str, email: str, password: str
) -> str:
opts = ""
if superuser:
opts += " --superuser"
if staff:
opts += " --staff"
return (
BASE_OPENEDX_COMMAND
+ f"""
./manage.py lms manage_user {opts} {username} {email}
./manage.py lms shell -c "
from django.contrib.auth import get_user_model
u = get_user_model().objects.get(username='{username}')
u.set_password('{password}')
u.save()"
"""
)


def import_demo_course_template() -> str:
return (
BASE_OPENEDX_COMMAND
+ """
# Import demo course
git clone https://github.com/openedx/edx-demo-course --branch {{ OPENEDX_COMMON_VERSION }} --depth 1 ../edx-demo-course
python ./manage.py cms import ../data ../edx-demo-course
# Re-index courses
./manage.py cms reindex_course --all --setup"""
)


def set_theme_template(theme_name: str, domain_names: t.List[str]) -> str:
"""
For each domain, get or create a Site object and assign the selected theme.
"""
# Note that there are no double quotes " in this piece of code
python_command = """
import sys
from django.contrib.sites.models import Site
def assign_theme(name, domain):
print('Assigning theme', name, 'to', domain)
if len(domain) > 50:
sys.stderr.write(
'Assigning a theme to a site with a long (> 50 characters) domain name.'
' The displayed site name will be truncated to 50 characters.\\n'
)
site, _ = Site.objects.get_or_create(domain=domain)
if not site.name:
name_max_length = Site._meta.get_field('name').max_length
site.name = domain[:name_max_length]
site.save()
site.themes.all().delete()
site.themes.create(theme_dir_name=name)
"""
domain_names = domain_names or [
"{{ LMS_HOST }}",
"{{ LMS_HOST }}:8000",
"{{ CMS_HOST }}",
"{{ CMS_HOST }}:8001",
"{{ PREVIEW_LMS_HOST }}",
"{{ PREVIEW_LMS_HOST }}:8000",
]
for domain_name in domain_names:
python_command += f"assign_theme('{theme_name}', '{domain_name}')\n"
return BASE_OPENEDX_COMMAND + f'./manage.py lms shell -c "{python_command}"'


def add_commands(command_group: click.Group) -> None:
for job_command in [createuser, importdemocourse, settheme]:
command_group.add_command(job_command)
Loading

0 comments on commit 109dd34

Please sign in to comment.