Skip to content
Open
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
51 changes: 48 additions & 3 deletions src/fourc_webviewer/fourc_webserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@

import numpy as np
import pyvista as pv
import yaml
from fourcipp import CONFIG
from fourcipp.fourc_input import FourCInput
from fourcipp.fourc_input import FourCInput, ValidationError
from trame.app import get_server
from trame.decorators import TrameApp, change, controller

Expand All @@ -27,7 +28,14 @@
read_fourc_yaml_file,
write_fourc_yaml_file,
)
from fourc_webviewer.python_utils import convert_string2number, find_value_recursively
from fourc_webviewer.python_utils import (
convert_string2number,
dict_leaves_to_number_if_schema,
dict_number_leaves_to_string,
find_value_recursively,
parse_validation_error_text,
smart_string2number_cast,
)
from fourc_webviewer.read_geometry_from_file import (
FourCGeometry,
)
Expand Down Expand Up @@ -73,6 +81,9 @@ def __init__(
# create temporary directory
self._server_vars["temp_dir_object"] = tempfile.TemporaryDirectory()

# Register on_field_blur function, which is called when the user leaves a field
self.server.controller.on_leave_edit_field = self.on_leave_edit_field

# initialize state variables for the different modes and
# statuses of the client (e.g. view mode versus edit mode,
# read-in and export status, ...)
Expand Down Expand Up @@ -158,6 +169,9 @@ def init_state_and_server_vars(self):
Path(self._server_vars["temp_dir_object"].name)
/ f"new_{self.state.fourc_yaml_file['name']}"
)
# dict to store input errors for the input validation
# imitates structure of self.state.general_sections
self.state.input_error_dict = {}

# get state variables of the general sections
self.init_general_sections_state_and_server_vars()
Expand Down Expand Up @@ -923,7 +937,6 @@ def change_fourc_yaml_file(self, fourc_yaml_file, **kwargs):
self._server_vars["fourc_yaml_last_modified"],
self._server_vars["fourc_yaml_read_in_status"],
) = read_fourc_yaml_file(temp_fourc_yaml_file)

self._server_vars["fourc_yaml_name"] = Path(temp_fourc_yaml_file).name

# set vtu file path empty to make the convert button visible
Expand Down Expand Up @@ -951,6 +964,11 @@ def change_selected_main_section_name(self, selected_main_section_name, **kwargs
selected_main_section_name
]["subsections"][0]

@change("selected_section_name")
def change_selected_section_name(self, selected_section_name, **kwargs):
"""Reaction to change of state.selected_section_name."""
self.state.selected_subsection_name = selected_section_name.split("/")[-1]

@change("selected_material")
def change_selected_material(self, selected_material, **kwargs):
"""Reaction to change of state.selected_material."""
Expand Down Expand Up @@ -1175,6 +1193,33 @@ def click_save_button(self, **kwargs):
else:
self.state.export_status = self.state.all_export_statuses["error"]

@change("general_sections")
def on_sections_change(self, general_sections, **kwargs):
"""Reaction to change of state.general_sections."""

self.sync_server_vars_from_state()
try:
fourcinput = FourCInput(self._server_vars["fourc_yaml_content"])

dict_leaves_to_number_if_schema(fourcinput._sections)

fourcinput.validate()
self.state.input_error_dict = {}
except ValidationError as exc:
self.state.input_error_dict = parse_validation_error_text(
str(exc.args[0])
) # exc.args[0] is the error message
return False

def on_leave_edit_field(self):
"""Reaction to user leaving the field.

Currently only supported for the general sections.
"""
# also gets called when a new file is loaded
# basically just sets the state based on server_vars
self.init_general_sections_state_and_server_vars()

""" --- Other helper functions"""

def convert_string2num_all_sections(self):
Expand Down
66 changes: 62 additions & 4 deletions src/fourc_webviewer/gui_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,9 @@ def _functions_panel(server):
)


def _prop_value_table():
def _prop_value_table(server):
"""Table (property - value) layout (for general sections)."""

