Skip to content

Commit

Permalink
WIP: FileUpload Widget
Browse files Browse the repository at this point in the history
Resolves: issues#1542
  • Loading branch information
mlucool committed Sep 15, 2018
1 parent e27c7a8 commit d60310a
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 61 deletions.
1 change: 1 addition & 0 deletions ipywidgets/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@
from .widget_layout import Layout
from .widget_media import Image, Video, Audio
from .widget_style import Style
from .widget_upload import FileUpload
26 changes: 26 additions & 0 deletions ipywidgets/widgets/tests/test_widget_upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

from unittest import TestCase

from traitlets import TraitError

from ipywidgets import FileUpload


class TestFileUpload(TestCase):

def test_construction(self):
FileUpload()
# Defaults
assert uploader.accept == ''
assert uploader.multiple == False


def test_construction_accept(self):
uploader = FileUpload(accept='.txt)
assert uploader.accept == '.txt'

def test_construction_multiple(self):
uploader = FileUpload(accept='.txt)
assert uploader.multiple == True
54 changes: 54 additions & 0 deletions ipywidgets/widgets/widget_upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

from .widget_core import CoreWidget
from .domwidget import DOMWidget
from .widget import register
from traitlets import Unicode, Bool, List, observe
import base64
import copy
from datetime import datetime

@register()
class FileUpload(DOMWidget, CoreWidget):
"""Upload files from a browser to python
Parameters
----------
values : List
List of file objects
accept : string
Limit accepted file types. See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept
multiple : bool
Allow for multiple files to be uploaded
"""
_view_name = Unicode('UploadView').tag(sync=True)
_model_name = Unicode('UploadModel').tag(sync=True)

disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True)
values = List().tag(sync=True)
_values_base64 = List(help='File content, base64 encoded.').tag(trait=Unicode()).tag(sync=True)
accept = Unicode(help='Type of files the input accepts. None for all. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept').tag(sync=True)
multiple = Bool(help='If true, allow for multiple input files. Else only accept one').tag(sync=True)


def __init__(self, multiple=False, accept='', **kwargs):
self.accept = accept
self.multiple = multiple
super(FileUpload, self).__init__(**kwargs)


@observe('_values_base64')
def _on_files_changed(self, *args):
"""
Parse data from JS and put into python friendly form
"""
values = []
for v in self._values_base64:
f = copy.copy(v)
if 'contents' in f:
f['contents'] = base64.b64decode(f['contents'].split(',', 1)[1])
if 'lastModified' in f:
f['lastModified'] = datetime.fromtimestamp(f['lastModified']/1000.0)
values.append(f)
self.values = values
1 change: 1 addition & 0 deletions packages/controls/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from './widget_selection';
export * from './widget_selectioncontainer';
export * from './widget_string';
export * from './widget_description';
export * from './widget_upload';

export
const version = (require('../package.json') as any).version;
117 changes: 117 additions & 0 deletions packages/controls/src/widget_upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import {
DOMWidgetView
} from '@jupyter-widgets/base';

import {
CoreDOMWidgetModel
} from './widget_core';

import * as _ from 'underscore';

export
class UploadModel extends CoreDOMWidgetModel {
defaults() {
return _.extend(super.defaults(), {
_model_name: 'UploadModel',
_view_name: 'UploadView',
values: [],
_values_base64: [],
accept: '',
multiple: false
});
}
}

export
class UploadView extends DOMWidgetView {
render() {
/**
* Called when view is rendered.
*/
super.render();
this.pWidget.addClass('jupyter-widgets');
this.el.classList.add('widget-inline-hbox');
this.pWidget.addClass('widget-upload');

this.upload_container = document.createElement('input');
this.upload_container.setAttribute('type', 'file');
this.handleUploadChanged = this.handleUploadChanged.bind(this);
this.upload_container.onchange = this.handleUploadChanged;

this.el.appendChild(this.upload_container);


this.listenTo(this.model, 'change:values', this._update_values);
this._update_values();
this.update(); // Set defaults.
}

_update_values() {
// Only allow for value to clear. This is a rule enforced by browsers
const values = this.model.get('values');
if (values.length === 0) {
this.upload_container.value = '';
}
}

update(options?) {
/**
* Update the contents of this view
*
* Called when the model is changed. The model may have been
* changed by another view or by a state update from the back-end.
*/
if (options === undefined || options.updated_view !== this) {
this.upload_container.disabled = this.model.get('disabled');
this.upload_container.setAttribute('accept', this.model.get('accept'));
if (this.model.get('multiple')) {
this.upload_container.setAttribute('multiple', 'true');
} else {
this.upload_container.removeAttribute('multiple');
}
}
return super.update(options);
}

handleUploadChanged(event) {
const that = this;
const {files} = event.target;
if (!files || files.length === 0) {
that.model.set('_values_base64', []);
that.touch();
return;
}

const fileContentsPromises = [];
for (let file of files) { // files it NOT an array
fileContentsPromises.push(new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = function fileReaderOnload() {
resolve({
name: file.name,
contents: fileReader.result,
type: file.type,
lastModified: file.lastModified
});
};
fileReader.readAsDataURL(file);
}));
}
Promise.all(fileContentsPromises)
.then((contents) => {
that.model.set('_values_base64', contents);
that.touch();
})
.catch((err) => {
console.error('FileUploadView Error loading files: %o', err);
// FIXME: Set state?
});
// FIXME: Add an icon that shows it loading?
}

upload_container: HTMLInputElement;
model: UploadModel;
}
Loading

0 comments on commit d60310a

Please sign in to comment.