From e80b065a58483d2a021cab464d7295ca8437d3c8 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Tue, 8 Jan 2019 19:11:38 +0000
Subject: [PATCH 1/4] Add FileInput widget
---
panel/models/fileinput.ts | 70 +++++++++++++++++++++++++++++++++++++++
panel/models/widgets.py | 11 +++++-
panel/widgets.py | 51 +++++++++++++++++++---------
3 files changed, 116 insertions(+), 16 deletions(-)
create mode 100644 panel/models/fileinput.ts
diff --git a/panel/models/fileinput.ts b/panel/models/fileinput.ts
new file mode 100644
index 0000000000..ee793f538a
--- /dev/null
+++ b/panel/models/fileinput.ts
@@ -0,0 +1,70 @@
+import * as p from "core/properties"
+import {div} from "core/dom"
+import {Widget, WidgetView} from "models/widgets/widget"
+
+export class FileInputView extends WidgetView {
+ model: FileInput
+
+ protected dialogEl: HTMLElement
+
+ initialize(options: any): void {
+ super.initialize(options)
+ this.render()
+ }
+
+ connect_signals(): void {
+ super.connect_signals()
+ this.connect(this.model.change, () => this.render())
+ this.connect(this.model.properties.value.change, () => this.render())
+ this.connect(this.model.properties.width.change, () => this.render())
+ }
+
+ render(): void {
+ if (this.dialogEl) {
+ return
+ }
+ this.dialogEl = document.createElement('input')
+ this.dialogEl.type = "file";
+ this.dialogEl.multiple = false;
+ this.dialogEl.style = `width:{this.model.width}px`;
+ this.dialogEl.onchange = (e) => this.load_file(e);
+ this.el.appendChild(this.dialogEl)
+ }
+
+ load_file(e: any): void {
+ const reader = new FileReader();
+ reader.onload = (e) => this.set_value(e)
+ reader.readAsDataURL(e.target.files[0])
+ }
+
+ set_value(e: any): void {
+ this.model.value = e.target.result;
+ }
+}
+
+export namespace FileInput {
+ export interface Attrs extends Widget.Attrs {}
+ export interface Props extends Widget.Props {}
+}
+
+export interface FileInput extends FileInput.Attrs {}
+
+export abstract class FileInput extends Widget {
+
+ properties: FileInput.Props
+
+ constructor(attrs?: Partial) {
+ super(attrs)
+ }
+
+ static initClass(): void {
+ this.prototype.type = "FileInput"
+ this.prototype.default_view = FileInputView
+
+ this.define({
+ value: [ p.Any, '' ],
+ })
+ }
+}
+
+FileInput.initClass()
diff --git a/panel/models/widgets.py b/panel/models/widgets.py
index 5c76577086..c4d3e08bf3 100644
--- a/panel/models/widgets.py
+++ b/panel/models/widgets.py
@@ -1,6 +1,6 @@
import os
-from bokeh.core.properties import Int, Override, Enum
+from bokeh.core.properties import Int, Override, Enum, String
from bokeh.models import Widget
from ..util import CUSTOM_MODELS
@@ -32,4 +32,13 @@ class Player(Widget):
height = Override(default=250)
+class FileInput(Widget):
+
+ __implementation__ = os.path.join(os.path.dirname(__file__), 'fileinput.ts')
+
+ value = String(help="Selected file")
+
+
CUSTOM_MODELS['panel.models.widgets.Player'] = Player
+CUSTOM_MODELS['panel.models.widgets.FileInput'] = FileInput
+
diff --git a/panel/widgets.py b/panel/widgets.py
index e260228a29..2e6a554c95 100644
--- a/panel/widgets.py
+++ b/panel/widgets.py
@@ -6,6 +6,8 @@
import re
import ast
+
+from base64 import b64decode
from collections import OrderedDict
from datetime import datetime
@@ -23,7 +25,7 @@
)
from .layout import Column, Row, Spacer, WidgetBox # noqa
-from .models.widgets import Player as _BkPlayer
+from .models.widgets import Player as _BkPlayer, FileInput as _BkFileInput
from .viewable import Reactive
from .util import as_unicode, push, value_as_datetime, hashable
@@ -89,6 +91,25 @@ class TextInput(Widget):
_widget_type = _BkTextInput
+class FileInput(Widget):
+
+ filetype = param.String(default='')
+
+ value = param.Parameter(default='')
+
+ _widget_type = _BkFileInput
+
+ _rename = {'name': None, 'filetype': None}
+
+ def _process_property_change(self, msg):
+ msg = super(FileInput, self)._process_property_change(msg)
+ if 'value' in msg:
+ header, content = msg['value'].split(",", 1)
+ msg['filetype'] = header.split(':')[1].split(';')[0]
+ msg['value'] = b64decode(content)
+ return msg
+
+
class StaticText(Widget):
value = param.Parameter(default=None)
@@ -429,7 +450,7 @@ def _process_property_change(self, msg):
class _RadioGroupBase(Select):
-
+
def _process_param_change(self, msg):
msg = super(Select, self)._process_param_change(msg)
mapping = OrderedDict([(hashable(v), k) for k, v in self.options.items()])
@@ -448,12 +469,12 @@ def _process_property_change(self, msg):
class RadioButtonGroup(_RadioGroupBase):
-
+
_widget_type = _BkRadioButtonGroup
class RadioBoxGroup(_RadioGroupBase):
-
+
_widget_type = _BkRadioBoxGroup
@@ -479,33 +500,33 @@ def _process_property_change(self, msg):
class CheckButtonGroup(_CheckGroupBase):
-
+
_widget_type = _BkCheckboxButtonGroup
class CheckBoxGroup(_CheckGroupBase):
-
+
_widget_type = _BkCheckboxGroup
-
+
class ToggleGroup(Select):
"""This class is a factory of ToggleGroup widgets.
-
+
A ToggleGroup is a group of widgets which can be switched 'on' or 'off'.
-
+
Two types of widgets are available through the widget_type argument :
- 'button' (default)
- 'box'
-
+
Two different behaviors are available through behavior argument:
- 'check' (default) : Any number of widgets can be selected. In this case value is a 'list' of objects
- 'radio' : One and only one widget is switched on. In this case value is an 'object'
-
+
"""
-
+
_widgets_type = ['button', 'box']
_behaviors = ['check', 'radio']
-
+
def __new__(cls, widget_type='button', behavior='check', **params):
if widget_type not in ToggleGroup._widgets_type:
@@ -514,7 +535,7 @@ def __new__(cls, widget_type='button', behavior='check', **params):
if behavior not in ToggleGroup._behaviors:
raise ValueError('behavior {} is not valid. Valid options are {}'
.format(widget_type, ToggleGroup._behaviors))
-
+
if behavior is 'check':
if widget_type == 'button':
return CheckButtonGroup(**params)
@@ -543,7 +564,7 @@ class ToggleButtons(CheckButtonGroup):
""""
Deprecated, use ToggleGroup instead.
"""
-
+
def __new__(cls, **params):
from warnings import warn
warn("Deprecated class, will be removed in future.\nSee ToggleGroup",
From 9f2272d6292f7c03c047169f498946a02e31f911 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 9 Jan 2019 14:46:05 +0000
Subject: [PATCH 2/4] Fixed issue syncing FileInput state
---
panel/models/widgets.py | 4 ++--
panel/widgets.py | 22 +++++++++++++++++-----
2 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/panel/models/widgets.py b/panel/models/widgets.py
index c4d3e08bf3..fc89aae22b 100644
--- a/panel/models/widgets.py
+++ b/panel/models/widgets.py
@@ -1,6 +1,6 @@
import os
-from bokeh.core.properties import Int, Override, Enum, String
+from bokeh.core.properties import Int, Override, Enum, Any
from bokeh.models import Widget
from ..util import CUSTOM_MODELS
@@ -36,7 +36,7 @@ class FileInput(Widget):
__implementation__ = os.path.join(os.path.dirname(__file__), 'fileinput.ts')
- value = String(help="Selected file")
+ value = Any(help="Encoded file data")
CUSTOM_MODELS['panel.models.widgets.Player'] = Player
diff --git a/panel/widgets.py b/panel/widgets.py
index 2e6a554c95..f86174c830 100644
--- a/panel/widgets.py
+++ b/panel/widgets.py
@@ -7,7 +7,7 @@
import re
import ast
-from base64 import b64decode
+from base64 import b64decode, b64encode
from collections import OrderedDict
from datetime import datetime
@@ -93,19 +93,31 @@ class TextInput(Widget):
class FileInput(Widget):
- filetype = param.String(default='')
+ mime_type = param.String(default=None)
- value = param.Parameter(default='')
+ value = param.Parameter(default=None)
_widget_type = _BkFileInput
- _rename = {'name': None, 'filetype': None}
+ _rename = {'name': None, 'mime_type': None}
+
+ def _process_param_change(self, msg):
+ msg = super(FileInput, self)._process_param_change(msg)
+ if 'value' in msg:
+ if self.mime_type:
+ template = 'data:{mime};base64,{data}'
+ data = b64encode(msg['value'])
+ msg['value'] = template.format(data=data.decode('utf-8'),
+ mime=self.mime_type)
+ else:
+ msg['value'] = ''
+ return msg
def _process_property_change(self, msg):
msg = super(FileInput, self)._process_property_change(msg)
if 'value' in msg:
header, content = msg['value'].split(",", 1)
- msg['filetype'] = header.split(':')[1].split(';')[0]
+ msg['mime_type'] = header.split(':')[1].split(';')[0]
msg['value'] = b64decode(content)
return msg
From 9fc3d70da2438e8fb14cac45d2b2eed57cd01ebe Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 9 Jan 2019 14:46:26 +0000
Subject: [PATCH 3/4] Added FileInput docs
---
examples/user_guide/Widgets.ipynb | 25 ++++++++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/examples/user_guide/Widgets.ipynb b/examples/user_guide/Widgets.ipynb
index 8d30a0416b..cb7e576ed6 100644
--- a/examples/user_guide/Widgets.ipynb
+++ b/examples/user_guide/Widgets.ipynb
@@ -756,12 +756,35 @@
"cross_select.value"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## FileInput"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The ``FileInput`` widget allows uploading a file from the frontend and makes the file data and file type available in Python."
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
- "source": []
+ "source": [
+ "FileInput()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To read out the content of the file you can access the ``value`` parameter which holds a bytestring containing the files contents. Additionally information about the file type is made available on the ``filetype`` parameter expressed as a MIME type, e.g. ``image/png`` or ``text/csv``."
+ ]
}
],
"metadata": {
From ee1fc4fcc095a780b3aa7a1734eb3eb6c76e9008 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Wed, 9 Jan 2019 14:46:52 +0000
Subject: [PATCH 4/4] Added FileInput unit test
---
panel/tests/test_widgets.py | 21 +++++++++++++++++++--
1 file changed, 19 insertions(+), 2 deletions(-)
diff --git a/panel/tests/test_widgets.py b/panel/tests/test_widgets.py
index 7107a9005a..90ae322d5a 100644
--- a/panel/tests/test_widgets.py
+++ b/panel/tests/test_widgets.py
@@ -5,14 +5,14 @@
from bokeh.layouts import WidgetBox
from bokeh.models import Div as BkDiv, Slider as BkSlider
-from panel.models.widgets import Player as BkPlayer
+from panel.models.widgets import Player as BkPlayer, FileInput as BkFileInput
from panel.util import block_comm
from panel.widgets import (
TextInput, StaticText, FloatSlider, IntSlider, RangeSlider,
LiteralInput, Checkbox, Select, MultiSelect, Button, Toggle,
DatePicker, DateRangeSlider, DiscreteSlider, DatetimeInput,
RadioButtons, ToggleButtons, CrossSelector, DiscretePlayer,
- ToggleGroup
+ ToggleGroup, FileInput
)
@@ -663,3 +663,20 @@ def test_discrete_player(document, comm):
discrete_player.value = 100
assert widget.value == 3
+
+
+def test_file_input(document, comm):
+ file_input = FileInput()
+
+ box = file_input._get_model(document, comm=comm)
+
+ assert isinstance(box, WidgetBox)
+
+ widget = box.children[0]
+ assert isinstance(widget, BkFileInput)
+
+ file_input._comm_change({'value': 'data:text/plain;base64,U29tZSB0ZXh0Cg=='})
+ assert file_input.value == b'Some text\n'
+
+ file_input.param.trigger('value')
+ assert widget.value == 'data:text/plain;base64,U29tZSB0ZXh0Cg=='