From 0cf2b5bda704654e4fcf163a5c044851ec05d730 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Thu, 31 Jan 2008 21:52:32 +0000 Subject: [PATCH] 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 --- config/Common.py | 3 + controller/Expose.py | 4 + controller/Notebooks.py | 176 +++++++++++++++++++++ static/css/note.css | 6 + static/css/style.css | 27 +++- static/css/upload.css | 46 ++++++ static/html/features.html | 1 + static/images/attach_button.png | Bin 0 -> 1080 bytes static/images/attach_button.xcf | Bin 0 -> 2588 bytes static/images/attach_button_down.png | Bin 0 -> 1498 bytes static/images/attach_button_down.xcf | Bin 0 -> 3761 bytes static/images/attach_button_down_hover.png | Bin 0 -> 1445 bytes static/images/attach_button_down_hover.xcf | Bin 0 -> 3739 bytes static/images/attach_button_hover.png | Bin 0 -> 1058 bytes static/images/attach_button_hover.xcf | Bin 0 -> 2588 bytes static/images/images.txt | 3 +- static/images/paperclip.png | Bin 0 -> 743 bytes static/images/paperclip.svg | 161 +++++++++++++++++++ static/images/tick.png | Bin 0 -> 150 bytes static/js/Editor.js | 52 ++++++ static/js/Wiki.js | 78 +++++++++ view/Link_area.py | 4 - view/Toolbar.py | 10 ++ view/Upload_page.py | 26 +++ 24 files changed, 591 insertions(+), 6 deletions(-) create mode 100644 static/css/upload.css create mode 100644 static/images/attach_button.png create mode 100644 static/images/attach_button.xcf create mode 100644 static/images/attach_button_down.png create mode 100644 static/images/attach_button_down.xcf create mode 100644 static/images/attach_button_down_hover.png create mode 100644 static/images/attach_button_down_hover.xcf create mode 100644 static/images/attach_button_hover.png create mode 100644 static/images/attach_button_hover.xcf create mode 100644 static/images/paperclip.png create mode 100644 static/images/paperclip.svg create mode 100644 static/images/tick.png create mode 100644 view/Upload_page.py diff --git a/config/Common.py b/config/Common.py index 53efaf0..5c93d91 100644 --- a/config/Common.py +++ b/config/Common.py @@ -63,4 +63,7 @@ settings = { """ """, }, + "/notebooks/upload_file": { + "stream_response": True + }, } diff --git a/controller/Expose.py b/controller/Expose.py index db1cab3..5d02a75 100644 --- a/controller/Expose.py +++ b/controller/Expose.py @@ -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 diff --git a/controller/Notebooks.py b/controller/Notebooks.py index 812618e..6bc5b4e 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -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'' % \ + ( progress_width_em * tick_increment ) + + yield \ + u""" + + + + + + + + """ + + if not filename: + yield \ + u""" +
upload error:
+ Please check that the filename is valid. + """ + return + + base_filename = filename.split( u"/" )[ -1 ].split( u"\\" )[ -1 ] + yield \ + u""" +
uploading %s:
+
+ %s +
+ + """ % ( 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 ';' % 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 ';' + + yield \ + u""" + + + + """ + + upload.file.close() + cherrypy.request.rfile.close() + + return process_upload() diff --git a/static/css/note.css b/static/css/note.css index f2692f2..a32249d 100644 --- a/static/css/note.css +++ b/static/css/note.css @@ -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; diff --git a/static/css/style.css b/static/css/style.css index 02b6ebf..80c1427 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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; +} diff --git a/static/css/upload.css b/static/css/upload.css new file mode 100644 index 0000000..5fe1595 --- /dev/null +++ b/static/css/upload.css @@ -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); +} diff --git a/static/html/features.html b/static/html/features.html index 0edff0b..162e8ff 100644 --- a/static/html/features.html +++ b/static/html/features.html @@ -52,6 +52,7 @@ need to download or install anything if you just want to make a wiki. + diff --git a/static/images/attach_button.png b/static/images/attach_button.png new file mode 100644 index 0000000000000000000000000000000000000000..8e7b2dc54e06a66c570fce0a12aff5d124871b17 GIT binary patch literal 1080 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 literal 0 HcmV?d00001 diff --git a/static/images/attach_button_down_hover.png b/static/images/attach_button_down_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..8950330e58c03557a9ff0bf1b5724fa50481b1b9 GIT binary patch literal 1445 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 literal 0 HcmV?d00001 diff --git a/static/images/attach_button_hover.xcf b/static/images/attach_button_hover.xcf new file mode 100644 index 0000000000000000000000000000000000000000..154c44421ba21355f68db4079549dcb7c1a8401d GIT binary patch literal 2588 zcmcgtO>Y}T7=G=ftx2f*m6WKI)|uJenccOW)HRF@Yc(X z3)96)wVa(wav^wSuV5U@U#wP|D84uwaMhPYoSAsed()>W%kIt86&@=Iz*-yoD3^dd?AMTq!-8VnjH$Q|qpOrK= z0_WhCn1l1iEj<@cl4`+Cf4DoA4z0D2dYw|Dqg+SOX?iTIf~Wn0MW^U5k+%g2ry%G# z>N+_Ai*&_=O(!vU8b23^Kxd5IgzTDA^mtV&8`t5#LdeKuSE6gZ*9U3FzJxzU0${R;V1+E97{V{uHjSY)7FeNCqt( ztsU%LNS#B6&a$ICC0S=_Pxx+bBGg$H97$45NY+{9BMVU0BQ1N8l5VIZ zPAVvtJrQ;n%()W6CYJ);7?PCRI?G95RgkgHUbyb$6YQcY)?LOryOMQw`~+*sIy;_B zbSGC!f}D}=uEw26$vTJk0NrIyO2`&r5GCszuGKEqb$?uhx4sCu0ee2nq3Ieaf#Ups2P zVth2gN6goZN7g#LzkTnB`G)amjPD;Zw}@_;(Yw+5eQLE*^m~i$MEC)HW?W%zCio$J zV3wB5!VW&MVa)nij2~_8Bln|gE)8z}f_&ZU<}Eh#JLVJ4{(SF`xAD0+P=4IWvCy@i zdHlnV9n*Q24Lx<^SF^SDu?~gCOFMaq^G9U(%yjYna=mu3dY)hB`_b`k|KGng$+r%v zGvDLEEh!WAWPXc6Ni5T4oThOzNYbRqHd|yC_MPLw0+xu6V+3Obj1@3ewrB+m9xPyq z_&7!|N??@0C~Z**3?3|CiTF6iG;I!x#pR8j88Mr^jpao#4BK?a8ms1{>8>`$*pFjZ z*Uc&OWc}(G4L8Mbtwb9sZGILk6yJ+m-S;{lOg6puK}?=FVB a+p8iv?+MPMC`*^KaL%0y2KJ~iMSlY_n0p8S literal 0 HcmV?d00001 diff --git a/static/images/images.txt b/static/images/images.txt index 1fd9dd1..16f104b 100644 --- a/static/images/images.txt +++ b/static/images/images.txt @@ -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. diff --git a/static/images/paperclip.png b/static/images/paperclip.png new file mode 100644 index 0000000000000000000000000000000000000000..3878ed58ef74de6476c8e5d2ae6bbfae3ccc18c2 GIT binary patch literal 743 zcmV?P)2txsd0d0Q(000McNliru*Z~?8Iu7YWQhNXZ0(414 zK~y-)ZIVA|6j2byznS;;Hh&Hi3}^)Rk;KA?h1e*F#WjLp7eT}-h#{RpNFx^Z79xm( zg(R(D7t@Il5rS9>T4>|jLlcAxfxzzFzW2srH_6_aX_lRt-^@4jhFYx_>Sj|*0G1=I z7g_O?_x^`-{<%raO90@(WM*1xHbaI?Z%BI>P?%UM9tzpnq|D*omTI)M; z9ACDn9dAU@VTVeKhzO)ENdzKt@W28A0|Jyzfe0W%1L6UIMIaC%mNJd1!TN@lKA%7U zu;8F-sDi3ipDQ1vQovP9n8fTl@AsJbTy^4L=Cj_1J(HNf<%FfSgF&5hn@XuBfLWW` z8O40ed-tVpb2Ui_A#4!QH2{m{Lo$NMMjwE3mHW2R>-Cn0hlZB`peTAB0I%u*haepy zI&M=NnZ*2d-e1Y{>~)@JtIqqYCNWz!wVH@0B}hm6;6KGte6uKuIRI}=V!i{oH*iX~ zv{sG0+xpnzsnu%1rgkZAG@dduJLmEs zk`kbZ*=VJ-_ulo2V!)wakCNb{; Z;2#b~Nu8;oN|XQq002ovPDHLkV1m=WN2&k- literal 0 HcmV?d00001 diff --git a/static/images/paperclip.svg b/static/images/paperclip.svg new file mode 100644 index 0000000..582d8b9 --- /dev/null +++ b/static/images/paperclip.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Mail Attachment + 2005-11-04 + + + Andreas Nilsson + + + http://tango-project.org + + + attachment + file + + + + + + Garrett LeSage + + + + + + + + + + + + + + + + + + + + diff --git a/static/images/tick.png b/static/images/tick.png new file mode 100644 index 0000000000000000000000000000000000000000..477d2e6d385d41d4472028d9076135a6a0e642dd GIT binary patch literal 150 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryoCO|{#S9GG!XV7ZFl&wkP>{XE z)7O>#2BVxfw?f^ULJpviY>8_`iF1B#Zfaf$gL6@8Vo7R>LV0FMhC)b2s)D=9mBzXV; literal 0 HcmV?d00001 diff --git a/static/js/Editor.js b/static/js/Editor.js index 011de3c..bd46c45 100644 --- a/static/js/Editor.js +++ b/static/js/Editor.js @@ -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( '' + filename + '' ); + 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(); diff --git a/static/js/Wiki.js b/static/js/Wiki.js index eee490a..78c66b1 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -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 + "¬e_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 ); +} + diff --git a/view/Link_area.py b/view/Link_area.py index 33bdcc6..8db4632 100644 --- a/view/Link_area.py +++ b/view/Link_area.py @@ -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, diff --git a/view/Toolbar.py b/view/Toolbar.py index 7c82fdf..9361906 100644 --- a/view/Toolbar.py +++ b/view/Toolbar.py @@ -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" ), diff --git a/view/Upload_page.py b/view/Upload_page.py new file mode 100644 index 0000000..09981bb --- /dev/null +++ b/view/Upload_page.py @@ -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" ), + ), + )