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

Add FileInput widget #207

Merged
merged 4 commits into from
Jan 9, 2019
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
25 changes: 24 additions & 1 deletion examples/user_guide/Widgets.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
70 changes: 70 additions & 0 deletions panel/models/fileinput.ts
Original file line number Diff line number Diff line change
@@ -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<FileInput.Attrs>) {
super(attrs)
}

static initClass(): void {
this.prototype.type = "FileInput"
this.prototype.default_view = FileInputView

this.define({
value: [ p.Any, '' ],
})
}
}

FileInput.initClass()
11 changes: 10 additions & 1 deletion panel/models/widgets.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os

from bokeh.core.properties import Int, Override, Enum
from bokeh.core.properties import Int, Override, Enum, Any
from bokeh.models import Widget

from ..util import CUSTOM_MODELS
Expand Down Expand Up @@ -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 = Any(help="Encoded file data")


CUSTOM_MODELS['panel.models.widgets.Player'] = Player
CUSTOM_MODELS['panel.models.widgets.FileInput'] = FileInput

21 changes: 19 additions & 2 deletions panel/tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)


Expand Down Expand Up @@ -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=='
63 changes: 48 additions & 15 deletions panel/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import re
import ast

from base64 import b64decode, b64encode
from collections import OrderedDict
from datetime import datetime

Expand All @@ -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

Expand Down Expand Up @@ -89,6 +91,37 @@ class TextInput(Widget):
_widget_type = _BkTextInput


class FileInput(Widget):

mime_type = param.String(default=None)

value = param.Parameter(default=None)

_widget_type = _BkFileInput

_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['mime_type'] = header.split(':')[1].split(';')[0]
msg['value'] = b64decode(content)
return msg


class StaticText(Widget):

value = param.Parameter(default=None)
Expand Down Expand Up @@ -429,7 +462,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()])
Expand All @@ -448,12 +481,12 @@ def _process_property_change(self, msg):


class RadioButtonGroup(_RadioGroupBase):

_widget_type = _BkRadioButtonGroup


class RadioBoxGroup(_RadioGroupBase):

_widget_type = _BkRadioBoxGroup


Expand All @@ -479,33 +512,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:
Expand All @@ -514,7 +547,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)
Expand Down Expand Up @@ -543,7 +576,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",
Expand Down