From e56503903b27bc8c3f4c9140c61ee34528279d1f Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 1 Feb 2008 19:17:10 +0000 Subject: [PATCH] Factored out file upload methods from Notebooks to new Files controller. Changed file link insertion code to reuse existing link creation code. --- controller/Files.py | 224 ++++++++++++++++++++++++++++++++++++++++ controller/Notebooks.py | 177 ------------------------------- controller/Root.py | 3 + static/css/upload.css | 4 + static/js/Editor.js | 70 +++---------- static/js/Wiki.js | 37 +++++-- view/Upload_page.py | 2 +- 7 files changed, 273 insertions(+), 244 deletions(-) create mode 100644 controller/Files.py diff --git a/controller/Files.py b/controller/Files.py new file mode 100644 index 0000000..5cb5bcd --- /dev/null +++ b/controller/Files.py @@ -0,0 +1,224 @@ +import cgi +import cherrypy +from cherrypy.filters import basefilter +from Expose import expose +from Validate import validate +from Database import Valid_id +from Users import grab_user_id +from Expire import strongly_expire +from view.Upload_page import Upload_page + + +class Access_error( Exception ): + def __init__( self, message = None ): + if message is None: + message = u"Sorry, you don't have access to do that. Please make sure you're logged in as the correct user." + + Exception.__init__( self, message ) + self.__message = message + + def to_dict( self ): + return dict( + error = self.__message + ) + + +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 != "/files/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 Files( object ): + _cpFilterList = [ File_upload_filter() ] + + """ + Controller for dealing with uploaded files, corresponding to the "/files" URL. + """ + def __init__( self, database, users ): + """ + Create a new Files object. + + @type database: controller.Database + @param database: database that files are stored in + @type users: controller.Users + @param users: controller for all users + @rtype: Files + @return: newly constructed Files + """ + self.__database = database + self.__users = users + + @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/controller/Notebooks.py b/controller/Notebooks.py index 6bc5b4e..ea21ff5 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -1,7 +1,5 @@ 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 @@ -17,7 +15,6 @@ 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 ): @@ -34,36 +31,8 @@ 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. """ @@ -1125,149 +1094,3 @@ 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/controller/Root.py b/controller/Root.py index 6687f01..fd2bfe7 100644 --- a/controller/Root.py +++ b/controller/Root.py @@ -5,6 +5,7 @@ from Expire import strongly_expire from Validate import validate, Valid_int, Valid_string from Notebooks import Notebooks from Users import Users, grab_user_id +from Files import Files from Database import Valid_id from model.Note import Note from model.Notebook import Notebook @@ -43,6 +44,7 @@ class Root( object ): settings[ u"global" ].get( u"luminotes.rate_plans", [] ), ) self.__notebooks = Notebooks( database, self.__users ) + self.__files = Files( database, self.__users ) @expose( Main_page ) @grab_user_id @@ -353,3 +355,4 @@ class Root( object ): database = property( lambda self: self.__database ) notebooks = property( lambda self: self.__notebooks ) users = property( lambda self: self.__users ) + files = property( lambda self: self.__files ) diff --git a/static/css/upload.css b/static/css/upload.css index 5fe1595..8de266f 100644 --- a/static/css/upload.css +++ b/static/css/upload.css @@ -39,6 +39,10 @@ div { height: 1em; } +td { + vertical-align: top; +} + #tick_preload { height: 0; overflow: hidden; diff --git a/static/js/Editor.js b/static/js/Editor.js index bd46c45..7d30e9a 100644 --- a/static/js/Editor.js +++ b/static/js/Editor.js @@ -416,7 +416,7 @@ Editor.prototype.empty = function () { return ( scrapeText( this.document.body ).length == 0 ); } -Editor.prototype.start_link = function () { +Editor.prototype.insert_link = function ( url ) { // 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(); @@ -428,7 +428,7 @@ Editor.prototype.start_link = function () { var placeholder = withDocument( this.document, function () { return getElement( "placeholder_title" ); } ); selection.selectAllChildren( placeholder ); - this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" ); + this.exec_command( "createLink", url ); selection.collapseToEnd(); // hack to prevent Firefox from erasing spaces before links that happen to be at the end of list items @@ -443,7 +443,7 @@ Editor.prototype.start_link = function () { // otherwise, just create a link with the selected text as the link title } else { this.link_started = null; - this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" ); + this.exec_command( "createLink", url ); return this.find_link_at_cursor(); } } else if ( this.document.selection ) { // browsers such as IE @@ -455,16 +455,24 @@ Editor.prototype.start_link = function () { range.text = " "; range.moveStart( "character", -1 ); range.select(); - this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" ); + this.exec_command( "createLink", url ); this.link_started = this.find_link_at_cursor(); } else { this.link_started = null; - this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" ); + this.exec_command( "createLink", url ); return this.find_link_at_cursor(); } } } +Editor.prototype.start_link = function () { + return this.insert_link( "/notebooks/" + this.notebook_id + "?note_id=new" ); +} + +Editor.prototype.start_file_link = function () { + return this.insert_link( "/files/new" ); +} + Editor.prototype.end_link = function () { this.link_started = null; var link = this.find_link_at_cursor(); @@ -492,46 +500,6 @@ 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(); @@ -582,18 +550,6 @@ 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 78c66b1..17156f7 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -932,7 +932,21 @@ Wiki.prototype.update_toolbar = function() { this.update_button( "title", "h3", node_names ); this.update_button( "insertUnorderedList", "ul", node_names ); this.update_button( "insertOrderedList", "ol", node_names ); - this.update_button( "createLink", "a", node_names ); + + var link = this.focused_editor.find_link_at_cursor(); + if ( link ) { + // determine whether the link is a note link or a file link + if ( link.target || !/\/files\//.test( link.href ) ) { + this.down_image_button( "createLink" ); + this.up_image_button( "attachFile" ); + } else { + this.up_image_button( "createLink" ); + this.down_image_button( "attachFile" ); + } + } else { + this.up_image_button( "createLink" ); + this.up_image_button( "attachFile" ); + } } Wiki.prototype.toggle_link_button = function ( event ) { @@ -975,7 +989,7 @@ Wiki.prototype.toggle_attach_button = function ( event ) { this.clear_messages(); this.clear_pulldowns(); - new Upload_pulldown( this, this.notebook_id, this.invoker, this.focused_editor, this.focused_editor.node_at_cursor() ); + new Upload_pulldown( this, this.notebook_id, this.invoker, this.focused_editor ); } event.stop(); @@ -2210,16 +2224,17 @@ Link_pulldown.prototype.shutdown = function () { this.link.pulldown = null; } -function Upload_pulldown( wiki, notebook_id, invoker, editor, anchor ) { - this.anchor = anchor; +function Upload_pulldown( wiki, notebook_id, invoker, editor ) { + editor.start_file_link(); + this.link = editor.find_link_at_cursor(); - Pulldown.call( this, wiki, notebook_id, "upload_" + editor.id, anchor, editor.iframe ); + Pulldown.call( this, wiki, notebook_id, "upload_" + editor.id, this.link, 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, + "src": "/files/upload_page?notebook_id=" + notebook_id + "¬e_id=" + editor.id, "frameBorder": "0", "scrolling": "no", "id": "upload_frame", @@ -2237,10 +2252,11 @@ Upload_pulldown.prototype.constructor = Upload_pulldown; Upload_pulldown.prototype.init_frame = function () { var self = this; + var doc = this.iframe.contentDocument || this.iframe.contentWindow.document; - withDocument( this.iframe.contentDocument, function () { + withDocument( doc, function () { connect( "upload_button", "onclick", function ( event ) { - withDocument( self.iframe.contentDocument, function () { + withDocument( doc, function () { self.upload_started( getElement( "file" ).value ); } ); } ); @@ -2254,7 +2270,10 @@ Upload_pulldown.prototype.upload_started = function ( filename ) { pieces = filename.split( "\\" ); filename = pieces[ pieces.length - 1 ]; - this.editor.insert_file_link( filename ); + // the current title is blank, replace the title with the upload's filename + if ( link_title( this.link ) == "" ) + replaceChildNodes( this.link, this.editor.document.createTextNode( filename ) ); + // TODO: set the link's href to the file } Upload_pulldown.prototype.shutdown = function () { diff --git a/view/Upload_page.py b/view/Upload_page.py index 09981bb..58b76b3 100644 --- a/view/Upload_page.py +++ b/view/Upload_page.py @@ -16,7 +16,7 @@ class Upload_page( Html ): 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", + action = u"/files/upload_file", method = u"post", enctype = u"multipart/form-data", ),