witten
/
luminotes
Archived
1
0
Fork 0

Factored out file upload methods from Notebooks to new Files controller.

Changed file link insertion code to reuse existing link creation code.
This commit is contained in:
Dan Helfman 2008-02-01 19:17:10 +00:00
parent 0cf2b5bda7
commit e56503903b
7 changed files with 273 additions and 244 deletions

224
controller/Files.py Normal file
View File

@ -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'<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>
<table><tr>
<td><div id="progress_border">
%s
</div></td>
<td></td>
<td><span id="status"></span></td>
</tr></table>
<script type="text/javascript">
function tick( fraction ) {
setElementDimensions(
"progress_bar",
{ "w": %s * fraction }, "em"
);
if ( fraction == 1.0 )
replaceChildNodes( "status", "100%%" );
else
replaceChildNodes( "status", Math.floor( fraction * 100.0 ) + "%%" );
}
</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

@ -1,7 +1,5 @@
import re import re
import cgi
import cherrypy import cherrypy
from cherrypy.filters import basefilter
from datetime import datetime from datetime import datetime
from Expose import expose from Expose import expose
from Validate import validate, Valid_string, Validation_error, Valid_bool 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.Main_page import Main_page
from view.Json import Json from view.Json import Json
from view.Html_file import Html_file from view.Html_file import Html_file
from view.Upload_page import Upload_page
class Access_error( Exception ): 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 ): class Notebooks( object ):
WHITESPACE_PATTERN = re.compile( u"\s+" ) WHITESPACE_PATTERN = re.compile( u"\s+" )
_cpFilterList = [ File_upload_filter() ]
""" """
Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL. Controller for dealing with notebooks and their notes, corresponding to the "/notebooks" URL.
""" """
@ -1125,149 +1094,3 @@ class Notebooks( object ):
result[ "count" ] = count result[ "count" ] = count
return result 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

@ -5,6 +5,7 @@ from Expire import strongly_expire
from Validate import validate, Valid_int, Valid_string from Validate import validate, Valid_int, Valid_string
from Notebooks import Notebooks from Notebooks import Notebooks
from Users import Users, grab_user_id from Users import Users, grab_user_id
from Files import Files
from Database import Valid_id from Database import Valid_id
from model.Note import Note from model.Note import Note
from model.Notebook import Notebook from model.Notebook import Notebook
@ -43,6 +44,7 @@ class Root( object ):
settings[ u"global" ].get( u"luminotes.rate_plans", [] ), settings[ u"global" ].get( u"luminotes.rate_plans", [] ),
) )
self.__notebooks = Notebooks( database, self.__users ) self.__notebooks = Notebooks( database, self.__users )
self.__files = Files( database, self.__users )
@expose( Main_page ) @expose( Main_page )
@grab_user_id @grab_user_id
@ -353,3 +355,4 @@ class Root( object ):
database = property( lambda self: self.__database ) database = property( lambda self: self.__database )
notebooks = property( lambda self: self.__notebooks ) notebooks = property( lambda self: self.__notebooks )
users = property( lambda self: self.__users ) users = property( lambda self: self.__users )
files = property( lambda self: self.__files )

View File

@ -39,6 +39,10 @@ div {
height: 1em; height: 1em;
} }
td {
vertical-align: top;
}
#tick_preload { #tick_preload {
height: 0; height: 0;
overflow: hidden; overflow: hidden;

View File

