Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable directory uploads with FileInput #6808

Merged
merged 9 commits into from
Jun 25, 2024
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
16 changes: 14 additions & 2 deletions examples/reference/widgets/FileInput.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"##### Core\n",
"\n",
"* **``accept``** (str): A list of file input filters that restrict what files the user can pick from\n",
"* **``directory``** (str): If directories is upload instead of files\n",
"* **``filename``** (str/list): The filename(s) of the uploaded file(s)\n",
"* **``mime_type``** (str/list): The mime type(s) of the uploaded file(s)\n",
"* **``multiple``** (boolean): Whether to allow uploading multiple files\n",
Expand Down Expand Up @@ -113,7 +114,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"To allow uploading multiple files we can also set `multiple=True`:"
"To allow uploading multiple files we can also set `multiple=True` or if you want to upload a whole directory `directory=True`:"
]
},
{
Expand All @@ -131,7 +132,18 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"When uploading one or more files the `filename`, `mime_type` and `value` parameters will now be lists. "
"When uploading one or more files the `filename`, `mime_type` and `value` parameters will now be lists. \n",
"\n",
"You can also clear the file input with the `.clear()` method."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"file_input.clear()"
]
},
{
Expand Down
15 changes: 1 addition & 14 deletions panel/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import textwrap

from collections import defaultdict
from functools import partial
from typing import (
TYPE_CHECKING, Any, Callable, ClassVar, Literal, Mapping, Optional,
)
Expand All @@ -18,7 +17,6 @@

from .config import config
from .io.datamodel import construct_data_model
from .io.notebook import push
from .io.state import state
from .models import (
AnyWidgetComponent as _BkAnyWidgetComponent,
Expand Down Expand Up @@ -406,15 +404,4 @@ def send(self, msg: dict):
---------
msg: dict
"""
for ref, (model, _) in self._models.items():
if ref not in state._views or ref in state._fake_roots:
continue
event = ESMEvent(model=model, data=msg)
viewable, root, doc, comm = state._views[ref]
if comm or state._unblocked(doc) or not doc.session_context:
doc.callbacks.send_event(event)
if comm and 'embedded' not in root.tags:
push(doc, comm)
else:
cb = partial(doc.callbacks.send_event, event)
doc.add_next_tick_callback(cb)
self._send_event(ESMEvent, data=msg)
29 changes: 28 additions & 1 deletion panel/reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@

from bokeh.document import Document
from bokeh.events import Event
from bokeh.model import Model
from bokeh.model import Model, ModelEvent
from bokeh.models.sources import DataDict, Patches
from pyviz_comms import Comm

Expand Down Expand Up @@ -874,6 +874,33 @@ def jslink(
return Link(self, target, properties=links, code=code, args=args,
bidirectional=bidirectional)

def _send_event(self, Event: ModelEvent, **event_kwargs):
"""
Send an event to the frontend

Arguments
----------
Event: Bokeh.Event
The event to send to the frontend
event_kwargs: dict
Additional keyword arguments to pass to the event
This will create the following event:
Event(model=model, **event_kwargs)

"""
for ref, (model, _) in self._models.items():
if ref not in state._views or ref in state._fake_roots:
continue
event = Event(model=model, **event_kwargs)
_viewable, root, doc, comm = state._views[ref]
if comm or state._unblocked(doc) or not doc.session_context:
doc.callbacks.send_event(event)
if comm and 'embedded' not in root.tags:
push(doc, comm)
else:
cb = partial(doc.callbacks.send_event, event)
doc.add_next_tick_callback(cb)


TData = Union['pd.DataFrame', 'DataDict']

Expand Down
27 changes: 26 additions & 1 deletion panel/widgets/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
PasswordInput as _BkPasswordInput, Spinner as _BkSpinner,
Switch as _BkSwitch,
)
from bokeh.models.widgets.inputs import ClearInput
from pyviz_comms import JupyterComm

from ..config import config
Expand Down Expand Up @@ -203,6 +204,20 @@ class FileInput(Widget):
An HTML string describing the function of this component
rendered as a tooltip icon.""")

directory = param.Boolean(default=False, doc="""
Whether to allow selection of directories instead of files.
The filename will be relative paths to the uploaded directory.

.. note::
When a directory is uploaded it will give add a confirmation pop up.
The confirmation pop up cannot be disabled, as this is a security feature
in the browser.

.. note::
The `accept` parameter only works with file extension.
When using `accept` with `directory`, the number of files
reported will be the total amount of files, not the filtered.""")

filename = param.ClassSelector(
default=None, class_=(str, list), is_instance=True, doc="""
Name of the uploaded file(s).""")
Expand Down Expand Up @@ -246,9 +261,13 @@ def _process_property_change(self, msg):
msg = super()._process_property_change(msg)
if 'value' in msg:
if isinstance(msg['value'], str):
msg['value'] = b64decode(msg['value'])
msg['value'] = b64decode(msg['value']) if msg['value'] else None
else:
msg['value'] = [b64decode(content) for content in msg['value']]
if 'filename' in msg and len(msg['filename']) == 0:
msg['filename'] = None
if 'mime_type' in msg and len(msg['mime_type']) == 0:
msg['mime_type'] = None
return msg

def save(self, filename):
Expand Down Expand Up @@ -285,6 +304,12 @@ def save(self, filename):
else:
fn.write(val)

def clear(self):
"""
Clear the file(s) in the FileInput widget
"""
self._send_event(ClearInput)


class FileDropper(Widget):
"""
Expand Down
Loading