Fixed several edge cases with re-ranking notes during a reorder. Now updating several notes' ranks on every reorder instead of making successively more precise fractional ranks.
This commit is contained in:
parent
5446eeec5d
commit
a8af8b3fb3
|
@ -391,6 +391,13 @@ class Database( object ):
|
||||||
|
|
||||||
cache.delete( obj.cache_key )
|
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
|
@staticmethod
|
||||||
def generate_id():
|
def generate_id():
|
||||||
int_id = random.getrandbits( Database.ID_BITS )
|
int_id = random.getrandbits( Database.ID_BITS )
|
||||||
|
|
|
@ -726,12 +726,26 @@ class Notebooks( object ):
|
||||||
position_before = None
|
position_before = None
|
||||||
position_after = 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
|
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
|
before_note = position_before and self.__database.load( Note, position_before ) or None
|
||||||
|
|
||||||
if after_note and before_note:
|
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:
|
elif after_note:
|
||||||
return float( after_note.rank ) + 1.0
|
return float( after_note.rank ) + 1.0
|
||||||
elif before_note:
|
elif before_note:
|
||||||
|
@ -750,7 +764,7 @@ class Notebooks( object ):
|
||||||
note.startup = startup
|
note.startup = startup
|
||||||
|
|
||||||
if position_after or position_before:
|
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:
|
elif note.rank is None:
|
||||||
note.rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1
|
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
|
# otherwise, create a new note
|
||||||
else:
|
else:
|
||||||
if position_after or position_before:
|
if position_after or position_before:
|
||||||
rank = calculate_rank( position_after, position_before )
|
rank = update_rank( position_after, position_before )
|
||||||
else:
|
else:
|
||||||
rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1
|
rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1
|
||||||
|
|
||||||
|
|
|
@ -1987,7 +1987,7 @@ class Test_notebooks( Test_controller ):
|
||||||
user = self.database.load( User, self.user.object_id )
|
user = self.database.load( User, self.user.object_id )
|
||||||
assert user.storage_bytes > 0
|
assert user.storage_bytes > 0
|
||||||
assert result[ "storage_bytes" ] == user.storage_bytes
|
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
|
# make sure the old title can no longer be loaded
|
||||||
result = self.http_post( "/notebooks/load_note_by_title/", dict(
|
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.contents == new_note_contents
|
||||||
assert note.startup == False
|
assert note.startup == False
|
||||||
assert note.user_id == self.user.object_id
|
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
|
# make sure that the correct revisions are returned and are in chronological order
|
||||||
result = self.http_post( "/notebooks/load_note_revisions/", dict(
|
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 ].user_id == self.user.object_id
|
||||||
assert revisions[ 2 ].username == self.username
|
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 ):
|
def test_save_note_in_notebook_with_read_write_for_own_notes( self, after_note_id = None, before_note_id = None ):
|
||||||
self.login()
|
self.login()
|
||||||
|
|
||||||
|
@ -2914,7 +2923,7 @@ class Test_notebooks( Test_controller ):
|
||||||
user = self.database.load( User, self.user.object_id )
|
user = self.database.load( User, self.user.object_id )
|
||||||
assert user.storage_bytes > 0
|
assert user.storage_bytes > 0
|
||||||
assert result[ "storage_bytes" ] == user.storage_bytes
|
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
|
# make sure the new title is now loadable
|
||||||
result = self.http_post( "/notebooks/load_note_by_title/", dict(
|
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.contents == new_note.contents
|
||||||
assert note.startup == False
|
assert note.startup == False
|
||||||
assert note.user_id == self.user.object_id
|
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"<h3>temp</h3>"
|
||||||
|
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"<h3>after this</h3>"
|
||||||
|
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"<h3>before this</h3>"
|
||||||
|
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"<h3>newest title</h3>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 ):
|
def test_save_new_note_in_notebook_with_read_write_for_own_notes( self, after_note_id = None, before_note_id = None ):
|
||||||
self.login()
|
self.login()
|
||||||
|
|
|
@ -2,6 +2,8 @@ import re
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from Note import Note
|
from Note import Note
|
||||||
from Persistent import Persistent, quote, quote_fuzzy
|
from Persistent import Persistent, quote, quote_fuzzy
|
||||||
|
from datetime import datetime
|
||||||
|
from pytz import utc
|
||||||
|
|
||||||
|
|
||||||
class Notebook( Persistent ):
|
class Notebook( Persistent ):
|
||||||
|
@ -357,6 +359,41 @@ class Notebook( Persistent ):
|
||||||
order by tag.name;
|
order by tag.name;
|
||||||
""" % ( quote( self.object_id ), quote( user_id ) )
|
""" % ( 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 ):
|
def to_dict( self ):
|
||||||
d = Persistent.to_dict( self )
|
d = Persistent.to_dict( self )
|
||||||
|
|
||||||
|
|
|
@ -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 the current range is not within this editor's static note div, then bail
|
||||||
if ( range.startContainer == document || range.endContainer == document )
|
if ( range.startContainer == document || range.endContainer == document )
|
||||||
return null;
|
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;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// mark the nodes that are start and end containers for the current range. we have to mark the
|
// 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
|
// parent node instead of the start/end container itself, because text nodes can't have classes
|
||||||
|
|
|
@ -1046,6 +1046,16 @@ Wiki.prototype.editor_focused = function ( editor, synchronous, remove_empty ) {
|
||||||
|
|
||||||
Wiki.prototype.editor_moved = function ( editor, position_after, position_before ) {
|
Wiki.prototype.editor_moved = function ( editor, position_after, position_before ) {
|
||||||
this.save_editor( editor, false, null, null, null, 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 ) {
|
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
|
// 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
|
// 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", {
|
var compare_button = createDOM( "input", {
|
||||||
"type": "button",
|
"type": "button",
|
||||||
"class": "message_button",
|
"class": "message_button",
|
||||||
|
|
Reference in New Issue