Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions plugins/tutor-contrib-paragon/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ classifiers = [

]
dependencies = [
"tutor>=19.0.0,<20.0.0",
"tutor>=19.0.0,<21.0.0",
"tutor-mfe @ git+https://github.com/overhangio/tutor-mfe.git@release",
]

optional-dependencies = { dev = ["tutor[dev]>=19.0.0,<20.0.0", "pytest>=8.3.4", "pytest-order>=1.3.0"] }
optional-dependencies = { dev = ["tutor[dev]>=19.0.0,<21.0.0", "pytest>=8.3.4", "pytest-order>=1.3.0", "requests>=2.32.2"] }

# These fields will be set by hatch_build.py
dynamic = ["version"]
Expand All @@ -45,6 +46,9 @@ Source = "https://github.com/openedx/openedx-tutor-plugins.git#subdirectory=plug
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.metadata]
allow-direct-references = true

# hatch-specific configuration
[tool.hatch.metadata.hooks.custom]
path = ".hatch_build.py"
Expand Down
12 changes: 9 additions & 3 deletions plugins/tutor-contrib-paragon/tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest
import subprocess

from .helpers import PARAGON_NAME, PARAGON_IMAGE
from .helpers import PARAGON_NAME, PARAGON_IMAGE, MFE_SERVICE


@pytest.fixture(scope="package", autouse=True)
Expand All @@ -15,7 +15,7 @@ def setup_tutor_paragon_plugin():
"""

subprocess.run(
["tutor", "plugins", "enable", PARAGON_NAME],
["tutor", "plugins", "enable", MFE_SERVICE, PARAGON_NAME],
check=True,
capture_output=True,
)
Expand All @@ -26,10 +26,16 @@ def setup_tutor_paragon_plugin():
capture_output=True,
)

subprocess.run(
["tutor", "config", "save", "--set", "LMS_HOST=local.openedx.io"],
check=True,
capture_output=True,
)

yield

subprocess.run(
["tutor", "plugins", "disable", PARAGON_NAME],
["tutor", "plugins", "disable", PARAGON_NAME, MFE_SERVICE],
check=True,
capture_output=True,
)
12 changes: 12 additions & 0 deletions plugins/tutor-contrib-paragon/tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

logger = logging.getLogger(__name__)

MFE_SERVICE = "mfe"
PARAGON_NAME = "paragon"
PARAGON_IMAGE = "paragon-builder"
PARAGON_JOB = "paragon-build-tokens"
Expand Down Expand Up @@ -52,3 +53,14 @@ def get_tutor_root_path():
raise RuntimeError("Failed to get Tutor root path: " + result.stderr)

return result.stdout.strip()


def get_config_value(key: str) -> str:
"""Get a configuration value from Tutor.

Returns:
str: The value of the configuration key.
"""
result = execute_tutor_command(["config", "printvalue", key])
assert result.returncode == 0, f"Error getting {key}: {result.stderr}"
return result.stdout.strip()
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
import shutil
import pytest
import re
import requests
import time

from .helpers import (
execute_tutor_command,
get_config_value,
get_tutor_root_path,
PARAGON_JOB,
PARAGON_COMPILED_THEMES_FOLDER,
Expand Down Expand Up @@ -145,3 +148,68 @@ def test_build_tokens_with_source_tokens_only():
assert not os.path.exists(
utility_classes_css
), f"{utility_classes_css} should not exist when --source-tokens-only is used."


@pytest.mark.order(6)
def test_build_tokens_generates_minified_bundle():
"""
Ensure that the build-tokens job generates the minified bundle files for hosting.
"""
theme = "light"
result = execute_tutor_command(["local", "do", PARAGON_JOB, "--themes", theme])
assert result.returncode == 0, f"Error running build-tokens job: {result.stderr}"

tutor_root = get_tutor_root_path()
compiled_path = os.path.join(tutor_root, PARAGON_COMPILED_THEMES_FOLDER)

minified_theme_bundle = os.path.join(
compiled_path, "themes", theme, f"{theme}.min.css"
)
minified_core_bundle = os.path.join(compiled_path, "core", "core.min.css")

assert os.path.exists(
minified_core_bundle
), f"Minified core bundle file {minified_core_bundle} does not exist."
assert os.path.exists(
minified_theme_bundle
), f"Minified theme bundle file {minified_theme_bundle} does not exist."


@pytest.mark.order(7)
def test_build_tokens_hosted_files():
"""
Verify that the compiled themes can be served through the tutor-mfe service.

