Commit 856b2600 authored by Duggan, John's avatar Duggan, John
Browse files

Switch to RemoteFileInput for accessing files via the server rather than browser upload

parent 64524833
Loading
Loading
Loading
Loading
+186 −159

File changed.

Preview size limit exceeded, changes collapsed.

+1 −1
Original line number Diff line number Diff line
@@ -23,7 +23,7 @@ ips-fastran-gui = { path = ".", editable = true }
hatch = "*"
gitpython = ">=3.1.40,<4"
python-gitlab = ">=5.6.0,<6"
nova-trame = "==1.3.2-dev0"
nova-trame = "*"
trame-datagrid = ">=0.2.2"
trame-code = ">=1.0.2"
nova-galaxy = ">=0.11.1"
+65 −22
Original line number Diff line number Diff line
"""Module for the main model."""

import json
import os
import zipfile
from enum import Enum
from io import BytesIO
@@ -76,11 +77,17 @@ class MainModel:

        self.file_tree: Dict[str, Any] = {}

    def _download_files(self, zip_obj: zipfile.ZipFile, files: List[Dict[str, Any]]) -> None:
        for file in files:
            if "children" in file:
                self._download_files(zip_obj, file["children"])
            else:
                zip_obj.writestr(file["relative_path"], file["content"])

    def download_files(self) -> bytes:
        archive = BytesIO()
        with zipfile.ZipFile(archive, "w") as zip_obj:
            for file in self.config.input_files:
                zip_obj.writestr(file["relative_path"], file["content"])
            self._download_files(zip_obj, self.config.input_files)
        archive.seek(0)

        return archive.read()
@@ -94,32 +101,68 @@ class MainModel:

        return file

    def set_file_contents(self, index: int, json_data: str) -> None:
        self.config.input_files[index]["content"] = json_data
    def read_files(self, file_path: str) -> None:
        self.file_tree = {}
        for dirpath, _, filenames in os.walk(file_path):
            relative_path = os.path.relpath(dirpath, file_path)
            if relative_path == ".":
                relative_path = ""
                parts = []
            else:
                parts = relative_path.split("/")

    def set_file_path(self, index: int, path: str) -> None:
        self.config.input_files[index]["name"] = path.split("/")[-1]
        self.config.input_files[index]["relative_path"] = path
            current_level = self.file_tree
            for part in parts:
                if part not in current_level:
                    current_level[part] = {}
                current_level = current_level[part]

    def set_files(self, files: List[Dict[str, Any]], relative_paths: List[str]) -> None:
        if not files:
            self.config.input_files = []
            for file in filenames:
                try:
                    path = os.path.join(dirpath, file)
                    with open(path, "r") as file_obj:
                        current_level[file] = {
                            "content": file_obj.read(),
                            "name": file,
                            "path": path,
                            "relative_path": os.path.join(relative_path, file),
                        }
                except Exception:
                    pass

        self.file_tree = {}
        for index, file in enumerate(files):
            relative_path = relative_paths[index]
            parts = relative_path.split("/")[1:]
        self.config.input_files = self.set_files_from_tree(self.file_tree)

            decoded_file = file.copy()
            decoded_file["content"] = decoded_file["content"].decode("latin1")
            decoded_file["path"] = "/".join(parts)
    def set_file_contents(self, path: str, relative_path: str, json_data: str) -> None:
        parts = relative_path.split("/") if relative_path != "." else []
        current_level = self.file_tree
        for part in parts:
            if part:
                current_level = current_level[part]
        current_level["content"] = json_data

        with open(path, "w") as file_obj:
            file_obj.write(json_data)

        self.config.input_files = self.set_files_from_tree(self.file_tree)

    def set_file_path(self, old_path: str, new_path: str) -> None:
        current_level = self.file_tree
        parts = old_path.split("/")
        for part in parts[:-1]:
                if part not in current_level:
                    current_level[part] = {}
            current_level = current_level[part]
            current_level[parts[-1]] = decoded_file
        file = current_level.pop(parts[-1])

        file["path"] = ""
        parts = new_path.split("/")
        current_level = self.file_tree
        for part in parts[:-1]:
            file["path"] += f"{part}/"
            current_level = current_level[part]
        if "path" not in current_level[parts[-1]]:
            file["path"] += f"{parts[-1]}/"
            current_level = current_level[parts[-1]]
        file["path"] += file["name"]
        current_level[file["name"]] = file

        self.config.input_files = self.set_files_from_tree(self.file_tree)

+14 −10
Original line number Diff line number Diff line
"""Module for the main ViewModel."""

import os
from pathlib import Path
from typing import Any, Dict, List

@@ -19,9 +20,9 @@ class ViewState(BaseModel):
    active_tab: str = Field(default="0")
    errors: List[str] = Field(default=[])
    results_disabled: bool = Field(default=False)
    editor_index: int = Field(default=0)
    editor_path: str = Field(default="", title="File Path")
    input_file_path: str = Field(default=os.getenv("HOME", os.getcwd()), title="Path to Input Files")
    editor_content: str = Field(default="")
    editor_path: str = Field(default="")