with vuetify.VTable(
v_if=(
"section_names[selected_main_section_name]['content_mode'] == all_content_modes['general_section']",
Expand Down Expand Up @@ -626,14 +627,71 @@ def _prop_value_table():
v_if="edit_mode == all_edit_modes['edit_mode']",
classes="text-center w-50",
):
item_error = "input_error_dict[selected_main_section_name]?.[item_key] || input_error_dict[selected_main_section_name + '~1' + selected_subsection_name]?.[item_key]"
# if item is a string, number or integer -> use VTextField
vuetify.VTextField(
v_model=(
"general_sections[selected_main_section_name][selected_section_name][item_key]", # binding item_val directly does not work, since Object.entries(...) creates copies for the mutable objects
),
v_if=(
"(json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] == 'string' "
"|| json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] == 'number' "
"|| json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] == 'integer')"
"&& !json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['enum']"
),
blur=server.controller.on_leave_edit_field,
update_modelValue="flushState('general_sections')", # this is required in order to flush the state changes correctly to the server, as our passed on v-model is a nested variable
classes="w-80",
classes="w-80 pb-1",
dense=True,
hide_details=True,
color=f"{item_error} && error",
bg_color=(f"{item_error} ? 'rgba(255, 0, 0, 0.2)' : ''",),
error_messages=(
f"{item_error}?.length > 100 ? {item_error}?.slice(0, 97)+' ...' : {item_error}",
),
)
# if item is a boolean -> use VSwitch
with html.Div(
v_if=(
"json_schema['properties']?.[selected_section_name]?.['properties']?.[item_key]?.['type'] === 'boolean'"
),
classes="d-flex align-center justify-center",
):
vuetify.VSwitch(
v_model=(
"general_sections[selected_main_section_name][selected_section_name][item_key]"
),
classes="mt-4",
update_modelValue="flushState('general_sections')",
class_="mx-100",
dense=True,
color="primary",
)
# if item is an enum -> use VAutocomplete
(
vuetify.VAutocomplete(
v_model=(
"general_sections[selected_main_section_name]"
"[selected_section_name][item_key]"
),
v_if=(
"json_schema['properties']?.[selected_section_name]"
"?.['properties']?.[item_key]?.['enum']"
),
update_modelValue="flushState('general_sections')",
# bind the enum array as items
items=(
"json_schema['properties'][selected_section_name]['properties'][item_key]['enum']",
),
dense=True,
solo=True,
filterable=True,
classes="w-80 pb-1",
color=f"{item_error} && error",
bg_color=(f"{item_error} ? 'rgba(255, 0, 0, 0.2)' : ''",),
error_messages=(
f"{item_error}?.length > 100 ? {item_error}?.slice(0, 97)+' ...' : {item_error}",
),
),
)


Expand Down Expand Up @@ -1259,7 +1317,7 @@ def create_gui(server, render_window):

# Further elements with conditional rendering (see above)
_sections_dropdown()
_prop_value_table()
_prop_value_table(server)
_materials_panel()
_functions_panel(server)
_design_conditions_panel()
Expand Down
85 changes: 84 additions & 1 deletion src/fourc_webviewer/python_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""Module for python utils."""

import re

from fourcipp import CONFIG


def flatten_list(input_list):
"""Flattens a given (multi-level) list into a single list.
Expand Down Expand Up @@ -48,6 +52,82 @@ def find_value_recursively(input_dict, target_key):
return None


def get_by_path(dct, path):
"""Retrieve the value at the nested path from dct.

Raises KeyError if any key is missing.
"""
current = dct
for key in path:
current = current[key]
return current


def dict_leaves_to_number_if_schema(value, schema_path=[]):
"""Convert all leaves of a dict to numbers if possible."""
if isinstance(value, dict):
for k, v in value.items():
value[k] = dict_leaves_to_number_if_schema(
v, schema_path + ["properties", k]
)
return value
if isinstance(value, str) and get_by_path(
CONFIG.fourc_json_schema, schema_path + ["type"]
) in ["number", "integer"]:
return smart_string2number_cast(value)
return value


def dict_number_leaves_to_string(value):
"""Convert all leaves of a dict to numbers if possible."""
if isinstance(value, bool):
return value # isinstance(True, int) is True
if isinstance(value, dict):
for k, v in value.items():
value[k] = dict_number_leaves_to_string(v)
return value
if isinstance(value, int) or isinstance(value, float):
return str(value)
return value


def parse_validation_error_text(text):
"""Parse a ValidationError message string (with multiple "- Parameter in
[...]" blocks) into a nested dict.

Args:
text (str): <fill in your definition>
Returns:
dict: <fill in your definition>
"""
error_dict = {}
# Match "- Parameter in [...]" blocks up until the next one or end of string
block_re = re.compile(
r"- Parameter in (?P<path>(?:\[[^\]]+\])+)\n"
r"(?P<body>.*?)(?=(?:- Parameter in )|\Z)",
re.DOTALL,
)
for m in block_re.finditer(text):
path_str = m.group("path")
body = m.group("body")

# extract the Error: line
err_m = re.search(r"Error:\s*(.+)", body)
if not err_m:
continue
err_msg = err_m.group(1).strip()

keys = re.findall(r'\["([^"]+)"\]', path_str)

# walk/create nested dicts, then assign the message at the leaf
cur = error_dict
for key in keys[:-1]:
cur = cur.setdefault(key, {})
cur[keys[-1]] = err_msg

return error_dict


def smart_string2number_cast(input_string):
"""Casts an input_string to float / int if possible. Helpful when dealing
with automatic to-string conversions from vuetify.VTextField input
Expand All @@ -56,8 +136,11 @@ def smart_string2number_cast(input_string):
Args:
input_string (str): input string to be converted.
Returns:
int | float | str: converted value.
int | float | str | object: converted value.
"""
# otherwise boolean values are converted to 0/1
if not isinstance(input_string, str):
return input_string
try:
# first convert to float
input_float = float(input_string)
Expand Down
Loading