witten
/
luminotes
Archived
1
0
Fork 0

Initial work on UI and controller for file uploading:

* new toolbar button for attaching a file
 * button opens new Upload_pulldown() for uploading a file
 * began controller.Notebooks.upload_file() to process the upload
This commit is contained in:
Dan Helfman 2008-01-31 21:52:32 +00:00
parent 6bdacd272e
commit 0cf2b5bda7
24 changed files with 591 additions and 6 deletions

View File

@ -63,4 +63,7 @@ settings = {
"""
""",
},
"/notebooks/upload_file": {
"stream_response": True
},
}

View File

@ -63,6 +63,10 @@ def expose( view = None, rss = None ):
cherrypy.root.report_traceback()
result = dict( error = u"An error occurred when processing your request. Please try again or contact support." )
# if the result is a generator, it's streaming data, so just let CherryPy handle it
if hasattr( result, "gi_running" ):
return result
redirect = result.get( u"redirect", None )
# try using the supplied view to render the result

View File

@ -1,5 +1,7 @@
import re
import cgi
import cherrypy
from cherrypy.filters import basefilter
from datetime import datetime
from Expose import expose
from Validate import validate, Valid_string, Validation_error, Valid_bool
@ -15,6 +17,7 @@ from model.User_revision import User_revision
from view.Main_page import Main_page
from view.Json import Json
from view.Html_file import Html_file
from view.Upload_page import Upload_page
class Access_error( Exception ):
@ -31,8 +34,35 @@ class Access_error( Exception ):
)
class Upload_error( Exception ):
def __init__( self, message = None ):
if message is None:
message = u"An error occurred when uploading the file."
Exception.__init__( self, message )
self.__message = message
def to_dict( self ):
return dict(
error = self.__message
)
class File_upload_filter( basefilter.BaseFilter ):
def before_request_body( self ):
if cherrypy.request.path != "/notebooks/upload_file":
return
if cherrypy.request.method != "POST":
raise Upload_error()
# tell CherryPy not to parse the POST data itself for this URL
cherrypy.request.processRequestBody = False
class Notebooks( object ):
WHITESPACE_PATTERN = re.compile( u"\s+" )
_cpFilterList = [ File_upload_filter() ]
"""
Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL.
@ -1095,3 +1125,149 @@ class Notebooks( object ):
result[ "count" ] = count
return result
@expose( view = Upload_page )
@validate(
notebook_id = Valid_id(),
note_id = Valid_id(),
)
def upload_page( self, notebook_id, note_id ):
"""
Provide the information necessary to display the file upload page.
@type notebook_id: unicode
@param notebook_id: id of the notebook that the upload will be to
@type note_id: unicode
@param note_id: id of the note that the upload will be to
@rtype: unicode
@return: rendered HTML page
"""
return dict(
notebook_id = notebook_id,
note_id = note_id,
)
@expose()
@strongly_expire
@grab_user_id
@validate(
user_id = Valid_id( none_okay = True ),
)
def upload_file( self, user_id ):
"""
Upload a file from the client for attachment to a particular note.
@type notebook_id: unicode
@param notebook_id: id of the notebook that the upload is to
@type note_id: unicode
@param note_id: id of the note that the upload is to
@raise Access_error: the current user doesn't have access to the given notebook or note
@rtype: unicode
@return: rendered HTML page
"""
cherrypy.server.max_request_body_size = 0 # remove file size limit of 100 MB
cherrypy.response.timeout = 3600 # increase upload timeout to one hour (default is 5 min)
cherrypy.server.socket_timeout = 60 # increase socket timeout to one minute (default is 10 sec)
# TODO: increase to 8k
CHUNK_SIZE = 1#8 * 1024 # 8 Kb
headers = {}
for key, val in cherrypy.request.headers.iteritems():
headers[ key.lower() ] = val
try:
file_size = int( headers.get( "content-length", 0 ) )
except ValueError:
raise Upload_error()
if file_size <= 0:
raise Upload_error()
parsed_form = cgi.FieldStorage( fp = cherrypy.request.rfile, headers = headers, environ = { "REQUEST_METHOD": "POST" }, keep_blank_values = 1)
upload = parsed_form[ u"file" ]
notebook_id = parsed_form.getvalue( u"notebook_id" )
note_id = parsed_form.getvalue( u"note_id" )
filename = upload.filename.strip()
if not self.__users.check_access( user_id, notebook_id ):
raise Access_error()
def process_upload():
"""
Process the file upload while streaming a progress meter as it uploads.
"""
progress_bytes = 0
fraction_reported = 0.0
progress_width_em = 20
tick_increment = 0.01
progress_bar = u'<img src="/static/images/tick.png" style="width: %sem; height: 1em;" id="progress_bar" />' % \
( progress_width_em * tick_increment )
yield \
u"""
<html>
<head>
<link href="/static/css/upload.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="/static/js/MochiKit.js"></script>
<meta content="text/html; charset=UTF-8" http_equiv="content-type" />
</head>
<body>
"""
if not filename:
yield \
u"""
<div class="field_label">upload error: </div>
Please check that the filename is valid.
"""
return
base_filename = filename.split( u"/" )[ -1 ].split( u"\\" )[ -1 ]
yield \
u"""
<div class="field_label">uploading %s: </div>
<div id="progress_border">
%s
</div>
<script type="text/javascript">
function tick( fraction ) {
setElementDimensions(
"progress_bar",
{ "w": %s * fraction }, "em"
);
}
</script>
""" % ( cgi.escape( base_filename ), progress_bar, progress_width_em )
import time
while True:
chunk = upload.file.read( CHUNK_SIZE )
if not chunk: break
progress_bytes += len( chunk )
fraction_done = float( progress_bytes ) / float( file_size )
if fraction_done > fraction_reported + tick_increment:
yield '<script type="text/javascript">tick(%s)</script>;' % fraction_reported
fraction_reported += tick_increment
time.sleep(0.025) # TODO: removeme
# TODO: write to the database
if fraction_reported == 0:
yield "An error occurred when uploading the file."
return
# the file finished uploading, so fill out the progress meter to 100%
if fraction_reported < 1.0:
yield '<script type="text/javascript">tick(1.0)</script>;'
yield \
u"""
</script>
</body>
</html>
"""
upload.file.close()
cherrypy.request.rfile.close()
return process_upload()

View File

@ -10,6 +10,12 @@
background-image: url(/static/images/link_button.png);
}
#attach_button_preload {
height: 0;
overflow: hidden;
background-image: url(/static/images/attach_button.png);
}
#bold_button_preload {
height: 0;
overflow: hidden;

View File

@ -71,6 +71,12 @@ img {
background-image: url(/static/images/link_button_hover.png);
}
#attach_button_hover_preload {
height: 0;
overflow: hidden;
background-image: url(/static/images/attach_button_hover.png);
}
#bold_button_hover_preload {
height: 0;
overflow: hidden;
@ -119,6 +125,12 @@ img {
background-image: url(/static/images/link_button_down_hover.png);
}
#attach_button_down_hover_preload {
height: 0;
overflow: hidden;
background-image: url(/static/images/attach_button_down_hover.png);
}
#bold_button_down_hover_preload {
height: 0;
overflow: hidden;
@ -167,6 +179,12 @@ img {
background-image: url(/static/images/link_button_down.png);
}
#attach_button_down_preload {
height: 0;
overflow: hidden;
background-image: url(/static/images/attach_button_down.png);
}
#bold_button_down_preload {
height: 0;
overflow: hidden;
@ -398,7 +416,7 @@ img {
overflow: auto;
padding: 0.5em;
border: 1px solid #000000;
background: #ffff99;
background-color: #ffff99;
}
.pulldown_link {
@ -596,3 +614,10 @@ img {
font-weight: bold;
color: #ff6600;
}
#upload_frame {
padding: 0;
margin: 0;
width: 40em;
height: 4em;
}

46
static/css/upload.css Normal file
View File

@ -0,0 +1,46 @@
html, body {
padding: 0;
margin: 0;
line-height: 140%;
font-family: sans-serif;
background-color: #ffff99;
}
form {
margin-bottom: 0.5em;
}
div {
margin-bottom: 0.5em;
}
.field_label {
font-weight: bold;
}
.button {
border-style: outset;
border-width: 0px;
background-color: #d0e0f0;
font-size: 100%;
outline: none;
cursor: pointer;
margin-left: 0.25em;
}
.button:hover {
background-color: #ffcc66;
}
#progress_border {
border: 1px solid #000000;
background-color: #ffffff;
width: 20em;
height: 1em;
}
#tick_preload {
height: 0;
overflow: hidden;
background-image: url(/static/images/tick.png);
}

View File

@ -52,6 +52,7 @@ need to download or install anything if you just want to make a wiki.
<span id="new_note_button_preload"></span>
<span id="link_button_preload"></span>
<span id="attach_button_preload"></span>
<span id="bold_button_preload"></span>
<span id="italic_button_preload"></span>
<span id="underline_button_preload"></span>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

View File

@ -2,7 +2,8 @@ Button dimensions are 40x40 pixels.
Button fonts are Bitstream Vera Sans (regular, bold, and mono oblique). Most
buttons are at 22 pt. The link button is at 12 pt. The list buttons are at 10
pt with a -4 pixel line spacing.
pt with a -4 pixel line spacing. Down (pressed) buttons have their text offset
two pixels down and two pixels to the right.
To make the white glowing effect (which isn't present on any buttons
currently), start with black text on a transparent background in the Gimp.

BIN
static/images/paperclip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

161
static/images/paperclip.svg Normal file
View File

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48"
height="48"
id="svg79606"
sodipodi:version="0.32"
inkscape:version="0.44.1"
version="1.0"
sodipodi:docbase="/home/witten/luminotes/trunk/static/images"
sodipodi:docname="paperclip.svg">
<defs
id="defs79608">
<linearGradient
id="linearGradient5783">
<stop
style="stop-color:#d3d7cf;stop-opacity:1;"
offset="0"
id="stop5785" />
<stop
id="stop5791"
offset="0.5"
style="stop-color:#f5f5f5;stop-opacity:1;" />
<stop
style="stop-color:#bebebe;stop-opacity:1;"
offset="1"
id="stop5787" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3558">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3560" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3562" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3558"
id="radialGradient3564"
cx="21.761711"
cy="23.07144"
fx="21.761711"
fy="23.07144"
r="15.571428"
gradientTransform="matrix(0.977282,3.554943e-8,-8.305337e-10,0.651376,-0.79443,15.82896)"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5783"
id="linearGradient5789"
x1="23.505953"
y1="5.7753429"
x2="20.604948"
y2="29.85923"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="0.23529412"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.979167"
inkscape:cx="24"
inkscape:cy="24"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:grid-bbox="true"
inkscape:document-units="px"
inkscape:window-width="1400"
inkscape:window-height="1026"
inkscape:window-x="0"
inkscape:window-y="0"
stroke="#d3d7cf"
inkscape:showpageshadow="false"
gridempspacing="4" />
<metadata
id="metadata79611">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Mail Attachment</dc:title>
<dc:date>2005-11-04</dc:date>
<dc:creator>
<cc:Agent>
<dc:title>Andreas Nilsson</dc:title>
</cc:Agent>
</dc:creator>
<dc:source>http://tango-project.org</dc:source>
<dc:subject>
<rdf:Bag>
<rdf:li>attachment</rdf:li>
<rdf:li>file</rdf:li>
</rdf:Bag>
</dc:subject>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/2.0/" />
<dc:contributor>
<cc:Agent>
<dc:title>Garrett LeSage</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/2.0/">
<cc:permits
rdf:resource="http://web.resource.org/cc/Reproduction" />
<cc:permits
rdf:resource="http://web.resource.org/cc/Distribution" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Notice" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Attribution" />
<cc:permits
rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
<cc:requires
rdf:resource="http://web.resource.org/cc/ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<g
id="g3902"
transform="matrix(0.862513,-0.506035,0.506035,0.862513,-8.845153,15.44454)">
<path
inkscape:r_cy="true"
inkscape:r_cx="true"
transform="translate(0.494048,1.056164)"
d="M 21.326337,9.3278633 L 10.449186,27.94227 C 8.5266861,31.23365 9.6775753,35.481172 13.008091,37.38221 L 15.102397,38.579075 C 18.434077,40.480111 22.732254,39.341738 24.655919,36.05036 L 36.41168,15.928621 C 38.335346,12.636117 37.625044,8.6405654 34.835356,7.0477444 C 32.045435,5.4549233 28.187846,6.8452672 26.265346,10.137772 L 18.109581,24.099704 C 16.186149,27.391081 15.978909,30.871442 17.647547,31.836583 C 19.317351,32.799475 22.257398,30.893938 24.179898,27.602558 L 28.142388,20.81957"
id="path7057"
style="fill:none;stroke:#888a85;stroke-width:3.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" />
<path
inkscape:r_cy="true"
inkscape:r_cx="true"
transform="translate(0.494048,1.168493)"
d="M 21.326337,9.2155349 L 10.449186,27.829941 C 8.5266861,31.121321 9.6775753,35.368843 13.008091,37.269881 L 15.102397,38.466746 C 18.434077,40.367782 22.732254,39.229409 24.655919,35.938031 L 36.41168,15.816292 C 38.335346,12.523788 37.625044,8.528237 34.835356,6.935416 C 32.045435,5.3425949 28.187846,6.7329388 26.265346,10.025444 L 18.109581,23.987375 C 16.186149,27.278752 15.978909,30.759113 17.647547,31.724254 C 19.317351,32.687146 22.257398,30.781609 24.179898,27.490229 L 28.142388,20.707241"
id="path7053"
style="fill:none;stroke:url(#linearGradient5789);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
static/images/tick.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

View File

@ -492,6 +492,46 @@ Editor.prototype.end_link = function () {
return link;
}
Editor.prototype.insert_file_link = function ( filename, file_id ) {
// get the current selection, which is the link title
if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox
var selection = this.iframe.contentWindow.getSelection();
// if no text is selected, then insert a link with the filename as the link title
if ( selection.toString().length == 0 ) {
this.insert_html( '<span id="placeholder_title">' + filename + '</span>' );
var placeholder = withDocument( this.document, function () { return getElement( "placeholder_title" ); } );
selection.selectAllChildren( placeholder );
this.exec_command( "createLink", "/files/" + file_id );
selection.collapseToEnd();
// replace the placeholder title span with just the filename, yielding an unselected link
var link = placeholder.parentNode;
link.innerHTML = filename;
link.target = "_new";
// otherwise, just create a link with the selected text as the link title
} else {
this.exec_command( "createLink", "/files/" + file_id );
var link = this.find_link_at_cursor();
link.target = "_new";
}
} else if ( this.document.selection ) { // browsers such as IE
var range = this.document.selection.createRange();
// if no text is selected, then insert a link with the filename as the link title
if ( range.text.length == 0 ) {
range.text = filename;
range.moveStart( "character", -1 * filename.length );
range.select();
}
this.exec_command( "createLink", "/files/" + file_id );
var link = this.find_link_at_cursor();
link.target = "_new";
}
}
Editor.prototype.find_link_at_cursor = function () {
if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox
var selection = this.iframe.contentWindow.getSelection();
@ -542,6 +582,18 @@ Editor.prototype.find_link_at_cursor = function () {
return null;
}
Editor.prototype.node_at_cursor = function () {
if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox
var selection = this.iframe.contentWindow.getSelection();
return selection.anchorNode;
} else if ( this.document.selection ) { // browsers such as IE
var range = this.document.selection.createRange();
return range.parentElement();
}
return null;
}
Editor.prototype.focus = function () {
if ( /Opera/.test( navigator.userAgent ) )
this.iframe.focus();

View File

@ -253,6 +253,7 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri
connect( window, "onunload", function ( event ) { self.editor_focused( null, true ); } );
connect( "newNote", "onclick", this, "create_blank_editor" );
connect( "createLink", "onclick", this, "toggle_link_button" );
connect( "attachFile", "onclick", this, "toggle_attach_button" );
connect( "bold", "onclick", function ( event ) { self.toggle_button( event, "bold" ); } );
connect( "italic", "onclick", function ( event ) { self.toggle_button( event, "italic" ); } );
connect( "underline", "onclick", function ( event ) { self.toggle_button( event, "underline" ); } );
@ -262,6 +263,7 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri
this.make_image_button( "newNote", "new_note", true );
this.make_image_button( "createLink", "link" );
this.make_image_button( "attachFile", "attach" );
this.make_image_button( "bold" );
this.make_image_button( "italic" );
this.make_image_button( "underline" );
@ -958,6 +960,27 @@ Wiki.prototype.toggle_link_button = function ( event ) {
event.stop();
}
Wiki.prototype.toggle_attach_button = function ( event ) {
if ( this.focused_editor && this.focused_editor.read_write ) {
this.focused_editor.focus();
// if the pulldown is already open, then just close it
var pulldown_id = "upload_" + this.focused_editor.id;
var existing_div = getElement( pulldown_id );
if ( existing_div ) {
existing_div.pulldown.shutdown();
return;
}
this.clear_messages();
this.clear_pulldowns();
new Upload_pulldown( this, this.notebook_id, this.invoker, this.focused_editor, this.focused_editor.node_at_cursor() );
}
event.stop();
}
Wiki.prototype.hide_editor = function ( event, editor ) {
this.clear_messages();
this.clear_pulldowns();
@ -2186,3 +2209,58 @@ Link_pulldown.prototype.shutdown = function () {
if ( this.link )
this.link.pulldown = null;
}
function Upload_pulldown( wiki, notebook_id, invoker, editor, anchor ) {
this.anchor = anchor;
Pulldown.call( this, wiki, notebook_id, "upload_" + editor.id, anchor, editor.iframe );
wiki.down_image_button( "attachFile" );
this.invoker = invoker;
this.editor = editor;
this.iframe = createDOM( "iframe", {
"src": "/notebooks/upload_page?notebook_id=" + notebook_id + "&note_id=" + editor.id,
"frameBorder": "0",
"scrolling": "no",
"id": "upload_frame",
"name": "upload_frame"
} );
var self = this;
connect( this.iframe, "onload", function ( event ) { self.init_frame(); } );
appendChildNodes( this.div, this.iframe );
}
Upload_pulldown.prototype = new function () { this.prototype = Pulldown.prototype; };
Upload_pulldown.prototype.constructor = Upload_pulldown;
Upload_pulldown.prototype.init_frame = function () {
var self = this;
withDocument( this.iframe.contentDocument, function () {
connect( "upload_button", "onclick", function ( event ) {
withDocument( self.iframe.contentDocument, function () {
self.upload_started( getElement( "file" ).value );
} );
} );
} );
}
Upload_pulldown.prototype.upload_started = function ( filename ) {
// get the basename of the file
var pieces = filename.split( "/" );
filename = pieces[ pieces.length - 1 ];
pieces = filename.split( "\\" );
filename = pieces[ pieces.length - 1 ];
this.editor.insert_file_link( filename );
}
Upload_pulldown.prototype.shutdown = function () {
Pulldown.prototype.shutdown.call( this );
this.wiki.up_image_button( "attachFile" );
disconnectAll( this.file_input );
}

View File

@ -86,10 +86,6 @@ class Link_area( Div ):
id = u"share_notebook_link",
title = u"Share this notebook with others.",
),
Span(
u"new!",
class_ = u"new_feature_text",
),
class_ = u"link_area_item",
) or None,

View File

@ -21,6 +21,13 @@ class Toolbar( Div ):
width = u"40", height = u"40",
class_ = "image_button",
) ),
Div( Input(
type = u"image",
id = u"attachFile", title = u"attach file",
src = u"/static/images/attach_button.png",
width = u"40", height = u"40",
class_ = "image_button",
) ),
),
P(
Div( Input(
@ -73,6 +80,7 @@ class Toolbar( Div ):
Span( id = "new_note_button_hover_preload" ),
Span( id = "link_button_hover_preload" ),
Span( id = "attach_button_hover_preload" ),
Span( id = "bold_button_hover_preload" ),
Span( id = "italic_button_hover_preload" ),
Span( id = "underline_button_hover_preload" ),
@ -82,6 +90,7 @@ class Toolbar( Div ):
Span( id = "new_note_button_down_hover_preload" ),
Span( id = "link_button_down_hover_preload" ),
Span( id = "attach_button_down_hover_preload" ),
Span( id = "bold_button_down_hover_preload" ),
Span( id = "italic_button_down_hover_preload" ),
Span( id = "underline_button_down_hover_preload" ),
@ -91,6 +100,7 @@ class Toolbar( Div ):
Span( id = "new_note_button_down_preload" ),
Span( id = "link_button_down_preload" ),
Span( id = "attach_button_down_preload" ),
Span( id = "bold_button_down_preload" ),
Span( id = "italic_button_down_preload" ),
Span( id = "underline_button_down_preload" ),

26
view/Upload_page.py Normal file
View File

@ -0,0 +1,26 @@
from Tags import Html, Head, Link, Meta, Body, P, Form, Span, Input
class Upload_page( Html ):
def __init__( self, notebook_id, note_id ):
Html.__init__(
self,
Head(
Link( href = u"/static/css/upload.css", type = u"text/css", rel = u"stylesheet" ),
Meta( content = u"text/html; charset=UTF-8", http_equiv = u"content-type" ),
),
Body(
Form(
Span( u"attach file: ", class_ = u"field_label" ),
Input( type = u"file", id = u"file", name = u"file", size = u"30" ),
Input( type = u"submit", id = u"upload_button", class_ = u"button", value = u"upload" ),
Input( type = u"hidden", id = u"notebook_id", name = u"notebook_id", value = notebook_id ),
Input( type = u"hidden", id = u"note_id", name = u"note_id", value = note_id ),
action = u"/notebooks/upload_file",
method = u"post",
enctype = u"multipart/form-data",
),
P( u"Please select a file to upload." ),
Span( id = u"tick_preload" ),
),
)