diff --git a/controller/Database.py b/controller/Database.py index 1519903..36cc2aa 100644 --- a/controller/Database.py +++ b/controller/Database.py @@ -391,6 +391,13 @@ class Database( object ): cache.delete( obj.cache_key ) + def uncache_many( self, Object_type, obj_ids ): + cache = self.__get_cache_connection() + if not cache: return + + for obj_id in obj_ids: + cache.delete( Persistent.make_cache_key( Object_type, obj_id ) ) + @staticmethod def generate_id(): int_id = random.getrandbits( Database.ID_BITS ) diff --git a/controller/Notebooks.py b/controller/Notebooks.py index e64d424..b22e023 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -726,12 +726,26 @@ class Notebooks( object ): position_before = None position_after = None - def calculate_rank( position_after, position_before ): + def update_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 + new_rank = float( after_note.rank ) + 1.0 + + # if necessary, increment the rank of all subsequent notes to make "room" for this note + if new_rank >= before_note.rank: + # clear the cache of before_note and all notes with subsequent rank + self.__database.uncache_many( + Note, + self.__database.select_many( + unicode, + notebook.sql_load_note_ids_starting_from_rank( before_note.rank ) + ) + ) + self.__database.execute( notebook.sql_increment_rank( before_note.rank ), commit = False ) + + return new_rank elif after_note: return float( after_note.rank ) + 1.0 elif before_note: @@ -750,7 +764,7 @@ class Notebooks( object ): note.startup = startup if position_after or position_before: - note.rank = calculate_rank( position_after, position_before ) + note.rank = update_rank( position_after, position_before ) elif note.rank is None: note.rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1 @@ -783,7 +797,7 @@ class Notebooks( object ): # otherwise, create a new note else: if position_after or position_before: - rank = calculate_rank( position_after, position_before ) + rank = update_rank( position_after, position_before ) else: rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1 diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index 3c19481..fb3b7ea 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -1987,7 +1987,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" ] == 0.5 + assert result[ "rank" ] == 1.0 # make sure the old title can no longer be loaded result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -2011,7 +2011,7 @@ class Test_notebooks( Test_controller ): assert note.contents == new_note_contents assert note.startup == False assert note.user_id == self.user.object_id - assert note.rank == 0.5 + assert note.rank == 1.0 # make sure that the correct revisions are returned and are in chronological order result = self.http_post( "/notebooks/load_note_revisions/", dict( @@ -2029,6 +2029,15 @@ class Test_notebooks( Test_controller ): assert revisions[ 2 ].user_id == self.user.object_id assert revisions[ 2 ].username == self.username + # the position_before note should have its rank incremented to make way for the new note + result = self.http_post( "/notebooks/load_note/", dict( + notebook_id = self.notebook.object_id, + note_id = before_note_id, + ), session_id = self.session_id ) + + note = result[ "note" ] + assert note.rank == 2.0 + def test_save_note_in_notebook_with_read_write_for_own_notes( self, after_note_id = None, before_note_id = None ): self.login() @@ -2914,7 +2923,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" ] == 0.5 + assert result[ "rank" ] == 1.0 # make sure the new title is now loadable result = self.http_post( "/notebooks/load_note_by_title/", dict( @@ -2929,7 +2938,108 @@ class Test_notebooks( Test_controller ): assert note.contents == new_note.contents assert note.startup == False assert note.user_id == self.user.object_id - assert note.rank == 0.5 + assert note.rank == 1.0 + + # the position_before note should have its rank incremented to make way for the new note + result = self.http_post( "/notebooks/load_note/", dict( + notebook_id = self.notebook.object_id, + note_id = before_note_id, + ), session_id = self.session_id ) + + note = result[ "note" ] + assert note.rank == 2.0 + + def test_save_new_note_with_position_after_and_before_and_gap( self ): + self.login() + + temp_note_id = u"someid0" + new_note_contents = u"

temp

" + result = self.http_post( "/notebooks/save_note/", dict( + notebook_id = self.notebook.object_id, + note_id = temp_note_id, + contents = new_note_contents, + startup = False, + previous_revision = None, + ), session_id = self.session_id ) + + assert result[ "rank" ] == 0.0 + + after_note_id = u"someid1" + new_note_contents = u"

after this

" + result = self.http_post( "/notebooks/save_note/", dict( + notebook_id = self.notebook.object_id, + note_id = after_note_id, + contents = new_note_contents, + startup = False, + previous_revision = None, + position_before = temp_note_id + ), session_id = self.session_id ) + + assert result[ "rank" ] == -1.0 + + before_note_id = u"someid2" + new_note_contents = u"

before this

" + result = self.http_post( "/notebooks/save_note/", dict( + notebook_id = self.notebook.object_id, + note_id = before_note_id, + contents = new_note_contents, + startup = False, + previous_revision = None, + ), session_id = self.session_id ) + + assert result[ "rank" ] == 1.0 + + self.http_post( "/notebooks/delete_note/", dict( + notebook_id = self.notebook.object_id, + note_id = temp_note_id, + ), session_id = self.session_id ) + + # save a completely new note + new_note = Note.create( "55", u"