This test builds tokens, starts the required services, and checks that the
static files are accessible via HTTP requests.
"""
result = execute_tutor_command(["local", "do", PARAGON_JOB])
assert result.returncode == 0, f"Error running build-tokens job: {result.stderr}"

static_url_prefix = get_config_value("PARAGON_STATIC_URL_PREFIX").lstrip("/")
mfe_host = "apps.local.openedx.io"

services_result = execute_tutor_command(["local", "start", "-d", "caddy", "mfe"])
assert services_result.returncode == 0, "Error starting hosting services"

time.sleep(10)

try:
base_url = f"http://{mfe_host}/{static_url_prefix}"
test_files = ["core/core.min.css", "themes/light/light.min.css"]

for test_file in test_files:
url = f"{base_url}{test_file}"
response = requests.get(url, timeout=2)

assert (
response.status_code == 200
), f"Expected status 200 for {url}, but got {response.status_code}. "

content_type = response.headers.get("Content-Type", "")
assert "text/css" in content_type.lower(), (
f"Expected 'text/css' Content-Type for {url}, but got '{content_type}'."
)

finally:
execute_tutor_command(["local", "stop", "caddy", "mfe"])
13 changes: 13 additions & 0 deletions plugins/tutor-contrib-paragon/tutorparagon/patches/mfe-caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% if MFE_HOST_EXTRA_FILES %}
# Paragon static files hosting
handle_path /{{ PARAGON_STATIC_URL_PREFIX }}* {
@mincss {
# Match only minified CSS files
path_regexp mincss \.min\.css$
}
handle @mincss {
root * /paragon-statics
file_server
}
}
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% if MFE_HOST_EXTRA_FILES %}
- ../../{{ PARAGON_COMPILED_THEMES_PATH }}:/paragon-statics
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% if MFE_HOST_EXTRA_FILES %}
{% set PROTOCOL = 'https' if ENABLE_HTTPS else 'http' %}
{% set PARAGON_BASE_URL = PROTOCOL ~ "://" ~ "localhost" ~ ":" ~ 8002 ~ "/" ~ PARAGON_STATIC_URL_PREFIX%}
{% include "paragon/settings/mfe-common-settings" %}
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% if MFE_HOST_EXTRA_FILES %}
{% set PROTOCOL = 'https' if ENABLE_HTTPS else 'http' %}
{% set PARAGON_BASE_URL = PROTOCOL ~ "://" ~ MFE_HOST ~ "/" ~ PARAGON_STATIC_URL_PREFIX %}
{% include "paragon/settings/mfe-common-settings" %}
{% endif %}
4 changes: 2 additions & 2 deletions plugins/tutor-contrib-paragon/tutorparagon/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@
# List of enabled themes to compile and serve
# Only themes listed here will be processed, even if others exist in sources
("PARAGON_ENABLED_THEMES", []),
# Whether Tutor should expose the compiled themes to be served (e.g. via nginx, cady or static server)
("PARAGON_SERVE_COMPILED_THEMES", True),
# Paragon Builder Docker image
# This image is used to compile themes and should be built with `tutor images build paragon-builder`
("PARAGON_BUILDER_IMAGE", "paragon-builder:latest"),
("PARAGON_STATIC_URL_PREFIX", "static/paragon/"),
("MFE_HOST_EXTRA_FILES", True), # Enable MFE host extra files
]
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,33 @@ parse_args() {
printf '%s\n' "$@"
}

build_css_bundle() {
# This function builds a CSS bundle using PostCSS.
# It takes the path to the index.css file as an argument.
# Usage: build_css_bundle <index_css_file>

local index_css_file="$1"

local bundle_directory=$(dirname "$index_css_file")
local bundle_name=$(basename "$bundle_directory")
local minified_output_file="$bundle_directory/${bundle_name}.min.css"

if npx postcss "$index_css_file" \
--use postcss-import \
--use postcss-custom-media \
--use postcss-combine-duplicated-selectors \
--use postcss-minify \
--no-map \
--output "$minified_output_file"; then

echo "Successfully created bundle: $bundle_name"
return 0
else
echo "Failed to build CSS bundle: $bundle_name" >&2
return 1
fi
}

set -- $(parse_args "$@")

# Executes the Paragon CLI to build themes.
Expand All @@ -50,8 +77,14 @@ npx paragon build-tokens \
--build-dir "$TMP_BUILD_DIR" \
"$@"

find "$TMP_BUILD_DIR" -type f -name 'index.css' | while read -r index; do
if [ -f "$index" ]; then
build_css_bundle "$index"
fi
done

# Moves the built themes to the final volume directory.
mkdir -p "$FINAL_BUILD_DIR"
rm -rf "$FINAL_BUILD_DIR"/*
cp -a "$TMP_BUILD_DIR/." "$FINAL_BUILD_DIR/"
chmod -R a+rw "$FINAL_BUILD_DIR"

Expand Down
Loading