diff --git a/tutordeck/server/app.py b/tutordeck/server/app.py index 082db35..4cef204 100644 --- a/tutordeck/server/app.py +++ b/tutordeck/server/app.py @@ -112,8 +112,55 @@ async def before_request() -> None: @app.get("/") -async def home() -> str: - return await render_template("plugin_installed.html") +async def home() -> BaseResponse: + """ + Home redirects to the list of installed plugins + """ + return redirect(url_for("plugin_installed")) + + +@app.get("/configuration") +async def configuration() -> str: + config = tutorclient.Project.get_config() + + # Load base config with essential settings + base_settings = [ + "LMS_HOST", + "CMS_HOST", + "LANGUAGE_CODE", + "ENABLE_HTTPS", + ] + base_config = {key: config.pop(key) for key in base_settings} + + # User-saved configuration + user_config = tutorclient.Project.get_user_config() + + return await render_template( + "configuration.html", + base_config=base_config, + user_config=user_config, + config=dict(sorted(config.items())), + ) + + +@app.post("/configuration") +async def configuration_update() -> BaseResponse: + """ + Update configuration settings. + + TODO display "need to run launch". + """ + await process_config_update_request() + + # Handle non-ajax call + next_url = request.args.get("next", "") + if next_url: + return redirect(next_url) + + # Handle ajax call + response = Response("", status=200, content_type="text/html") + response.headers["HX-Redirect"] = url_for("configuration") + return response @app.get("/plugin/store") @@ -281,23 +328,9 @@ async def plugins_update() -> BaseResponse: return redirect(url_for("plugin_store")) -@app.post("/config//update") -async def config_update(name: str) -> Response: - form = await request.form - - unset = form.get("unset") - if unset: - tutorclient.CliPool.run_sequential(["config", "save", f"--unset={unset}"]) - else: - cmd = ["config", "save"] - for key, value in form.items(): - if value.startswith("{{"): - # Templated values that start with {{ should be explicitely converted to string - # Otherwise there will be a parsing error because it might be considered a dictionary - value = f"'{value}'" - cmd.extend(["--set", f"{key}={value}"]) - tutorclient.CliPool.run_sequential(cmd) - # TODO error management +@app.post("/plugin//config/update") +async def plugin_config_update(name: str) -> Response: + await process_config_update_request() response = t.cast( Response, await make_response( @@ -310,14 +343,31 @@ async def config_update(name: str) -> Response: ) ), ) - response.set_cookie( - f"{constants.WARNING_COOKIE_PREFIX}-{name}", - "requires launch", - max_age=constants.ONE_MONTH, - ) + update_plugins_requiring_launch(response, add=name) return response +async def process_config_update_request() -> None: + """ + Set/Unset config key/values based on request form. + + TODO how to handle configuration changes? For instance: reloading + """ + form = await request.form + if unset := form.get("unset"): + tutorclient.CliPool.run_sequential(["config", "save", f"--unset={unset}"]) + else: + cmd = ["config", "save"] + for key, value in form.items(): + if value.startswith("{{"): + # Templated values that start with {{ should be explicitely converted to string + # Otherwise there will be a parsing error because it might be considered a dictionary + value = f"'{value}'" + cmd.extend(["--set", f"{key}={value}"]) + tutorclient.CliPool.run_sequential(cmd) + # TODO error management + + @app.get("/local/launch") async def local_launch_view() -> str: return await render_template( diff --git a/tutordeck/server/static/img/gear.svg b/tutordeck/server/static/img/gear.svg new file mode 100644 index 0000000..ec3561d --- /dev/null +++ b/tutordeck/server/static/img/gear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tutordeck/server/static/js/config.js b/tutordeck/server/static/js/config.js new file mode 100644 index 0000000..f3b93ea --- /dev/null +++ b/tutordeck/server/static/js/config.js @@ -0,0 +1,27 @@ +// Add change event to all inputs, selects +document.querySelectorAll('#config-forms-container input').forEach((element) => { + // TODO is this working? + element.addEventListener('change', () => { + element.classList.add('changed'); + // Find the associated hidden input, for checkbox changes + const hiddenInput = element.nextElementSibling; + if (hiddenInput && hiddenInput.type === 'hidden') { + hiddenInput.classList.add('changed'); + } + }) +}); + +// Handle form submission +// TODO can we simplify this with document.querySelectorAll('#config-forms-container')? +document.querySelectorAll('form').forEach((form) => { + form.addEventListener('submit', (e) => { + // Disable all inputs that don't have the 'changed' class + // TODO can we simplify this with e.target.querySelectorAll('input:...') + document.querySelectorAll('#config-forms-container input:not(.changed)').forEach((element) => { + // TODO is this check even necessary? if yes, why? + if (element.id != "plugin-name") { + element.disabled = true; + } + }); + }); +}); diff --git a/tutordeck/server/templates/_config.html b/tutordeck/server/templates/_config.html index eed32e6..50737f6 100644 --- a/tutordeck/server/templates/_config.html +++ b/tutordeck/server/templates/_config.html @@ -10,17 +10,31 @@ {% else %} {% endif %} - - + {% else %} + {# Global configuration #} + + {% endif %} + - {% endfor %} + {% endfor %} diff --git a/tutordeck/server/templates/configuration.html b/tutordeck/server/templates/configuration.html new file mode 100644 index 0000000..9f180e1 --- /dev/null +++ b/tutordeck/server/templates/configuration.html @@ -0,0 +1,18 @@ +{% extends "index.html" %} + +{% block workspace_content %} +
+

Global configuration

+
+

Base configuration

+ {% with config=base_config %}{% include "_config.html" %}{% endwith %} + +

Default configuration

+ {% with config=config %}{% include "_config.html" %}{% endwith %} + + +
+
+ + +{% endblock %} diff --git a/tutordeck/server/templates/index.html b/tutordeck/server/templates/index.html index 95069ad..c42b68e 100644 --- a/tutordeck/server/templates/index.html +++ b/tutordeck/server/templates/index.html @@ -26,6 +26,10 @@ + + +

Configuration

+

Plugin Marketplace

diff --git a/tutordeck/server/templates/plugin.html b/tutordeck/server/templates/plugin.html index 1154534..f7d2a64 100644 --- a/tutordeck/server/templates/plugin.html +++ b/tutordeck/server/templates/plugin.html @@ -87,29 +87,6 @@ showPluginEnableDisableBar(); ShowRunCommandButton(); - - // Add change event to all inputs, selects - document.querySelectorAll('#config-forms-container input').forEach((element) => { - element.addEventListener('change', () => { - element.classList.add('changed'); - // Find the associated hidden input - const hiddenInput = element.nextElementSibling; - if (hiddenInput && hiddenInput.type === 'hidden') { - hiddenInput.classList.add('changed'); - } - }) - }); - - // Handle form submission - document.querySelectorAll('form').forEach((form) => { - form.addEventListener('submit', (e) => { - // Disable all inputs that don't have the 'changed' class - document.querySelectorAll('#config-forms-container input:not(.changed)').forEach((element) => { - if (element.id != "plugin-name") { - element.disabled = true; - } - }); - }); - }); -{% endblock %} \ No newline at end of file + +{% endblock %}