-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(frontend): add webserver application for Vue.js frontend
Move legacy web server to `webserver_legacy`.
- Loading branch information
Showing
49 changed files
with
1,066 additions
and
444 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,8 @@ | |
# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
import subprocess | ||
import sys | ||
import zipfile | ||
from pathlib import Path | ||
from typing import Any | ||
|
@@ -22,8 +24,23 @@ def create_example_zip() -> None: | |
zip_file.write(file, file.relative_to(minimal_example), COMPRESS_TYPE) | ||
|
||
|
||
def _run_command(args: list[str], rel_path: str) -> None: | ||
cwd = Path(__file__).parent.absolute() / rel_path | ||
try: | ||
subprocess.run(args, cwd=cwd, check=True, text=True, stdout=sys.stdout, stderr=sys.stderr) # noqa: S603 | ||
except subprocess.CalledProcessError as e: | ||
msg = f"npm build failed with exit code {e.returncode}" | ||
raise RuntimeError(msg) from e | ||
|
||
|
||
def build_frontend() -> None: | ||
_run_command(["npm", "ci", "--ignore-scripts"], "frontend") | ||
_run_command(["npm", "run", "build"], "frontend") | ||
|
||
|
||
def build(_setup_kwargs: Any) -> None: | ||
create_example_zip() | ||
build_frontend() | ||
|
||
|
||
if __name__ == "__main__": | ||
|
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,8 @@ | |
# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
import os | ||
from pathlib import Path | ||
|
||
PACKAGE_CONFIG_FILENAME = "qpy_config.yml" | ||
DEFAULT_STATE_STORAGE_PATH = Path(os.getenv("XDG_STATE_HOME", os.path.expanduser("~/.local/state"))) / "questionpy-sdk" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# This file is part of the QuestionPy SDK. (https://questionpy.org) | ||
# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
from pathlib import Path | ||
from typing import Protocol, Self | ||
|
||
from questionpy_server.worker.runtime.package_location import PackageLocation | ||
|
||
|
||
class WebServerProtocol(Protocol): | ||
async def run_forever(self: Self) -> None: ... | ||
async def stop_server(self: Self) -> None: ... | ||
async def start_server(self: Self) -> None: ... | ||
|
||
|
||
def get_webserver( | ||
package_location: PackageLocation, | ||
state_storage_path: Path, | ||
host: str, | ||
port: int, | ||
*, | ||
legacy_frontend: bool = False, | ||
) -> WebServerProtocol: | ||
if legacy_frontend: | ||
from questionpy_sdk.webserver_legacy.app import WebServer as WebServerLegacy # noqa: PLC0415 | ||
|
||
return WebServerLegacy(package_location, state_storage_path, host, port) | ||
|
||
from questionpy_sdk.webserver.app import WebServer as WebServerSpa # noqa: PLC0415 | ||
|
||
return WebServerSpa(package_location, state_storage_path, host, port) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
import operator | ||
from collections.abc import Iterable | ||
from typing import Any | ||
|
||
|
||
|
@@ -19,7 +20,7 @@ def _unflatten(flat_form_data: dict[str, str]) -> dict[str, Any]: | |
... "general[my_repetition][1][role]": "OPT_1", | ||
... "general[my_repetition][1][name][first_name]": "John", | ||
... }) | ||
{'general': {'my_hidden': 'foo', 'my_repetition': {'1': {'role': 'OPT_1', 'name': {'first_name': 'John'}}}}} | ||
{'general': {'my_hidden': 'foo', 'my_repetition': [{'role': 'OPT_1', 'name': {'first_name': 'John'}}]}} | ||
""" | ||
unflattened_dict: dict[str, Any] = {} | ||
for flat_key, value in flat_form_data.items(): | ||
|
@@ -45,27 +46,33 @@ def _convert_repetition_dict_to_list(dictionary: dict[str, Any]) -> dict[str, An | |
for key, value in dictionary.items(): | ||
dictionary[key] = _convert_repetition_dict_to_list(value) | ||
|
||
if dictionary.pop("qpy_repetition_marker", ...) is not ...: | ||
if all(key.isnumeric() for key in dictionary): | ||
# Sort by key (i.e. the index) and put the sorted values into a list. | ||
return [value for key, value in sorted(dictionary.items(), key=operator.itemgetter(0))] | ||
|
||
return dictionary | ||
|
||
|
||
def parse_form_data(form_data: dict) -> dict: | ||
"""Parses form data from a flat dictionary into a nested dictionary. | ||
def parse_form_data(form_data: dict[str, Any]) -> dict[str, Any]: | ||
"""Parses form data from a flat into a nested dictionary to be consumed by Pydantic. | ||
This function parses a dictionary, where the keys are the references to the Form Element from the Options Form. | ||
This function parses a dictionary, where the keys are the references to the Form Elements from the Options Form. | ||
The references are used to create a nested dictionary with the form data. Elements in the 'general' section are | ||
moved to the root of the dictionary. | ||
Args: | ||
form_data: The flat dictionary representing the form data. | ||
Returns: | ||
The nested form data. | ||
Examples: | ||
>>> parse_form_data({ | ||
... "general[my_hidden]": "foo", | ||
... "general[my_repetition][1][role]": "OPT_1", | ||
... "general[my_repetition][1][name][first_name]": "John", | ||
... }) | ||
{'my_hidden': 'foo', 'my_repetition': {'1': {'role': 'OPT_1', 'name': {'first_name': 'John'}}}} | ||
{'my_hidden': 'foo', 'my_repetition': [{'role': 'OPT_1', 'name': {'first_name': 'John'}}]} | ||
""" | ||
unflattened_form_data = _unflatten(form_data) | ||
options = unflattened_form_data.get("general", {}) | ||
|
@@ -75,15 +82,48 @@ def parse_form_data(form_data: dict) -> dict: | |
return options | ||
|
||
|
||
def get_nested_form_data(form_data: dict[str, Any], reference: str) -> object: | ||
current_element = form_data | ||
parts = reference.replace("]", "").split("[") | ||
def _flatten_value(value: Any, prefix: str, result: dict[str, Any]) -> None: | ||
# group | ||
if isinstance(value, dict): | ||
for k, v in value.items(): | ||
_flatten_value(v, f"{prefix}[{k}]", result) | ||
|
||
# repetition | ||
elif isinstance(value, list) and len(value) > 0 and all(isinstance(item, dict) for item in value): | ||
for idx, v in enumerate(value, start=1): | ||
item_prefix = f"{prefix}[{idx}]" | ||
_flatten_value(v, item_prefix, result) | ||
|
||
else: | ||
result[prefix] = value | ||
|
||
|
||
def flatten_form_data(form_data: dict[str, Any], section_names: Iterable[str]) -> dict[str, Any]: | ||
"""Flattens form data from a nested dictionary into a flat dictionary to be consumed by the frontend. | ||
ref = parts.pop(0) | ||
if ref != "general": | ||
current_element = current_element[ref] | ||
while parts: | ||
ref = parts.pop(0) | ||
current_element = current_element[ref] | ||
This function flattens a nested dictionary into a flat dictionary, where the keys are references | ||
to the Form Elements in the Options Form. Top-level elements are put under the 'general' section, | ||
while elements under the given `section_names` are put under their respective sections. | ||
return current_element | ||
Args: | ||
form_data: The nested dictionary representing the form data. | ||
section_names: An iterable of section names that should be treated as sections and not put | ||
under 'general'. | ||
Returns: | ||
The flat form data. | ||
Examples: | ||
>>> flatten_form_data( | ||
... { | ||
... "my_hidden": "foo", | ||
... "my_repetition": [{"input": "foo"}], | ||
... }, | ||
... section_names=[], | ||
... ) | ||
{'general[my_hidden]': 'foo', 'general[my_repetition][1][input]': 'foo'} | ||
""" | ||
result: dict[str, Any] = {} | ||
for key, value in form_data.items(): | ||
_flatten_value(value, key if key in section_names else f"general[{key}]", result) | ||
return result |
Oops, something went wrong.