* Added replace_contents() to model.Note to set the contents without updating the revision or anything else.
* Added new optional previous_revision params to default(), contents() and load_note() in controller.Notebooks. These use Html_differ() to generate and return diffs. * Updated Wiki.js: * provide previous_revision when a revision is opened in a new window/tab * call load_note() when two revisions when a revision is clicked in Changes_pulldown * update compare_versions() to display a diff instead of opening the two revisions separately * update load_editor(): * update all invocations of load_editor(), including in Editor.js, to accept a new previous_revision argument * modify load_editor() to use the previous_revision argument (when supplied) to load a diff
This commit is contained in:
parent
42ae3e0ba1
commit
97c373561d
5
NEWS
5
NEWS
|
@ -1,3 +1,8 @@
|
|||
1.3.9: May 2, 2008
|
||||
* When viewing a note's revision, changes since the previous revision are now
|
||||
shown in red strikeout (deletions/modifications) and green text
|
||||
(additions/modifications).
|
||||
|
||||
1.3.8: April 29, 2008
|
||||
* Can now load children links for a note that's in the trash.
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from Database import Valid_id, Valid_revision, end_transaction
|
|||
from Users import grab_user_id, Access_error
|
||||
from Expire import strongly_expire
|
||||
from Html_nuker import Html_nuker
|
||||
from Html_differ import Html_differ
|
||||
from model.Notebook import Notebook
|
||||
from model.Note import Note
|
||||
from model.Invite import Invite
|
||||
|
@ -59,13 +60,15 @@ class Notebooks( object ):
|
|||
note_id = Valid_id(),
|
||||
parent_id = Valid_id(),
|
||||
revision = Valid_revision(),
|
||||
previous_revision = Valid_revision( none_okay = True ),
|
||||
rename = Valid_bool(),
|
||||
deleted_id = Valid_id(),
|
||||
preview = Valid_string(),
|
||||
user_id = Valid_id( none_okay = True ),
|
||||
)
|
||||
def default( self, notebook_id, note_id = None, parent_id = None, revision = None, rename = False,
|
||||
deleted_id = None, preview = None, user_id = None ):
|
||||
def default( self, notebook_id, note_id = None, parent_id = None, revision = None,
|
||||
previous_revision = None, rename = False, deleted_id = None, preview = None,
|
||||
user_id = None ):
|
||||
"""
|
||||
Provide the information necessary to display the page for a particular notebook. If a
|
||||
particular note id is given without a revision, then the most recent version of that note is
|
||||
|
@ -79,6 +82,8 @@ class Notebooks( object ):
|
|||
@param parent_id: id of parent notebook to this notebook (optional)
|
||||
@type revision: unicode or NoneType
|
||||
@param revision: revision timestamp of the provided note (optional)
|
||||
@type previous_revision: unicode or NoneType
|
||||
@param previous_revision: older revision timestamp to diff with the given revision (optional)
|
||||
@type rename: bool or NoneType
|
||||
@param rename: whether this is a new notebook and should be renamed (optional, defaults to False)
|
||||
@type deleted_id: unicode or NoneType
|
||||
|
@ -115,7 +120,7 @@ class Notebooks( object ):
|
|||
else:
|
||||
raise Access_error()
|
||||
|
||||
result.update( self.contents( notebook_id, note_id, revision, read_write, owner, user_id ) )
|
||||
result.update( self.contents( notebook_id, note_id, revision, previous_revision, read_write, owner, user_id ) )
|
||||
result[ "parent_id" ] = parent_id
|
||||
if revision:
|
||||
result[ "note_read_write" ] = False
|
||||
|
@ -138,7 +143,8 @@ class Notebooks( object ):
|
|||
|
||||
return result
|
||||
|
||||
def contents( self, notebook_id, note_id = None, revision = None, read_write = True, owner = True, user_id = None ):
|
||||
def contents( self, notebook_id, note_id = None, revision = None, previous_revision = None,
|
||||
read_write = True, owner = True, user_id = None ):
|
||||
"""
|
||||
Return the startup notes for the given notebook. Optionally include a single requested note as
|
||||
well.
|
||||
|
@ -149,6 +155,8 @@ class Notebooks( object ):
|
|||
@param note_id: id of single note in this notebook to return (optional)
|
||||
@type revision: unicode or NoneType
|
||||
@param revision: revision timestamp of the provided note (optional)
|
||||
@type previous_revision: unicode or NoneType
|
||||
@param previous_revision: older revision timestamp to diff with the given revision (optional)
|
||||
@type read_write: bool or NoneType
|
||||
@param read_write: whether the notebook should be returned as read-write (optional, defaults to True)
|
||||
@type owner: bool or NoneType
|
||||
|
@ -190,6 +198,12 @@ class Notebooks( object ):
|
|||
note = None
|
||||
else:
|
||||
raise Access_error()
|
||||
|
||||
# if two revisions were provided, then make the returned note's contents into a diff
|
||||
if note and revision and previous_revision:
|
||||
previous_note = self.__database.load( Note, note_id, previous_revision )
|
||||
if previous_note and previous_note.contents:
|
||||
note.replace_contents( Html_differ().diff( previous_note.contents, note.contents ) )
|
||||
else:
|
||||
note = None
|
||||
|
||||
|
@ -281,10 +295,11 @@ class Notebooks( object ):
|
|||
notebook_id = Valid_id(),
|
||||
note_id = Valid_id(),
|
||||
revision = Valid_revision(),
|
||||
previous_revision = Valid_revision( none_okay = True ),
|
||||
summarize = Valid_bool(),
|
||||
user_id = Valid_id( none_okay = True ),
|
||||
)
|
||||
def load_note( self, notebook_id, note_id, revision = None, summarize = False, user_id = None ):
|
||||
def load_note( self, notebook_id, note_id, revision = None, previous_revision = None, summarize = False, user_id = None ):
|
||||
"""
|
||||
Return the information on a particular note by its id.
|
||||
|
||||
|
@ -294,6 +309,8 @@ class Notebooks( object ):
|
|||
@param note_id: id of note to return
|
||||
@type revision: unicode or NoneType
|
||||
@param revision: revision timestamp of the note (optional)
|
||||
@type previous_revision: unicode or NoneType
|
||||
@param previous_revision: older revision timestamp to diff with the given revision (optional)
|
||||
@type summarize: bool or NoneType
|
||||
@param summarize: True to return a summary of the note's contents, False to return full text
|
||||
(optional, defaults to False)
|
||||
|
@ -330,6 +347,11 @@ class Notebooks( object ):
|
|||
|
||||
raise Access_error()
|
||||
|
||||
if note and revision and previous_revision:
|
||||
previous_note = self.__database.load( Note, note_id, previous_revision )
|
||||
if previous_note and previous_note.contents:
|
||||
note.replace_contents( Html_differ().diff( previous_note.contents, note.contents ) )
|
||||
|
||||
return dict(
|
||||
note = summarize and self.summarize_note( note ) or note,
|
||||
)
|
||||
|
|
|
@ -428,6 +428,56 @@ class Test_notebooks( Test_controller ):
|
|||
user = self.database.load( User, self.user.object_id )
|
||||
assert user.storage_bytes == 0
|
||||
|
||||
def test_default_with_note_and_previous_revision( self ):
|
||||
self.login()
|
||||
|
||||
previous_revision = self.note.revision
|
||||
self.note.contents = u"<h3>my title</h3>foo blah"
|
||||
self.database.save( self.note )
|
||||
|
||||
result = self.http_get(
|
||||
"/notebooks/%s?note_id=%s&revision=%s&previous_revision=%s" % (
|
||||
self.notebook.object_id,
|
||||
self.note.object_id,
|
||||
quote( unicode( self.note.revision ) ),
|
||||
quote( unicode( previous_revision ) ),
|
||||
),
|
||||
session_id = self.session_id,
|
||||
)
|
||||
|
||||
assert result.get( u"user" ).object_id == self.user.object_id
|
||||
assert len( result.get( u"notebooks" ) ) == 3
|
||||
assert result.get( u"notebooks" )[ 0 ].object_id == self.notebook.object_id
|
||||
assert result.get( u"notebooks" )[ 0 ].read_write == True
|
||||
assert result.get( u"notebooks" )[ 0 ].owner == True
|
||||
assert result.get( u"login_url" ) is None
|
||||
assert result.get( u"logout_url" )
|
||||
assert result.get( u"rate_plan" )
|
||||
assert result.get( u"notebook" ).object_id == self.notebook.object_id
|
||||
assert result.get( u"notebook" ).read_write == True
|
||||
assert result.get( u"notebook" ).owner == True
|
||||
assert len( result.get( u"startup_notes" ) ) == 1
|
||||
assert result[ "total_notes_count" ] == 2
|
||||
|
||||
assert result.get( "notes" )
|
||||
assert len( result.get( "notes" ) ) == 1
|
||||
assert result.get( u"notes" )[ 0 ].object_id == self.note.object_id
|
||||
assert result.get( u"notes" )[ 0 ].revision == self.note.revision
|
||||
assert result.get( u"notes" )[ 0 ].contents == u'<h3>my title</h3><ins class="diff">foo </ins>blah'
|
||||
assert result.get( u"parent_id" ) == None
|
||||
assert result.get( u"note_read_write" ) == False
|
||||
assert len( result.get( "recent_notes" ) ) == 2
|
||||
assert result.get( "recent_notes" )[ 0 ].object_id == self.note.object_id
|
||||
assert result.get( "recent_notes" )[ 1 ].object_id == self.note2.object_id
|
||||
|
||||
invites = result[ "invites" ]
|
||||
assert len( invites ) == 1
|
||||
invite = invites[ 0 ]
|
||||
assert invite.object_id == self.invite.object_id
|
||||
|
||||
user = self.database.load( User, self.user.object_id )
|
||||
assert user.storage_bytes == 0
|
||||
|
||||
def test_default_with_parent( self ):
|
||||
self.login()
|
||||
|
||||
|
@ -604,6 +654,46 @@ class Test_notebooks( Test_controller ):
|
|||
user = self.database.load( User, self.user.object_id )
|
||||
assert user.storage_bytes == 0
|
||||
|
||||
def test_contents_with_note_and_previous_revision( self ):
|
||||
previous_revision = self.note.revision
|
||||
self.note.contents = u"<h3>my title</h3>foo blah"
|
||||
self.database.save( self.note )
|
||||
|
||||
result = cherrypy.root.notebooks.contents(
|
||||
notebook_id = self.notebook.object_id,
|
||||
note_id = self.note.object_id,
|
||||
revision = unicode( self.note.revision ),
|
||||
previous_revision = unicode( previous_revision ),
|
||||
user_id = self.user.object_id,
|
||||
)
|
||||
self.login()
|
||||
|
||||
notebook = result[ "notebook" ]
|
||||
startup_notes = result[ "startup_notes" ]
|
||||
assert result[ "total_notes_count" ] == 2
|
||||
|
||||
invites = result[ "invites" ]
|
||||
assert len( invites ) == 1
|
||||
invite = invites[ 0 ]
|
||||
assert invite.object_id == self.invite.object_id
|
||||
|
||||
assert notebook.object_id == self.notebook.object_id
|
||||
assert notebook.read_write == True
|
||||
assert notebook.owner == True
|
||||
assert len( startup_notes ) == 1
|
||||
assert startup_notes[ 0 ].object_id == self.note.object_id
|
||||
|
||||
notes = result[ "notes" ]
|
||||
|
||||
assert notes
|
||||
assert len( notes ) == 1
|
||||
note = notes[ 0 ]
|
||||
assert note.object_id == self.note.object_id
|
||||
assert note.revision == self.note.revision
|
||||
assert note.contents == u'<h3>my title</h3><ins class="diff">foo </ins>blah'
|
||||
user = self.database.load( User, self.user.object_id )
|
||||
assert user.storage_bytes == 0
|
||||
|
||||
def test_contents_with_different_invites( self ):
|
||||
# create an invite with a different email address from the previous
|
||||
invite = Invite.create(
|
||||
|
@ -870,6 +960,42 @@ class Test_notebooks( Test_controller ):
|
|||
user = self.database.load( User, self.user.object_id )
|
||||
assert user.storage_bytes == 0
|
||||
|
||||
def test_load_note_with_previous_revision( self ):
|
||||
self.login()
|
||||
|
||||
# update the note to generate a new revision
|
||||
previous_revision = self.note.revision
|
||||
previous_title = self.note.title
|
||||
previous_contents = self.note.contents
|
||||
new_note_contents = u"<h3>my title</h3>foo blah"
|
||||
result = self.http_post( "/notebooks/save_note/", dict(
|
||||
notebook_id = self.notebook.object_id,
|
||||
note_id = self.note.object_id,
|
||||
contents = new_note_contents,
|
||||
startup = False,
|
||||
previous_revision = self.note.revision,
|
||||
), session_id = self.session_id )
|
||||
|
||||
new_revision = result[ "new_revision" ].revision
|
||||
|
||||
# load the note by the new revision, providing the previous revision as well
|
||||
result = self.http_post( "/notebooks/load_note/", dict(
|
||||
notebook_id = self.notebook.object_id,
|
||||
note_id = self.note.object_id,
|
||||
revision = unicode( new_revision ),
|
||||
previous_revision = previous_revision,
|
||||
), session_id = self.session_id )
|
||||
|
||||
note = result[ "note" ]
|
||||
|
||||
# assert that we get a composite diff of the two revisions
|
||||
assert note.object_id == self.note.object_id
|
||||
assert note.revision == new_revision
|
||||
assert note.title == previous_title
|
||||
assert note.contents == u'<h3>my title</h3><ins class="diff">foo </ins>blah'
|
||||
user = self.database.load( User, self.user.object_id )
|
||||
assert user.storage_bytes > 0
|
||||
|
||||
def test_load_note_without_login( self ):
|
||||
result = self.http_post( "/notebooks/load_note/", dict(
|
||||
notebook_id = self.notebook.object_id,
|
||||
|
|
|
@ -96,6 +96,9 @@ class Note( Persistent ):
|
|||
else:
|
||||
self.__title = None
|
||||
|
||||
def replace_contents( self, contents ):
|
||||
self.__contents = contents
|
||||
|
||||
def __set_summary( self, summary ):
|
||||
self.__summary = summary
|
||||
|
||||
|
|
|
@ -87,6 +87,25 @@ class Test_note( object ):
|
|||
assert self.note.user_id == self.user_id
|
||||
assert self.note.creation == self.creation
|
||||
|
||||
def test_replace_contents( self ):
|
||||
new_contents = u"<h3>new</h3>new blah"
|
||||
original_revision = self.note.revision
|
||||
original_title = self.note.title
|
||||
|
||||
self.note.replace_contents( new_contents )
|
||||
|
||||
# nothing should change but the contents itself
|
||||
assert self.note.revision == original_revision
|
||||
assert self.note.contents == new_contents
|
||||
assert self.note.summary == None
|
||||
assert self.note.title == original_title
|
||||
assert self.note.notebook_id == self.notebook_id
|
||||
assert self.note.startup == self.startup
|
||||
assert self.note.deleted_from_id == None
|
||||
assert self.note.rank == self.rank
|
||||
assert self.note.user_id == self.user_id
|
||||
assert self.note.creation == self.creation
|
||||
|
||||
def test_set_summary( self ):
|
||||
summary = u"summary goes here..."
|
||||
original_revision = self.note.revision
|
||||
|
|
|
@ -392,7 +392,7 @@ Editor.prototype.mouse_clicked = function ( event ) {
|
|||
var query = parse_query( link );
|
||||
var title = link_title( link, query );
|
||||
var id = query.note_id;
|
||||
signal( self, "load_editor", title, id, null, link, self.iframe );
|
||||
signal( self, "load_editor", title, id, null, null, link, self.iframe );
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -356,7 +356,7 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri
|
|||
var share_notebook_link = getElement( "share_notebook_link" );
|
||||
if ( share_notebook_link ) {
|
||||
connect( share_notebook_link, "onclick", function ( event ) {
|
||||
self.load_editor( "share this notebook", "null", null, null, getElement( "notes_top" ) );
|
||||
self.load_editor( "share this notebook", "null", null, null, null, getElement( "notes_top" ) );
|
||||
event.stop();
|
||||
} );
|
||||
}
|
||||
|
@ -403,7 +403,7 @@ Wiki.prototype.create_blank_editor = function ( event ) {
|
|||
signal( this, "note_added", editor );
|
||||
}
|
||||
|
||||
Wiki.prototype.load_editor = function ( note_title, note_id, revision, link, position_after ) {
|
||||
Wiki.prototype.load_editor = function ( note_title, note_id, revision, previous_revision, link, position_after ) {
|
||||
if ( this.notebook.name == "trash" && !revision ) {
|
||||
this.display_message( "If you'd like to use this note, try undeleting it first.", undefined, position_after );
|
||||
return;
|
||||
|
@ -495,7 +495,8 @@ Wiki.prototype.load_editor = function ( note_title, note_id, revision, link, pos
|
|||
"/notebooks/load_note", "GET", {
|
||||
"notebook_id": this.notebook_id,
|
||||
"note_id": note_id,
|
||||
"revision": revision
|
||||
"revision": revision,
|
||||
"previous_revision": previous_revision
|
||||
},
|
||||
function ( result ) { self.parse_loaded_editor( result, note_title, revision, link, position_after ); }
|
||||
);
|
||||
|
@ -1200,7 +1201,7 @@ Wiki.prototype.undelete_editor_via_undo = function( event, editor, position_afte
|
|||
|
||||
this.startup_notes[ editor.id ] = true;
|
||||
this.increment_total_notes_count();
|
||||
this.load_editor( "Note not found.", editor.id, null, null, position_after );
|
||||
this.load_editor( "Note not found.", editor.id, null, null, null, position_after );
|
||||
}
|
||||
|
||||
event.stop();
|
||||
|
@ -1221,7 +1222,7 @@ Wiki.prototype.undelete_editor_via_undelete = function( event, note_id, position
|
|||
|
||||
this.startup_notes[ note_id ] = true;
|
||||
this.increment_total_notes_count();
|
||||
this.load_editor( "Note not found.", note_id, null, null, position_after );
|
||||
this.load_editor( "Note not found.", note_id, null, null, null, position_after );
|
||||
|
||||
event.stop();
|
||||
}
|
||||
|
@ -1237,9 +1238,8 @@ Wiki.prototype.undelete_notebook = function( event, notebook_id ) {
|
|||
Wiki.prototype.compare_versions = function( event, editor, previous_revision ) {
|
||||
this.clear_pulldowns();
|
||||
|
||||
// display the two revisions for comparison by the user
|
||||
this.load_editor( editor.title, editor.id, previous_revision, null, editor.closed ? null : editor.iframe );
|
||||
this.load_editor( editor.title, editor.id, null, null, editor.closed ? null : editor.iframe );
|
||||
// display a diff between the two revisions for examination by the user
|
||||
this.load_editor( editor.title, editor.id, editor.revision, previous_revision, editor.closed ? null : editor.iframe );
|
||||
}
|
||||
|
||||
Wiki.prototype.save_editor = function ( editor, fire_and_forget, callback, synchronous, suppress_save_signal ) {
|
||||
|
@ -2167,10 +2167,12 @@ function Changes_pulldown( wiki, notebook_id, invoker, editor ) {
|
|||
var self = this;
|
||||
for ( var i = 0; i < user_revisions.length - 1; ++i ) { // -1 to skip the oldest revision
|
||||
var user_revision = user_revisions[ i ];
|
||||
var previous_revision = user_revisions[ i + 1 ];
|
||||
|
||||
var short_revision = this.wiki.brief_revision( user_revision.revision );
|
||||
var href = "/notebooks/" + this.notebook_id + "?" + queryString(
|
||||
[ "note_id", "revision" ],
|
||||
[ this.editor.id, user_revision.revision ]
|
||||
[ "note_id", "revision", "previous_revision" ],
|
||||
[ this.editor.id, user_revision.revision, previous_revision.revision ]
|
||||
);
|
||||
|
||||
var link = createDOM(
|
||||
|
@ -2181,6 +2183,7 @@ function Changes_pulldown( wiki, notebook_id, invoker, editor ) {
|
|||
|
||||
this.links.push( link );
|
||||
link.revision = user_revision.revision;
|
||||
link.previous_revision = previous_revision.revision;
|
||||
connect( link, "onclick", function ( event ) { self.link_clicked( event, self.editor.id ); } );
|
||||
appendChildNodes( this.div, link );
|
||||
appendChildNodes( this.div, createDOM( "br" ) );
|
||||
|
@ -2192,7 +2195,8 @@ Changes_pulldown.prototype.constructor = Changes_pulldown;
|
|||
|
||||
Changes_pulldown.prototype.link_clicked = function( event, note_id ) {
|
||||
var revision = event.target().revision;
|
||||
this.wiki.load_editor( "Revision not found.", note_id, revision, null, this.editor.iframe );
|
||||
var previous_revision = event.target().previous_revision;
|
||||
this.wiki.load_editor( "Revision not found.", note_id, revision, previous_revision, null, this.editor.iframe );
|
||||
event.stop();
|
||||
}
|
||||
|
||||
|
|
Reference in New Issue