@ -416,7 +416,7 @@ Editor.prototype.empty = function () {
return ( scrapeText( this.document.body ).length == 0 ); 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 // get the current selection, which is the link title
if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox
var selection = this.iframe.contentWindow.getSelection(); 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" ); } ); var placeholder = withDocument( this.document, function () { return getElement( "placeholder_title" ); } );
selection.selectAllChildren( placeholder ); selection.selectAllChildren( placeholder );
this.exec_command( "createLink", "/notebooks/" + this.notebook_id + "?note_id=new" ); this.exec_command( "createLink", url );
selection.collapseToEnd(); selection.collapseToEnd();
// hack to prevent Firefox from erasing spaces before links that happen to be at the end of list items // 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 // otherwise, just create a link with the selected text as the link title
} else { } else {
this.link_started = null; 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(); return this.find_link_at_cursor();
} }
} else if ( this.document.selection ) { // browsers such as IE } else if ( this.document.selection ) { // browsers such as IE
@ -455,16 +455,24 @@ Editor.prototype.start_link = function () {
range.text = " "; range.text = " ";
range.moveStart( "character", -1 ); range.moveStart( "character", -1 );
range.select(); 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(); this.link_started = this.find_link_at_cursor();
} else { } else {
this.link_started = null; 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(); 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 () { Editor.prototype.end_link = function () {
this.link_started = null; this.link_started = null;
var link = this.find_link_at_cursor(); var link = this.find_link_at_cursor();
@ -492,46 +500,6 @@ Editor.prototype.end_link = function () {
return link; 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 () { Editor.prototype.find_link_at_cursor = function () {
if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox if ( this.iframe.contentWindow && this.iframe.contentWindow.getSelection ) { // browsers such as Firefox
var selection = this.iframe.contentWindow.getSelection(); var selection = this.iframe.contentWindow.getSelection();
@ -582,18 +550,6 @@ Editor.prototype.find_link_at_cursor = function () {
return null; 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 () { Editor.prototype.focus = function () {
if ( /Opera/.test( navigator.userAgent ) ) if ( /Opera/.test( navigator.userAgent ) )
this.iframe.focus(); this.iframe.focus();

View File

@ -932,7 +932,21 @@ Wiki.prototype.update_toolbar = function() {
this.update_button( "title", "h3", node_names ); this.update_button( "title", "h3", node_names );
this.update_button( "insertUnorderedList", "ul", node_names ); this.update_button( "insertUnorderedList", "ul", node_names );
this.update_button( "insertOrderedList", "ol", 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 ) { Wiki.prototype.toggle_link_button = function ( event ) {
@ -975,7 +989,7 @@ Wiki.prototype.toggle_attach_button = function ( event ) {
this.clear_messages(); this.clear_messages();
this.clear_pulldowns(); 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(); event.stop();
@ -2210,16 +2224,17 @@ Link_pulldown.prototype.shutdown = function () {
this.link.pulldown = null; this.link.pulldown = null;
} }
function Upload_pulldown( wiki, notebook_id, invoker, editor, anchor ) { function Upload_pulldown( wiki, notebook_id, invoker, editor ) {
this.anchor = anchor; 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" ); wiki.down_image_button( "attachFile" );
this.invoker = invoker; this.invoker = invoker;
this.editor = editor; this.editor = editor;
this.iframe = createDOM( "iframe", { this.iframe = createDOM( "iframe", {
"src": "/notebooks/upload_page?notebook_id=" + notebook_id + "&note_id=" + editor.id, "src": "/files/upload_page?notebook_id=" + notebook_id + "&note_id=" + editor.id,
"frameBorder": "0", "frameBorder": "0",
"scrolling": "no", "scrolling": "no",
"id": "upload_frame", "id": "upload_frame",
@ -2237,10 +2252,11 @@ Upload_pulldown.prototype.constructor = Upload_pulldown;
Upload_pulldown.prototype.init_frame = function () { Upload_pulldown.prototype.init_frame = function () {
var self = this; 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 ) { connect( "upload_button", "onclick", function ( event ) {
withDocument( self.iframe.contentDocument, function () { withDocument( doc, function () {
self.upload_started( getElement( "file" ).value ); self.upload_started( getElement( "file" ).value );
} ); } );
} ); } );
@ -2254,7 +2270,10 @@ Upload_pulldown.prototype.upload_started = function ( filename ) {
pieces = filename.split( "\\" ); pieces = filename.split( "\\" );
filename = pieces[ pieces.length - 1 ]; 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 () { Upload_pulldown.prototype.shutdown = function () {

View File

@ -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"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"notebook_id", name = u"notebook_id", value = notebook_id ),
Input( type = u"hidden", id = u"note_id", name = u"note_id", value = note_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", method = u"post",
enctype = u"multipart/form-data", enctype = u"multipart/form-data",
), ),