diff --git a/controller/Notebooks.py b/controller/Notebooks.py index 1cd7a09..215a93f 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -672,9 +672,12 @@ class Notebooks( object ): contents = Valid_string( min = 1, max = 50000, escape_html = False ), startup = Valid_bool(), previous_revision = Valid_revision( none_okay = True ), + position_after = Valid_id( none_okay = True ), + position_before = Valid_id( none_okay = True ), user_id = Valid_id( none_okay = True ), ) - def save_note( self, notebook_id, note_id, contents, startup, previous_revision, user_id ): + def save_note( self, notebook_id, note_id, contents, startup, previous_revision = None, + position_after = None, position_before = None, user_id = None ): """ Save a new revision of the given note. This function will work both for creating a new note and for updating an existing note. If the note exists and the given contents are identical to the @@ -692,6 +695,10 @@ class Notebooks( object ): @type previous_revision: unicode or NoneType @param previous_revision: previous known revision timestamp of the provided note, or None if the note is new + @type position_after: unicode or NoneType + @param position_after: id of note to position the saved note after (optional) + @type position_before: unicode or NoneType + @param position_before: id of note to position the saved note before (optional) @type user_id: unicode or NoneType @param user_id: id of current logged-in user (if any), determined by @grab_user_id @rtype: json dict @@ -699,7 +706,7 @@ class Notebooks( object ): 'new_revision': User_revision of saved note, or None if nothing was saved 'previous_revision': User_revision immediately before new_revision, or None if the note is new 'storage_bytes': current storage usage by user - 'rank': integer rank of the saved note, or None + 'rank': float rank of the saved note, or None } @raise Access_error: the current user doesn't have access to the given notebook @raise Validation_error: one of the arguments is invalid @@ -717,22 +724,35 @@ class Notebooks( object ): if notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES: startup = True + def calculate_rank( position_after, position_before ): + after_note = position_after and self.__database.load( Note, position_after ) or None + before_note = position_before and self.__database.load( Note, position_before ) or None + + if after_note and before_note: + return ( float( after_note.rank ) + float( before_note.rank ) ) / 2.0 + elif after_note: + return float( after_note.rank ) + 1.0 + elif before_note: + return max( float( before_note.rank ) - 1.0, 0.0 ) + return 0.0 + # check whether the provided note contents have been changed since the previous revision def update_note( current_notebook, old_note, startup, user ): # the note hasn't been changed, so bail without updating it - if contents.replace( u"\n", u"" ) == old_note.contents.replace( u"\n", "" ) and startup == old_note.startup: + if not position_after and not position_before and startup == old_note.startup and \ + contents.replace( u"\n", u"" ) == old_note.contents.replace( u"\n", "" ): new_revision = None # the note has changed, so update it else: note.contents = contents note.startup = startup - if startup: - if note.rank is None: - note.rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1 - else: - note.rank = None - note.user_id = user.object_id + if position_after or position_before: + note.rank = calculate_rank( position_after, position_before ) + elif note.rank is None: + note.rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1 + + note.user_id = user.object_id new_revision = User_revision( note.revision, note.user_id, user.username ) self.__files.purge_unused( note ) @@ -760,10 +780,10 @@ class Notebooks( object ): new_revision = update_note( notebook, old_note, startup, user ) # otherwise, create a new note else: - if startup: - rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1 + if position_after or position_before: + note.rank = calculate_rank( position_after, position_before ) else: - rank = None + rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1 previous_revision = None note = Note.create( note_id, contents, notebook_id = notebook.object_id, startup = startup, rank = rank, user_id = user_id ) diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index 0e1dfd6..31b4796 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -1105,6 +1105,7 @@ class Test_notebooks( Test_controller ): notebook_id = self.notebook.object_id, note_id = self.note.object_id, contents = new_note_contents, + startup = False, ), session_id = self.session_id ) # load the note by the old revision @@ -1122,7 +1123,7 @@ class Test_notebooks( Test_controller ): assert note.title == previous_title assert note.contents == previous_contents user = self.database.load( User, self.user.object_id ) - assert user.storage_bytes == 0 + assert user.storage_bytes > 0 def test_load_note_with_previous_revision( self ): self.login() @@ -1730,11 +1731,7 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes > 0 assert result[ "storage_bytes" ] == user.storage_bytes - - if startup: - assert result[ "rank" ] == 0.0 - else: - assert result[ "rank" ] is None + assert result[ "rank" ] == 0.0 # make sure the old title can no longer be loaded result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -1758,11 +1755,7 @@ class Test_notebooks( Test_controller ): assert note.contents == new_note_contents assert note.startup == startup assert note.user_id == self.user.object_id - - if startup: - assert note.rank == 0.0 - else: - assert note.rank is None + assert note.rank == 0.0 # make sure that the correct revisions are returned and are in chronological order result = self.http_post( "/notebooks/load_note_revisions/", dict( @@ -1872,7 +1865,7 @@ class Test_notebooks( Test_controller ): assert result[ "previous_revision" ].revision == previous_revision assert result[ "previous_revision" ].user_id == self.user.object_id assert result[ "previous_revision" ].username == self.username - assert result[ "rank" ] is None + assert result[ "rank" ] == 0.0 self.login() @@ -1898,11 +1891,7 @@ class Test_notebooks( Test_controller ): assert note.contents == new_note_contents assert note.startup == startup assert note.user_id == self.user2.object_id - - if startup: - assert note.rank == 0.0 - else: - assert note.rank is None + assert note.rank == 0.0 # make sure that the correct revisions are returned and are in chronological order result = self.http_post( "/notebooks/load_note_revisions/", dict( @@ -2004,7 +1993,7 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes > 0 assert result[ "storage_bytes" ] == user.storage_bytes - assert result[ "rank" ] is None + assert result[ "rank" ] == 0.0 # make sure the old title can no longer be loaded result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -2028,11 +2017,7 @@ class Test_notebooks( Test_controller ): assert note.contents == new_note_contents.replace( u"", "" ) assert note.startup == startup assert note.user_id == self.user.object_id - - if startup: - assert note.rank == 0.0 - else: - assert note.rank is None + assert note.rank == 0.0 # make sure that the correct revisions are returned and are in chronological order result = self.http_post( "/notebooks/load_note_revisions/", dict( @@ -2081,7 +2066,7 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes > 0 assert result[ "storage_bytes" ] == user.storage_bytes - assert result[ "rank" ] is None + assert result[ "rank" ] == 0.0 # make sure the old title can no longer be loaded result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -2155,7 +2140,7 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == previous_storage_bytes assert result[ "storage_bytes" ] == 0 - assert result[ "rank" ] is None + assert result[ "rank" ] == 0.0 result = self.http_post( "/notebooks/load_note_by_title/", dict( notebook_id = self.notebook.object_id, @@ -2209,7 +2194,7 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == previous_storage_bytes assert result[ "storage_bytes" ] == 0 - assert result[ "rank" ] is None + assert result[ "rank" ] == 0.0 result = self.http_post( "/notebooks/load_note_by_title/", dict( notebook_id = self.notebook.object_id, @@ -2325,7 +2310,7 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == previous_storage_bytes assert result[ "storage_bytes" ] == 0 - assert result[ "rank" ] is None + assert result[ "rank" ] == 0.0 result = self.http_post( "/notebooks/load_note_by_title/", dict( notebook_id = self.notebook.object_id, @@ -2377,7 +2362,7 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes > 0 assert result[ "storage_bytes" ] == user.storage_bytes - assert result[ "rank" ] is None + assert result[ "rank" ] == 0.0 # make sure the first title can no longer be loaded result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -2450,11 +2435,7 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes > 0 assert result[ "storage_bytes" ] == user.storage_bytes - - if startup: - assert result[ "rank" ] == 0.0 - else: - assert result[ "rank" ] is None + assert result[ "rank" ] == 0.0 # make sure the new title is now loadable result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -2469,11 +2450,7 @@ class Test_notebooks( Test_controller ): assert note.contents == new_note.contents assert note.startup == startup assert note.user_id == self.user.object_id - - if startup: - assert note.rank == 0.0 - else: - assert note.rank is None + assert note.rank == 0.0 def test_save_new_startup_note( self ): self.test_save_new_note( startup = True ) @@ -2547,7 +2524,7 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes > 0 assert result[ "storage_bytes" ] == user.storage_bytes - assert result[ "rank" ] == None + assert result[ "rank" ] == 0.0 # make sure the new title is now loadable result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -2588,7 +2565,7 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes > 0 assert result[ "storage_bytes" ] == user.storage_bytes - assert result[ "rank" ] == None + assert result[ "rank" ] == 0.0 # make sure the new title is now loadable result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -2639,11 +2616,7 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes > 0 assert result[ "storage_bytes" ] == user.storage_bytes - - if startup: - assert result[ "rank" ] == 1.0 - else: - assert result[ "rank" ] is None + assert result[ "rank" ] == 1.0 # make sure the new title is now loadable result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -2658,11 +2631,7 @@ class Test_notebooks( Test_controller ): assert note.contents == new_note.contents assert note.startup == startup assert note.user_id == self.user.object_id - - if startup: - assert note.rank == 1 # one greater than the previous new note's rank - else: - assert note.rank is None + assert note.rank == 1.0 # one greater than the previous new note's rank def test_save_two_new_startup_notes( self ): self.test_save_two_new_notes( startup = True ) diff --git a/static/js/Editor.js b/static/js/Editor.js index 0432eaa..3d5f9e8 100644 --- a/static/js/Editor.js +++ b/static/js/Editor.js @@ -848,6 +848,7 @@ Editor.prototype.drop = function( event ) { swapDOM( hover_drop_target, this.holder ); removeElement( "note_drag_source_area" ); this.highlight(); + signal( this, "moved", this, this.previous_editor(), this.next_editor() ); } var drop_targets = getElementsByTagAndClassName( "div", "note_drop_target" ); @@ -863,6 +864,28 @@ Editor.prototype.drop = function( event ) { } } +Editor.prototype.previous_editor = function () { + var previous_holder = this.holder.previousSibling; + while ( previous_holder && previous_holder.nodeValue == "\n" ) + previous_holder = previous_holder.previousSibling; + if ( !previous_holder || !hasElementClass( previous_holder, "note_holder" ) ) return null; + var div = getFirstElementByTagAndClassName( "div", "static_note_div", previous_holder ); + if ( !div || !div.editor ) return null; + + return div.editor; +} + +Editor.prototype.next_editor = function () { + var next_holder = this.holder.nextSibling; + while ( next_holder && next_holder.nodeValue == "\n" ) + next_holder = next_holder.nextSibling; + if ( !next_holder || !hasElementClass( next_holder, "note_holder" ) ) return null; + var div = getFirstElementByTagAndClassName( "div", "static_note_div", next_holder ); + if ( !div || !div.editor ) return null; + + return div.editor; +} + Editor.prototype.key_pressed = function ( event ) { signal( this, "key_pressed", this, event ); diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 1d09ed2..09025a9 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -849,6 +849,9 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revisi connect( editor, "focused", this, "editor_focused" ); connect( editor, "mouse_hovered", function ( target ) { self.editor_mouse_hovered( editor, target ) } ); connect( editor, "grabber_pressed", function ( event ) { self.editor_focused( null ); } ); + connect( editor, "moved", function ( editor, position_after, position_before ) { + self.editor_moved( editor, position_after, position_before ); + } ); } connect( editor, "load_editor", this, "load_editor" ); @@ -1038,6 +1041,10 @@ Wiki.prototype.editor_focused = function ( editor, synchronous ) { this.update_toolbar(); } +Wiki.prototype.editor_moved = function ( editor, position_after, position_before ) { + this.save_editor( editor, false, null, null, null, position_after, position_before ); +} + Wiki.prototype.make_byline = function ( username, creation, note_id ) { if ( username == "anonymous" ) username = "admin"; @@ -1205,15 +1212,9 @@ Wiki.prototype.focus_previous_editor = function () { return; } - var previous_holder = this.focused_editor.holder.previousSibling; - while ( previous_holder && previous_holder.nodeValue == "\n" ) - previous_holder = previous_holder.previousSibling; - if ( !previous_holder || !hasElementClass( previous_holder, "note_holder" ) ) return; - var div = getFirstElementByTagAndClassName( "div", "static_note_div", previous_holder ); - if ( !div || !div.editor ) return; - + var previous_editor = this.focused_editor.previous_editor(); this.editor_focused( null ); - div.editor.highlight(); + previous_editor.highlight(); } Wiki.prototype.focus_next_editor = function () { @@ -1224,15 +1225,9 @@ Wiki.prototype.focus_next_editor = function () { return; } - var next_holder = this.focused_editor.holder.nextSibling; - while ( next_holder && next_holder.nodeValue == "\n" ) - next_holder = next_holder.nextSibling; - if ( !next_holder || !hasElementClass( next_holder, "note_holder" ) ) return; - var div = getFirstElementByTagAndClassName( "div", "static_note_div", next_holder ); - if ( !div || !div.editor ) return; - + var next_editor = this.focused_editor.next_editor(); this.editor_focused( null ); - div.editor.highlight(); + next_editor.highlight(); } Wiki.prototype.get_toolbar_image_dir = function ( always_small ) { @@ -1694,12 +1689,13 @@ Wiki.prototype.compare_versions = function( event, editor, previous_revision ) { this.load_editor( editor.title, editor.id, editor.revision, previous_revision, editor.closed ? null : editor.holder ); } -Wiki.prototype.save_editor = function ( editor, fire_and_forget, callback, synchronous, suppress_save_signal ) { +Wiki.prototype.save_editor = function ( editor, fire_and_forget, callback, synchronous, suppress_save_signal, position_after, position_before ) { if ( !editor ) editor = this.focused_editor; var self = this; - if ( editor && editor.read_write && !editor.empty() && !editor.closed && editor.dirty() ) { + if ( editor && editor.read_write && !editor.empty() && !editor.closed && + ( editor.dirty() || position_after || position_before ) ) { editor.scrape_title(); this.invoker.invoke( "/notebooks/save_note", "POST", { @@ -1707,7 +1703,9 @@ Wiki.prototype.save_editor = function ( editor, fire_and_forget, callback, synch "note_id": editor.id, "contents": editor.contents(), "startup": editor.startup, - "previous_revision": editor.revision ? editor.revision : "None" + "previous_revision": editor.revision ? editor.revision : "None", + "position_after": position_after ? position_after.id : "None", + "position_before": position_before ? position_before.id : "None" }, function ( result ) { self.update_editor_revisions( result, editor ); self.display_storage_usage( result.storage_bytes );