Skip to content
Merged
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
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ This repository contains examples of how to build application functionalities in
7. [Changing Pydantic rules based on user input](examples/dynamic_pydantic_rules)
8. [Complex Pydantic rules](examples/complex_pydantic_rules)
9. [Selecting datafiles from the server](examples/data_selector)
10. [Running a Galaxy tool](examples/run_galaxy_tool)
11. [Running a Galaxy workflow](examples/run_galaxy_workflow)
12. [Working with Plotly](examples/plotly)
13. [Working with Matplotlib](examples/matplotlib)
14. [Working with VTK](examples/vtk)
15. [Synchronizing changes between tabs](examples/multitab)
10. [Downloading files to the user's computer](examples/downloading_files)
11. [Running a Galaxy tool](examples/run_galaxy_tool)
12. [Running a Galaxy workflow](examples/run_galaxy_workflow)
13. [Working with Plotly](examples/plotly)
14. [Working with Matplotlib](examples/matplotlib)
15. [Working with VTK](examples/vtk)
16. [Synchronizing changes between tabs](examples/multitab)

We also provide examples that take advantage of ORNL resources:

Expand Down
5 changes: 5 additions & 0 deletions examples/downloading_files/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Entrypoint."""

from .main import main

main()
12 changes: 12 additions & 0 deletions examples/downloading_files/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Runs the file download example."""

from examples.downloading_files.view import App


def main() -> None:
app = App()
app.server.start(open_browser=False)


if __name__ == "__main__":
main()
21 changes: 21 additions & 0 deletions examples/downloading_files/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Model implementation for file download example."""

from typing import List

from pydantic import BaseModel, Field


class FormData(BaseModel):
"""Pydantic model for the form data."""

selected_files: List[str] = Field(default=[], title="Selected Files")


class Model:
"""Model implementation for file download example."""

def __init__(self) -> None:
self.form = FormData()

def get_selected_files(self) -> List[str]:
return self.form.selected_files
66 changes: 66 additions & 0 deletions examples/downloading_files/view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""View for file download example."""

import os
from typing import Optional

from nova.mvvm.trame_binding import TrameBinding
from nova.trame import ThemedApp
from nova.trame.view.components import DataSelector
from nova.trame.view.layouts import VBoxLayout
from trame.widgets import client
from trame.widgets import vuetify3 as vuetify

from .model import Model
from .view_model import ViewModel


class App(ThemedApp):
"""View for file download example."""

def __init__(self) -> None:
super().__init__()

self.create_vm()
# If you forget to call connect, then the application will crash when you attempt to update the view.
self.view_model.form_data_bind.connect("data")
# Generally, we want to initialize the view state before creating the UI for ease of use. If initialization
# is expensive, then you can defer it. In this case, you must handle the view state potentially being
# uninitialized in the UI via v_if statements.
self.view_model.update_form_data()

self._download: Optional[client.JSEval] = None

self.create_ui()

def create_ui(self) -> None:
self.set_theme("CompactTheme")

with super().create_ui() as layout:
with layout.content:
with VBoxLayout(classes="mb-2", stretch=True):
# Please note that this is a dangerous operation. You should ensure that you restrict this
# component to only expose files that are strictly necessary to making your application
# functional.
DataSelector(
v_model="data.selected_files",
directory=os.environ.get("HOME", "/"),
classes="mb-1",
)

with VBoxLayout(halign="center"):
vuetify.VBtn("Download Selected Files", click=self.prepare_download)

def create_vm(self) -> None:
binding = TrameBinding(self.state)

model = Model()
self.view_model = ViewModel(model, binding)

async def prepare_download(self) -> None:
content = self.view_model.prepare_zip()
if content:
# See https://nova-application-development.readthedocs.io/projects/nova-trame/en/stable/api.html#nova.trame.ThemedApp.download_file
# for more information on the method.
# application/zip is the MIME type of the data to download. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types
# for further discussion and common types.
self.download_file("selected_files.zip", "application/zip", content)
44 changes: 44 additions & 0 deletions examples/downloading_files/view_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""View model implementation for file download example."""

import os
import zipfile
from io import BytesIO
from typing import Any, Dict, Optional

from nova.mvvm.interface import BindingInterface

from .model import Model


class ViewModel:
"""View model implementation for file download example."""

def __init__(self, model: Model, binding: BindingInterface) -> None:
self.model = model

# self.on_update is called any time the view updates the binding.
self.form_data_bind = binding.new_bind(self.model.form, callback_after_update=self.on_update)

def on_update(self, results: Dict[str, Any]) -> None:
# This fires when the data selector is updated. You could run some process on the newly selected data or update
# other portions of the UI here as necessary.
print(f"Selected files updated: {self.model.get_selected_files()}")

def prepare_zip(self) -> Optional[bytes]:
# To download a file through the browser, we need an in-memory bytestream that we can send to the browser.
selected_files = self.model.get_selected_files()
if not selected_files:
return None

zip_buffer = BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED, False) as zip_file:
for file_path in selected_files:
file_name = os.path.basename(file_path)
# arcname allows us to specify a folder into which the files will be extracted.
zip_file.write(file_path, arcname=f"test/{file_name}")

return zip_buffer.getvalue()

def update_form_data(self) -> None:
# This will fail if you haven't called connect on the binding!
self.form_data_bind.update_in_view(self.model.form)
Loading