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",