From 871db10edfb18e0de8879e1dcfc1a3ce5e1eb100 Mon Sep 17 00:00:00 2001 From: Lasse Date: Mon, 10 Nov 2025 22:15:06 +0100 Subject: [PATCH 1/3] Add include files pop up --- src/fourc_webviewer/fourc_webserver.py | 236 ++++++++++++++++++++----- src/fourc_webviewer/gui_utils.py | 63 ++++++- 2 files changed, 254 insertions(+), 45 deletions(-) diff --git a/src/fourc_webviewer/fourc_webserver.py b/src/fourc_webviewer/fourc_webserver.py index dca8cc0..e759d21 100644 --- a/src/fourc_webviewer/fourc_webserver.py +++ b/src/fourc_webviewer/fourc_webserver.py @@ -68,6 +68,10 @@ def __init__( self.server = get_server() + # initialize include upload value: False (bottom sheet with include upload is not displayed until there is a fourcyaml file uploaded) + self.state.include_upload_open = False + self.state.included_files = [] + # declare server-side variable dict: variables which should not # be exposed to the client-side self._server_vars = {} @@ -120,18 +124,11 @@ def __init__( # initialize state object self.init_state_and_server_vars() - # convert file to vtu and create dedicated render objects - fourc_geometry = FourCGeometry( - fourc_yaml_file=fourc_yaml_file, - temp_dir=Path(self._server_vars["temp_dir_object"].name), - ) - self.state.vtu_path = fourc_geometry.vtu_file_path + if "render_window" not in self._actors: + self._server_vars["render_window"] = pv.Plotter() + self.state.vtu_path = "" - if self.state.vtu_path == "": - self.state.read_in_status = self.state.all_read_in_statuses[ - "vtu_conversion_error" - ] - self.init_pyvista_render_objects() + self._server_vars["fourc_yaml_file_dir"] = Path(fourc_yaml_file).parent # create ui create_gui(self.server, self._server_vars["render_window"]) @@ -267,11 +264,57 @@ def init_pyvista_render_objects(self): The saved vtu file path is hereby utilized. """ - if "render_window" not in self._actors: - self._server_vars["render_window"] = pv.Plotter() + # convert file to vtu and create dedicated render objects + if not Path( + self._server_vars["temp_dir_object"].name + + "\\" + + self._server_vars["fourc_yaml_name"] + ).exists(): + raise Exception( + "File does not exist: " + + self._server_vars["temp_dir_object"].name + + "\\" + + self._server_vars["fourc_yaml_name"] + ) + geometry_file_name = ( + self._server_vars["fourc_yaml_content"] + .sections.get("STRUCTURE GEOMETRY", {}) + .get("FILE") + ) + if geometry_file_name: + if not Path( + self._server_vars["temp_dir_object"].name + + "\\" + + Path( + self._server_vars["fourc_yaml_content"] + .sections.get("STRUCTURE GEOMETRY", {}) + .get("FILE") + ).name + ).exists(): + raise Exception( + "File does not exist: " + + self._server_vars["temp_dir_object"].name + + "\\" + + Path( + self._server_vars["fourc_yaml_content"]["STRUCTURE GEOMETRY"][ + "FILE" + ] + ).name + ) + fourc_geometry = FourCGeometry( + fourc_yaml_file=self._server_vars["temp_dir_object"].name + + "\\" + + self._server_vars["fourc_yaml_name"], + temp_dir=Path(self._server_vars["temp_dir_object"].name), + ) + self.state.vtu_path = fourc_geometry.vtu_file_path - self._server_vars["render_window"].clear_actors() + if self.state.vtu_path == "": + self.state.read_in_status = self.state.all_read_in_statuses[ + "vtu_conversion_error" + ] + self._server_vars["render_window"].clear_actors() problem_mesh = pv.read(self.state.vtu_path) # get problem mesh self._actors["problem_mesh"] = self._server_vars["render_window"].add_mesh( @@ -334,6 +377,12 @@ def init_pyvista_render_objects(self): all_result_descriptions = self.state.result_description_section.keys() for dc in all_result_descriptions: + if ( + not self.state.result_description_section[dc] + .get("PARAMETERS", {}) + .get("NODE") + ): + continue node_coords = problem_mesh.points[ self.state.result_description_section[dc]["PARAMETERS"]["NODE"] - 1, :, @@ -351,6 +400,8 @@ def init_pyvista_render_objects(self): ) self.update_pyvista_render_objects() + self._server_vars["render_window"].reset_camera() + def update_pyvista_render_objects(self): """Update/ initialize pyvista view objects (reader, thresholds, global COS, ...) for the rendered window. @@ -359,7 +410,7 @@ def update_pyvista_render_objects(self): """ legend_items = [] - for dc in self._actors["result_description_nodes"].values(): + for dc in self._actors.get("result_description_nodes", {}).values(): dc.SetVisibility(False) if ( self.state.selected_main_section_name == "RESULT DESCRIPTION" @@ -372,7 +423,7 @@ def update_pyvista_render_objects(self): ].SetVisibility(True) legend_items.append(("Selected result description", "deepskyblue")) - for rd in self._actors["dc_geometry_entities"].values(): + for rd in self._actors.get("dc_geometry_entities", {}).values(): rd.SetVisibility(False) if ( self.state.selected_main_section_name == "DESIGN CONDITIONS" @@ -384,7 +435,7 @@ def update_pyvista_render_objects(self): ].SetVisibility(True) legend_items.append(("Selected design condition", "navy")) - for mat in self._actors["material_meshes"].values(): + for mat in self._actors.get("material_meshes", {}).values(): mat.SetVisibility(False) if ( self.state.selected_material @@ -743,7 +794,9 @@ def init_result_description_state_and_server_vars(self): # get result description section result_description_section = copy.deepcopy( - self._server_vars["fourc_yaml_content"]["RESULT DESCRIPTION"] + self._server_vars["fourc_yaml_content"].sections.get( + "RESULT DESCRIPTION", {} + ) ) # initialize empty dict as the result description section @@ -913,6 +966,10 @@ def init_funct_state_and_server_vars(self): all_contained_var_names = get_variable_names_in_funct_expression( item_data["SYMBOLIC_FUNCTION_OF_SPACE_TIME"] ) + if "e" in all_contained_var_names: + all_contained_var_names.remove("e") + if "E" in all_contained_var_names: + all_contained_var_names.remove("E") # loop through contained variables and see whether they are evaluable for contained_var_name in all_contained_var_names: @@ -954,6 +1011,46 @@ def init_funct_state_and_server_vars(self): 6 # precision for the user input of the values defined above: x, y, z and t_max ) + def request_included_files(self): + """Requests the included files from the user by opening a the include + files dialog and setting up the state variable accordingly.""" + included_files = [] + + exo_file_name = Path( + self._server_vars.get("fourc_yaml_content") + .sections.get("STRUCTURE GEOMETRY", {}) + .get("FILE") + or "" + ).name + if exo_file_name: + exo_file_server = Path( + self._server_vars["fourc_yaml_file_dir"], + exo_file_name, + ) + exo_temp_path = Path( + self._server_vars["temp_dir_object"].name, + exo_file_name, + ) + if exo_file_server.is_file(): + with open(exo_file_server, "rb") as fr: + with open(exo_temp_path, "wb") as fw: + fw.write(fr.read()) + elif not exo_temp_path.is_file(): + included_files.append( + { + "name": exo_file_name, + "uploaded": False, + "error": None, + "content": None, + } + ) + + self.state.included_files = included_files + if self.state.included_files: + self.state.include_upload_open = True + else: + self.confirm_included_files() + def sync_funct_section_from_state(self): """Syncs the server-side functions section based on the current values of the dedicated state variables.""" @@ -1025,12 +1122,22 @@ def init_mode_state_vars(self): def change_fourc_yaml_file(self, fourc_yaml_file, **kwargs): """Reaction to change of state.fourc_yaml_file.""" + if not fourc_yaml_file or fourc_yaml_file["name"].split(".")[-1] not in [ + "yaml", + "yml", + "DAT", + "dat", + ]: + print( + "Warning: File does not have a .yml / .yaml / .dat / .DAT ending or is empty. Try opening another file." + ) + return # create temporary fourc yaml file from the content of the given file temp_fourc_yaml_file = Path( self._server_vars["temp_dir_object"].name, fourc_yaml_file["name"] ) - with open(temp_fourc_yaml_file, "w") as f: - f.writelines(fourc_yaml_file["content"].decode("utf-8")) + with open(temp_fourc_yaml_file, "wb") as f: + f.write(fourc_yaml_file["content"]) # read content, lines and other details of the given file ( @@ -1042,12 +1149,63 @@ def change_fourc_yaml_file(self, fourc_yaml_file, **kwargs): ) = 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 - # (only if the function was not run yet, i.e., after the - # initial rendering) - self._server_vars["render_count"]["change_fourc_yaml_file"] += 1 - if self._server_vars["render_count"]["change_fourc_yaml_file"] > 1: - self.state.vtu_path = "" + if self._server_vars["fourc_yaml_read_in_status"]: + self.state.read_in_status = self.state.all_read_in_statuses["success"] + else: + self.state.read_in_status = self.state.all_read_in_statuses[ + "validation_error" + ] + + self._server_vars["fourc_yaml_name"] = temp_fourc_yaml_file.name + + self.request_included_files() + + @controller.set("on_upload_include_file") + def on_upload_include_file(self, uploaded_file, index, **kwargs): + """Gets called when an included file is uploaded. + + Saves the uploaded file into the state variable. + """ + self.state.included_files[index]["content"] = uploaded_file + + try: + if self.state.included_files[index]["name"] != uploaded_file["name"]: + self.state.included_files[index]["error"] = ( + "File name mismatch. Expected: " + + self.state.included_files[index]["name"] + ) + elif self.state.included_files[index]["content"]["size"] == 0: + self.state.included_files[index]["error"] = "File is empty." + else: + self.state.included_files[index]["error"] = None + self.state.included_files[index]["uploaded"] = True + except Exception: + self.state.included_files[index]["error"] = "Please upload a file." + self.state.included_files[index]["uploaded"] = False + self.state.dirty("included_files") + self.state.flush() + + @controller.set("confirm_included_files") + def confirm_included_files(self, **kwargs): + """Gets called when the Accept button in the included files dialog is + pressed. + + Saves all files into the temporary directory. + """ + self.state.include_upload_open = False + + for included_file in self.state.included_files: + # create file in temp directory + included_file_path = Path( + self._server_vars["temp_dir_object"].name, + included_file["content"]["name"], + ) + with open(included_file_path, "wb") as f: + f.write(included_file["content"]["content"]) + + self.init_state_and_server_vars() + + self.init_pyvista_render_objects() @change("export_fourc_yaml_path") def change_export_fourc_yaml_path(self, export_fourc_yaml_path, **kwargs): @@ -1082,7 +1240,6 @@ def change_selected_material(self, selected_material, **kwargs): # material (if we are not in an initial rendering scenario) if self._server_vars["render_count"]["change_selected_material"] > 0: # first get the master material id - master_mat_id = self.determine_master_mat_ind_for_current_selection() # update plotter / render objects self.update_pyvista_render_objects() @@ -1158,7 +1315,7 @@ def change_selected_funct(self, selected_funct, **kwargs): # set the selected funct item to the first within the newly # selected funct self.state.selected_funct_item = next( - iter(self.state.funct_section[selected_funct]) + iter(self.state.funct_section.get(selected_funct, {})) ) # update plotly figure @@ -1183,18 +1340,22 @@ def change_selected_funct_item(self, selected_funct_item, **kwargs): def change_funct_plot(self, funct_plot, **kwargs): """Reaction to change of state.funct_plot.""" # update plotly figure - if self.state.funct_section[self.state.selected_funct][ - self.state.selected_funct_item - ]["VISUALIZATION"]: + if ( + self.state.funct_section.get(self.state.selected_funct, {}) + .get(self.state.selected_funct_item, {}) + .get("VISUALIZATION") + ): self.server.controller.figure_update(function_plot_figure(self.state)) @change("funct_section") def change_funct_section(self, funct_section, **kwargs): """Reaction to change of state.funct_section.""" # update plotly figure - if self.state.funct_section[self.state.selected_funct][ - self.state.selected_funct_item - ]["VISUALIZATION"]: + if ( + self.state.funct_section.get(self.state.selected_funct, {}) + .get(self.state.selected_funct_item, {}) + .get("VISUALIZATION") + ): self.server.controller.figure_update(function_plot_figure(self.state)) ################################################# @@ -1281,12 +1442,7 @@ def click_convert_button(self, **kwargs): # initialize state object self.init_state_and_server_vars() - # convert to vtu - fourc_geometry = FourCGeometry( - fourc_yaml_file=temp_fourc_yaml_file, - temp_dir=Path(self._server_vars["temp_dir_object"].name), - ) - self.state.vtu_path = fourc_geometry.vtu_file_path + self.init_pyvista_render_objects() # catch eventual conversion error if self.state.vtu_path == "": diff --git a/src/fourc_webviewer/gui_utils.py b/src/fourc_webviewer/gui_utils.py index 41834bd..4b86a45 100644 --- a/src/fourc_webviewer/gui_utils.py +++ b/src/fourc_webviewer/gui_utils.py @@ -143,7 +143,7 @@ def _toolbar(server_controller): vuetify.VFileInput( label="Input file", v_model=("fourc_yaml_file",), - update_modelValue="flushState('fourc_yaml_file')", + # update_modelValue="flushState('fourc_yaml_file')", accept=".yaml,.yml", ) vuetify.VBtn( @@ -242,6 +242,58 @@ def _bottom_sheet_export(server_controller): ) +def _bottom_sheet_include_upload(server): + """Bottom sheet layout (EXPORT mode).""" + + with vuetify.VDialog( + v_model=("include_upload_open",), persistent=True, max_width="600px" + ): + with vuetify.VCard(classes="pa-5"): + vuetify.VCardTitle("Upload Included Files") + + with vuetify.VCardText(): + with vuetify.VRow( + dense=True, + align="center", + v_for="(file, i) in included_files", + key=("included_files[i].name",), + ): + with vuetify.VCol(cols=11): + vuetify.VFileInput( + update_modelValue=( + server.controller.on_upload_include_file, + "[$event, i]", + ), + label=("file.name",), + multiple=False, + variant="outlined", + color=( + "file.error ? 'error' : file.uploaded ? 'success' : undefined", + ), + error_messages=("file.error",), + ) + with vuetify.VCol(cols=1): + vuetify.VIcon( + icon=( + "file.error || !file.uploaded ? 'mdi-alert-circle' : 'mdi-check-circle'", + ), + color=( + "file.error ? 'error' : file.uploaded ? 'success' : 'primary'", + ), + classes="mr-2 pb-5 pl-3", + size="36", + ) + with vuetify.VCardActions(classes="justify-end"): + vuetify.VBtn( + "Accept", + size="large", + color="primary", + disabled=("!included_files.every(f => !f.error && f.uploaded)",), + click=(server.controller.confirm_included_files,), + variant="text", + ) + + def _sections_dropdown(): """Section dropdown layout.""" vuetify.VSelect( @@ -754,8 +806,7 @@ def _prop_value_table(server): "|| json_schema['properties']?.[selected_section_name]?.['properties']?.[add_key]?.['type'] == 'integer')" "&& !json_schema['properties']?.[selected_section_name]?.['properties']?.[add_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 + update_modelValue="flushState('add_value')", # 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 pb-1", dense=True, # If we will add errors for this later @@ -775,7 +826,7 @@ def _prop_value_table(server): vuetify.VSwitch( v_model=("add_value"), classes="mt-4", - update_modelValue="flushState('general_sections')", + update_modelValue="flushState('add_value')", class_="mx-100", dense=True, color="primary", @@ -787,7 +838,7 @@ def _prop_value_table(server): "json_schema['properties']?.[selected_section_name]" "?.['properties']?.[add_key]?.['enum']" ), - update_modelValue="flushState('general_sections')", + update_modelValue="flushState('add_value')", # bind the enum array as items items=( "json_schema['properties'][selected_section_name]['properties'][add_key]['enum']", @@ -1411,6 +1462,8 @@ def create_gui(server, render_window): _bottom_sheet_info() _bottom_sheet_export(server.controller) + _bottom_sheet_include_upload(server) + with layout.drawer as drawer: drawer.width = 800 with html.Div(v_if=("vtu_path != ''",)): From 918ac2e3e45c10c3b777c29ade820dcc6058ba32 Mon Sep 17 00:00:00 2001 From: Lasse Date: Tue, 11 Nov 2025 11:21:31 +0100 Subject: [PATCH 2/3] Add yaml includes recursive --- src/fourc_webviewer/fourc_webserver.py | 69 ++++++++++++++++++-------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/src/fourc_webviewer/fourc_webserver.py b/src/fourc_webviewer/fourc_webserver.py index e759d21..fbe328d 100644 --- a/src/fourc_webviewer/fourc_webserver.py +++ b/src/fourc_webviewer/fourc_webserver.py @@ -12,6 +12,7 @@ import yaml from fourcipp import CONFIG from fourcipp.fourc_input import FourCInput, ValidationError +from fourcipp.utils.yaml_io import load_yaml from trame.app import get_server from trame.decorators import TrameApp, change, controller @@ -1011,41 +1012,58 @@ def init_funct_state_and_server_vars(self): 6 # precision for the user input of the values defined above: x, y, z and t_max ) - def request_included_files(self): - """Requests the included files from the user by opening a the include - files dialog and setting up the state variable accordingly.""" - included_files = [] + def append_include_files(self, file_paths): + """Appends list of files to the included files input field. - exo_file_name = Path( - self._server_vars.get("fourc_yaml_content") - .sections.get("STRUCTURE GEOMETRY", {}) - .get("FILE") - or "" - ).name - if exo_file_name: - exo_file_server = Path( + They will be uploaded before the user can edit or view the file. + """ + yaml_include_names = [Path(file_path).name for file_path in file_paths] + included_files = copy.deepcopy(self.state.included_files) + for include_name in yaml_include_names: + include_file_server = Path( self._server_vars["fourc_yaml_file_dir"], - exo_file_name, + include_name, ) - exo_temp_path = Path( + include_temp_path = Path( self._server_vars["temp_dir_object"].name, - exo_file_name, + include_name, ) - if exo_file_server.is_file(): - with open(exo_file_server, "rb") as fr: - with open(exo_temp_path, "wb") as fw: + if include_file_server.is_file(): + with open(include_file_server, "rb") as fr: + with open(include_temp_path, "wb") as fw: fw.write(fr.read()) - elif not exo_temp_path.is_file(): + elif not include_temp_path.is_file(): included_files.append( { - "name": exo_file_name, + "name": include_name, "uploaded": False, "error": None, "content": None, } ) - self.state.included_files = included_files + + def request_included_files(self): + """Requests the included files from the user by opening a the include + files dialog and setting up the state variable accordingly.""" + + self.append_include_files( + [ + self._server_vars.get("fourc_yaml_content") + .sections.get("STRUCTURE GEOMETRY", {}) + .get("FILE") + or "" + ] + ) + # add yaml includes + yaml_include_names = [ + Path(file_path).name + for file_path in self._server_vars.get("fourc_yaml_content").sections.get( + "INCLUDES", [] + ) + ] + self.append_include_files(yaml_include_names) + if self.state.included_files: self.state.include_upload_open = True else: @@ -1168,6 +1186,15 @@ def on_upload_include_file(self, uploaded_file, index, **kwargs): """ self.state.included_files[index]["content"] = uploaded_file + if uploaded_file["name"].split(".")[-1] in ["yaml", "yml"]: + content = ( + load_yaml(uploaded_file.get("content", {}).get("content", "")) or {} + ) + yaml_include_names = [ + Path(file_path).name for file_path in content.get("INCLUDES", []) + ] + self.append_include_files(yaml_include_names) + try: if self.state.included_files[index]["name"] != uploaded_file["name"]: self.state.included_files[index]["error"] = ( From 6109f1a70c15e62f94de31de0c2fc17d13e46b98 Mon Sep 17 00:00:00 2001 From: Lasse Date: Thu, 27 Nov 2025 20:50:17 +0100 Subject: [PATCH 3/3] improve readability and fixes --- src/fourc_webviewer/fourc_webserver.py | 98 +++++++++++-------- .../fourc_yaml_file_visualization.py | 4 +- 2 files changed, 58 insertions(+), 44 deletions(-) diff --git a/src/fourc_webviewer/fourc_webserver.py b/src/fourc_webviewer/fourc_webserver.py index 0d39880..5c0f652 100644 --- a/src/fourc_webviewer/fourc_webserver.py +++ b/src/fourc_webviewer/fourc_webserver.py @@ -4,6 +4,7 @@ import copy import re +import shutil import tempfile from pathlib import Path @@ -125,7 +126,7 @@ def __init__( # initialize state object self.init_state_and_server_vars() - if "render_window" not in self._actors: + if "render_window" not in self._server_vars: self._server_vars["render_window"] = pv.Plotter() self.state.vtu_path = "" @@ -266,46 +267,43 @@ def init_pyvista_render_objects(self): The saved vtu file path is hereby utilized. """ # convert file to vtu and create dedicated render objects - if not Path( - self._server_vars["temp_dir_object"].name - + "\\" - + self._server_vars["fourc_yaml_name"] + if not ( + Path(self._server_vars["temp_dir_object"].name) + / self._server_vars["fourc_yaml_name"] ).exists(): raise Exception( "File does not exist: " + self._server_vars["temp_dir_object"].name - + "\\" + + "/" + self._server_vars["fourc_yaml_name"] ) - geometry_file_name = ( - self._server_vars["fourc_yaml_content"] - .sections.get("STRUCTURE GEOMETRY", {}) - .get("FILE") - ) + + # contains the dict of the structure geometry section of the current yaml file. + structure_geometry_section = self._server_vars[ + "fourc_yaml_content" + ].sections.get("STRUCTURE GEOMETRY", {}) + # contains the name of the geometry file defined in the STRUCTURE GEOMETRY section. + geometry_file_name = structure_geometry_section.get("FILE", None) + if geometry_file_name: - if not Path( - self._server_vars["temp_dir_object"].name - + "\\" - + Path( - self._server_vars["fourc_yaml_content"] - .sections.get("STRUCTURE GEOMETRY", {}) - .get("FILE") - ).name + # ensure that geometry_file_name really only contains the name and not a path + geometry_file_name = Path(geometry_file_name).name + if not ( + Path(self._server_vars["temp_dir_object"].name) / geometry_file_name ).exists(): + # if the current yaml file references a geometry file it will have already been loaded into the temp dir by now. + # if not something went wrong raise Exception( "File does not exist: " + self._server_vars["temp_dir_object"].name - + "\\" - + Path( - self._server_vars["fourc_yaml_content"]["STRUCTURE GEOMETRY"][ - "FILE" - ] - ).name + + "/" + + geometry_file_name ) + + # creates the FourCGeometry. By now every used file has to be in the temp dir fourc_geometry = FourCGeometry( - fourc_yaml_file=self._server_vars["temp_dir_object"].name - + "\\" - + self._server_vars["fourc_yaml_name"], + fourc_yaml_file=Path(self._server_vars["temp_dir_object"].name) + / self._server_vars["fourc_yaml_name"], temp_dir=Path(self._server_vars["temp_dir_object"].name), ) self.state.vtu_path = fourc_geometry.vtu_file_path @@ -972,10 +970,6 @@ def init_funct_state_and_server_vars(self): all_contained_var_names = get_variable_names_in_funct_expression( item_data["SYMBOLIC_FUNCTION_OF_SPACE_TIME"] ) - if "e" in all_contained_var_names: - all_contained_var_names.remove("e") - if "E" in all_contained_var_names: - all_contained_var_names.remove("E") # loop through contained variables and see whether they are evaluable for contained_var_name in all_contained_var_names: @@ -1022,22 +1016,32 @@ def append_include_files(self, file_paths): They will be uploaded before the user can edit or view the file. """ + # get the file names of the needed files. These names will be shown in the pop up window. yaml_include_names = [Path(file_path).name for file_path in file_paths] included_files = copy.deepcopy(self.state.included_files) + # make a copy, so the state triggers reactivity for include_name in yaml_include_names: + # this file path is created to check weather the needed file is already present on the server. + # If the user is, for example, continuously working on a file that references an exodus file + # they can copy it into the server file directory and it will be opened automatically + # without prompting the user to upload the .exo file every time. include_file_server = Path( self._server_vars["fourc_yaml_file_dir"], include_name, ) + # every file the user is working on will be loaded into the temp directory. + # This is because the FourCGeometry Constructor requires the .yaml file and the .exo file to be in the same directory. include_temp_path = Path( self._server_vars["temp_dir_object"].name, include_name, ) + + # if the file has been copied into the server directory it will be loaded into the temp dir automatically + # without prompting the user every time they open the .yaml file. if include_file_server.is_file(): - with open(include_file_server, "rb") as fr: - with open(include_temp_path, "wb") as fw: - fw.write(fr.read()) + shutil.copyfile(include_file_server, include_temp_path) elif not include_temp_path.is_file(): + # This is the standard case. The file is not present on the server and the user is prompted to upload it. included_files.append( { "name": include_name, @@ -1046,20 +1050,24 @@ def append_include_files(self, file_paths): "content": None, } ) + # trigger reactivity self.state.included_files = included_files def request_included_files(self): """Requests the included files from the user by opening a the include files dialog and setting up the state variable accordingly.""" - self.append_include_files( - [ - self._server_vars.get("fourc_yaml_content") - .sections.get("STRUCTURE GEOMETRY", {}) - .get("FILE") - or "" - ] + self.state.included_files = [] + # if the uploaded .yaml file contains a reference to a geometry file, this variable will be it's name. + # otherwise it will be None + geometry_file_name = ( + self._server_vars.get("fourc_yaml_content") + .sections.get("STRUCTURE GEOMETRY", {}) + .get("FILE", None) ) + + if geometry_file_name: + self.append_include_files([geometry_file_name]) # add yaml includes yaml_include_names = [ Path(file_path).name @@ -1435,10 +1443,14 @@ def change_selected_result_description_id( @change("selected_funct") def change_selected_funct(self, selected_funct, **kwargs): """Reaction to change of state.selected_funct.""" + # if there is no function_section + if not self.state.funct_section.get(selected_funct, {}): + return + # set the selected funct item to the first within the newly # selected funct self.state.selected_funct_item = next( - iter(self.state.funct_section.get(selected_funct, {})) + iter(self.state.funct_section.get(selected_funct, {})), ) # update plotly figure diff --git a/src/fourc_webviewer/input_file_utils/fourc_yaml_file_visualization.py b/src/fourc_webviewer/input_file_utils/fourc_yaml_file_visualization.py index 046e180..6e90a57 100644 --- a/src/fourc_webviewer/input_file_utils/fourc_yaml_file_visualization.py +++ b/src/fourc_webviewer/input_file_utils/fourc_yaml_file_visualization.py @@ -22,7 +22,9 @@ def get_variable_names_in_funct_expression(funct_expression: str): regular expressions.""" vars_found = re.findall(r"[A-Za-z_]+", funct_expression) return [ - v for v in vars_found if v not in DEF_FUNCT and v not in ["t", "x", "y", "z"] + v + for v in vars_found + if v not in DEF_FUNCT and v not in ["t", "x", "y", "z", "e", "E"] ]