class MainViewModel:
@@ -68,7 +69,11 @@ class MainViewModel:
        self.view_state.errors = []
        try:
            self.view_state.editor_content = json_data
            # self.model.set_file_contents(self.view_state.editor_index, json_data)
            self.model.set_file_contents(
                self.view_state.editor_path,
                os.path.relpath(self.view_state.editor_path, self.view_state.input_file_path),
                json_data,
            )
        except ValidationError as e:
            for error in e.errors():
                msg = ""
@@ -103,8 +108,8 @@ class MainViewModel:
        updated = results.get("updated", [])
        for update in updated:
            match update:
                case "editor_path":
                    # self.model.set_file_path(self.view_state.editor_index, self.view_state.editor_path)
                case "input_file_path":
                    self.model.read_files(self.view_state.input_file_path)
                    self.config_bind.update_in_view(self.model.config)

    def on_completion(self, _sender: Any) -> None:
@@ -121,18 +126,14 @@ class MainViewModel:
            self.view_state.active_tab = "2"
            self.view_state_bind.update_in_view(self.view_state)

    def set_files(self, files: List[Dict[str, Any]], relative_paths: List[str]) -> None:
        self.model.set_files(files, relative_paths)
        self.config_bind.update_in_view(self.model.config)

    def edit_file(self, path: List[str]) -> None:
        if not path:
            return

        file = self.model.get_file_from_path(path[0])

        self.view_state.editor_path = file["path"]
        self.view_state.editor_content = file["content"]
        self.view_state.editor_path = file["path"]

        self.view_state_bind.update_in_view(self.view_state)

@@ -142,3 +143,6 @@ class MainViewModel:
        plotter.set_page()

        self.figure_bind.update_in_view(None)

    def update_path(self, old_path: str, new_path: str) -> None:
        self.model.set_file_path(old_path, new_path)
+30 −27
Original line number Diff line number Diff line
"""Module for the Config tab."""

from nova.trame.view.components import InputField
import os

from nova.trame.view.components import InputField, RemoteFileInput
from nova.trame.view.layouts import GridLayout, HBoxLayout, VBoxLayout
from trame.app import get_server
from trame.widgets import code
from trame.widgets import vuetify3 as vuetify

@@ -12,6 +15,7 @@ class ConfigTab:
    """Config view class."""

    def __init__(self, view_model: MainViewModel) -> None:
        self.server = get_server(None, client_type="vue3")
        self.view_model = view_model
        self.create_ui()

@@ -22,44 +26,39 @@ class ConfigTab:
                InputField(v_model="config.time_id")

            with HBoxLayout():
                InputField(
                    v_model="config.input_files",
                    active=True,
                    id="file-upload",
                    multiple=True,
                    raw_attrs=["webkitdirectory"],
                    type="file",
                    update_modelValue=(
                        self.view_model.set_files,
                        # TODO: I would like to better understand why webkitRelativePath is not available in the v-model
                        # by default.
                        (
                            "["
                            "  config.input_files,"
                            "  Array.from(window.document.getElementById('file-upload').files).map("
                            "   file => file.webkitRelativePath"
                            ")]"
                        ),
                    ),
                RemoteFileInput(
                    v_model="state.input_file_path",
                    allow_files=False,
                    allow_folders=True,
                    # TODO: base_paths should be set differently if running on a cluster
                    base_paths=[os.getenv("HOME", os.getcwd())],
                )

            with GridLayout(classes="mb-2", columns=3, stretch=True):
                with VBoxLayout():
                with VBoxLayout(classes="pl-2"):
                    vuetify.VListSubheader("Uploaded Files")
                    vuetify.VTreeview(
                    with vuetify.VTreeview(
                        v_if="config.input_files.length > 0",
                        activatable=True,
                        active_strategy="single-leaf",
                        items=("config.input_files",),
                        item_title="name",
                        item_value="path",
                        update_activated=(self.view_model.edit_file, "[$event]"),
                    ):
                        with vuetify.Template(v_slot_title="{ item }"):
                            vuetify.VListItemTitle(
                                "{{ item.name }}",
                                raw_attrs=[
                                    '''draggable="true"''',
                                    '''@dragenter.prevent=""''',
                                    '''@dragover.prevent=""''',
                                    '''@dragstart="window.ips_drag_path = item.path"''',
                                ],
                                __events=["drop"],
                                drop="trigger('on_drop', [window.ips_drag_path, item.path])",
                            )
                    vuetify.VListItem("No files available.", v_else=True)
                with VBoxLayout(column_span=2, stretch=True):
                    with VBoxLayout():
                        InputField("state.editor_path")

                    code.Editor(
                        ref="input_config",
                        model_value=("state.editor_content",),
@@ -67,3 +66,7 @@ class ConfigTab:
                        theme="vs-dark",
                        input=(self.view_model.on_change_file, "[$event]"),
                    )

        @self.server.controller.trigger("on_drop")
        def on_drop(old_path: str, new_path: str) -> None:
            self.view_model.update_path(old_path, new_path)