newest title

foo" ) + previous_revision = new_note.revision + result = self.http_post( "/notebooks/save_note/", dict( + notebook_id = self.notebook.object_id, + note_id = new_note.object_id, + contents = new_note.contents, + startup = False, + previous_revision = None, + position_after = after_note_id, + position_before = before_note_id, + ), session_id = self.session_id ) + + assert result[ "new_revision" ] + assert result[ "new_revision" ] != previous_revision + assert result[ "new_revision" ].user_id == self.user.object_id + assert result[ "new_revision" ].username == self.username + assert result[ "previous_revision" ] == None + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes > 0 + assert result[ "storage_bytes" ] == user.storage_bytes + assert result[ "rank" ] == 0.0 + + # make sure the new title is now loadable + result = self.http_post( "/notebooks/load_note_by_title/", dict( + notebook_id = self.notebook.object_id, + note_title = new_note.title, + ), session_id = self.session_id ) + + note = result[ "note" ] + + assert note.object_id == new_note.object_id + assert note.title == new_note.title + assert note.contents == new_note.contents + assert note.startup == False + assert note.user_id == self.user.object_id + assert note.rank == 0.0 + + # the position_before note should have its rank incremented to make way for the new note + result = self.http_post( "/notebooks/load_note/", dict( + notebook_id = self.notebook.object_id, + note_id = before_note_id, + ), session_id = self.session_id ) + + note = result[ "note" ] + assert note.rank == 1.0 def test_save_new_note_in_notebook_with_read_write_for_own_notes( self, after_note_id = None, before_note_id = None ): self.login() diff --git a/model/Notebook.py b/model/Notebook.py index e38b393..2b37cae 100644 --- a/model/Notebook.py +++ b/model/Notebook.py @@ -2,6 +2,8 @@ import re from copy import copy from Note import Note from Persistent import Persistent, quote, quote_fuzzy +from datetime import datetime +from pytz import utc class Notebook( Persistent ): @@ -357,6 +359,41 @@ class Notebook( Persistent ): order by tag.name; """ % ( quote( self.object_id ), quote( user_id ) ) + def sql_load_note_ids_starting_from_rank( self, start_note_rank ): + """ + Return a SQL string to load a list of all the note ids with rank greater than or equal to the + given rank. + """ + return \ + """ + select + id + from + note_current + where + notebook_id = %s and + rank is not null and + rank >= %s; + """ % ( quote( self.object_id ), start_note_rank ) + + def sql_increment_rank( self, start_note_rank ): + """ + Return a SQL string to increment the rank for every note in this notebook (in rank order) + starting from the given note rank. Notes before the given note rank are not updated. + """ + return \ + """ + update + note_current + set + rank = rank + 1, + revision = %s + where + notebook_id = %s and + rank is not null and + rank >= %s; + """ % ( quote( datetime.now( tz = utc ) ), quote( self.object_id ), start_note_rank ) + def to_dict( self ): d = Persistent.to_dict( self ) diff --git a/static/js/Editor.js b/static/js/Editor.js index 374d628..f41a789 100644 --- a/static/js/Editor.js +++ b/static/js/Editor.js @@ -332,8 +332,12 @@ Editor.prototype.add_selection_bookmark = function () { // if the current range is not within this editor's static note div, then bail if ( range.startContainer == document || range.endContainer == document ) return null; - if ( !isChildNode( range.startContainer.parentNode, this.div ) || !isChildNode( range.endContainer.parentNode, this.div ) ) + try { + if ( !isChildNode( range.startContainer.parentNode, this.div ) || !isChildNode( range.endContainer.parentNode, this.div ) ) + return null; + } catch ( e ) { return null; + } // mark the nodes that are start and end containers for the current range. we have to mark the // parent node instead of the start/end container itself, because text nodes can't have classes diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 97d4f19..763aa43 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -1046,6 +1046,16 @@ Wiki.prototype.editor_focused = function ( editor, synchronous, remove_empty ) { Wiki.prototype.editor_moved = function ( editor, position_after, position_before ) { this.save_editor( editor, false, null, null, null, position_after, position_before ); + + // reset the revision for each open editor. this is because the server is updating the revisions + // on the server while reordering the ntoes. and we don't want to have a stale idea of what the + // current revision is for a given editor + var divs = getElementsByTagAndClassName( "div", "static_note_div" ); + for ( var i in divs ) { + var editor = divs[ i ].editor; + editor.revision = null; + editor.user_revisions = new Array(); + } } Wiki.prototype.make_byline = function ( username, creation, note_id ) { @@ -1760,7 +1770,8 @@ Wiki.prototype.update_editor_revisions = function ( result, editor ) { // if the server's idea of the previous revision doesn't match the client's, then someone has // gone behind our back and saved the editor's note from another window - if ( result.previous_revision && result.previous_revision.revision != client_previous_revision ) { + if ( result.previous_revision && client_previous_revision && + result.previous_revision.revision != client_previous_revision ) { var compare_button = createDOM( "input", { "type": "button", "class": "message_button",