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:
+
+
+ """ % ( 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",
),