witten
/
luminotes
Archived
1
0
Fork 0

* 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:
Dan Helfman 2008-05-03 04:29:23 +00:00
parent 42ae3e0ba1
commit 97c373561d
7 changed files with 196 additions and 17 deletions

5
NEWS
View File

@ -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.

View File

@ -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,
)

View File

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

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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();
}